Compare commits

..

689 Commits

Author SHA1 Message Date
psychedelicious
f8dadc8c0a chore: release v5.0.0.dev13 2024-09-06 21:33:24 +10:00
psychedelicious
d421eaeff0 feat(ui): move seed out of advanced, hide HRF settings 2024-09-06 21:27:58 +10:00
psychedelicious
d434e80e99 chore(ui): lint 2024-09-06 21:27:58 +10:00
psychedelicious
918ed5c264 feat(ui): tweak padding on entity group header 2024-09-06 21:27:58 +10:00
psychedelicious
2f98c876df feat(ui): use plurals for entity group header hidden tooltip 2024-09-06 21:27:58 +10:00
psychedelicious
2b9f9f3d68 feat(ui): move delete entity button down to entity list item 2024-09-06 21:27:58 +10:00
psychedelicious
f326bfe1b0 feat(ui): add fit bbox to layers 2024-09-06 21:27:58 +10:00
psychedelicious
c57966ec38 fix(ui): tidy incorrect component name 2024-09-06 21:27:58 +10:00
psychedelicious
3c5bf48810 feat(ui): do not allow invoke while transforming or filtering 2024-09-06 21:27:58 +10:00
psychedelicious
c0db830398 feat(ui): do not allow transform, filter or merge while staging 2024-09-06 21:27:58 +10:00
psychedelicious
3e3e00b24d fix(ui): prevent stage scale/size from being invalid 2024-09-06 21:27:58 +10:00
psychedelicious
640af85c93 fix(ui): do not save filtered previews to gallery 2024-09-06 21:27:58 +10:00
psychedelicious
81060eef71 feat(ui): filter UI layout 2024-09-06 21:27:58 +10:00
psychedelicious
ea193903ee feat(ui): revised entity list action bars
- Global action bar on top
- Selected Entity action bar below
2024-09-06 21:27:58 +10:00
psychedelicious
5a251a4876 feat(ui): fit bbox to stage on canvas reset 2024-09-06 21:27:58 +10:00
psychedelicious
ce93fdd076 chore(ui): lint 2024-09-06 21:27:58 +10:00
psychedelicious
788e03fbc3 feat(ui): reworked image context menu
- Add `Open in Viewer`
- Remove `Send to Image to Image`
- Fix `Send to Canvas`
- Split out logic for composability
2024-09-06 21:27:57 +10:00
psychedelicious
8cb1584379 feat(ui): restore aspect ratio preview component 2024-09-06 21:27:57 +10:00
psychedelicious
63a2b6cdfe fix(ui): transformer rendered behind layer objects 2024-09-06 21:27:57 +10:00
psychedelicious
b193bc7b14 feat(ui): inverted shift behavior for transformer 2024-09-06 21:27:57 +10:00
psychedelicious
dce488c691 fix(ui): ignore filters when calculating bbox 2024-09-06 21:27:57 +10:00
psychedelicious
615b09d453 feat(ui): cancel by destination, not origin 2024-09-06 21:27:57 +10:00
psychedelicious
1dd521a792 chore(ui): typegen 2024-09-06 21:27:57 +10:00
psychedelicious
f6a703c367 feat(app): cancel by destination, not origin
When resetting the canvas or staging area, we don't want to cancel generations that are going to the gallery - only those going to the canvas.

Thus the method should not cancel by origin, but instead cancel by destination.

Update the queue method and route.
2024-09-06 21:27:57 +10:00
psychedelicious
5a69b62a1a fix(ui): scaled size not correctly reset when canvas reset 2024-09-06 21:27:57 +10:00
psychedelicious
c4b9ba8e12 feat(ui): use black bg when rasterizing control images 2024-09-06 21:27:57 +10:00
psychedelicious
c04790158f fix(ui): ignore Konva filters when previewing filter 2024-09-06 21:27:57 +10:00
psychedelicious
a0ed7baf73 fix(ui): filter preview accidentally committed to layer 2024-09-06 21:27:57 +10:00
psychedelicious
f96148ca52 feat(ui): improved transparency effect
Use the min of each pixel's alpha value and lightness for the output alpha. This prevents artifacts when using the transparency effect, especially with non-black pixels with low alpha.
2024-09-06 21:27:57 +10:00
psychedelicious
96786ed62b chore: release v4.2.9.dev12 2024-09-06 21:27:57 +10:00
psychedelicious
cc9cc62707 fix(ui): missing translation 2024-09-06 21:27:57 +10:00
psychedelicious
07cf5807be fix(ui): save to gallery uses auto-add board 2024-09-06 21:27:57 +10:00
psychedelicious
013414e962 fix(ui): cancel transform/filter when deleting entity 2024-09-06 21:27:57 +10:00
psychedelicious
10bba49e28 chore(ui): lint 2024-09-06 21:27:57 +10:00
psychedelicious
7888b4913a feat(ui): iterate on state flow and rendering 2
- Rely on redux + reselect more
- Remove all nanostores that simply "mirrored" redux state in favor of direct subscriptions to redux store
- Add abstractions for creating redux subs and running selectors
- Add `initialize` method to CanvasModuleBase, for post-instantiation tasks
- Reduce local caching of state in modules to a minimum
2024-09-06 21:27:57 +10:00
psychedelicious
27b00cedc5 feat(ui): iterate on state flow and rendering 2024-09-06 21:27:57 +10:00
psychedelicious
0bfdd5acb8 feat(ui): slight layout change for staging area toolbar 2024-09-06 21:27:57 +10:00
psychedelicious
6e6629ad74 feat(ui): clean up adapter API 2024-09-06 21:27:57 +10:00
psychedelicious
fe257e3e46 feat(ui): streamlined state flow 2024-09-06 21:27:57 +10:00
psychedelicious
0b1dc365d0 fix(ui): handle optimal dimension when resetting canvas 2024-09-06 21:27:57 +10:00
psychedelicious
c75a933b17 feat(ui): background and staging area modules have own store subscription and render themselves 2024-09-06 21:27:57 +10:00
psychedelicious
f1525c2a82 feat(ui): make rendering methods not need args
They should pull from the entity's state directly. This allows more freedom with updating the canvas.
2024-09-06 21:27:57 +10:00
psychedelicious
8266c10c53 feat(ui): restore size of invoke button 2024-09-06 21:27:57 +10:00
psychedelicious
34554f8555 tidy(ui): remove unnecessary awaits in rendering module 2024-09-06 21:27:57 +10:00
psychedelicious
200dbbbf55 tidy(ui): rename some classes to better represent their responsibilities 2024-09-06 21:27:57 +10:00
psychedelicious
5a30ff6045 feat(ui): abstract out CanvasEntityAdapterBase
Things were getting to complex to reason about & classes a bit complicated. Trying to simplify...
2024-09-06 21:27:57 +10:00
psychedelicious
73e8837d45 feat(ui): revise entity rendering flow 2024-09-06 21:27:57 +10:00
psychedelicious
6ba78e7632 tidy(ui): remove unused id on konva nodes 2024-09-06 21:27:57 +10:00
psychedelicious
736fabe327 tidy(ui): remove commented code 2024-09-06 21:27:57 +10:00
psychedelicious
ad53169eba tidy(ui): remove extraneous docstrings 2024-09-06 21:27:57 +10:00
psychedelicious
0a22e6b5a1 feat(ui): clean up unused tool module state 2024-09-06 21:27:57 +10:00
psychedelicious
5eb9e2ba04 tidy(ui): disable isDebugging flag on root component 2024-09-06 21:27:57 +10:00
psychedelicious
376a626419 fix(ui): unable to drag while transforming after switching tools 2024-09-06 21:27:57 +10:00
psychedelicious
5d089ecc77 feat(ui): prevent layer interactions when transforming or filtering 2024-09-06 21:27:57 +10:00
psychedelicious
3ed85d821b feat(ui): add compositeMaskedRegions setting 2024-09-06 21:27:57 +10:00
psychedelicious
7c29caa245 tidy(ui): merge tool slice, sendToCanvas into settings slice 2024-09-06 21:27:57 +10:00
psychedelicious
6161c71aa7 build(ui): add csstype dev dependency 2024-09-06 21:27:57 +10:00
psychedelicious
f7e68ac6d6 feat(ui): clean up tool preview rendering 2024-09-06 21:27:57 +10:00
psychedelicious
5074c97683 feat(ui): tool buttons are only disabled when currently selected 2024-09-06 21:27:57 +10:00
psychedelicious
bb6b5c4941 feat(ui): better types on CanvasStateApiModule.getEntity 2024-09-06 21:27:57 +10:00
psychedelicious
ff12f16343 feat(ui): update default logging context path to be string 2024-09-06 21:27:57 +10:00
psychedelicious
397038e6e8 tidy(ui): mark canvas module attrs readonly 2024-09-06 21:27:57 +10:00
psychedelicious
fcacc0f70a chore: release v4.2.9.dev11 2024-09-06 21:27:57 +10:00
psychedelicious
3ed4cf63ae feat(ui): tidy stateApi atoms & add docstrings 2024-09-06 21:27:57 +10:00
psychedelicious
4b753941e4 feat(ui): streamline manager -> react transform interface 2024-09-06 21:27:57 +10:00
psychedelicious
dbff929be5 tidy(ui): remove unused $isProcessingTransform atom 2024-09-06 21:27:57 +10:00
psychedelicious
44be058ac7 docs(ui): docstrings for $canvasCache 2024-09-06 21:27:57 +10:00
psychedelicious
437129cf70 feat(ui): tweak bookmark verbiage 2024-09-06 21:27:57 +10:00
psychedelicious
2e4a48b71c feat(ui): move transformer state to nanostores
This provides some free reactivity for this canvas-manager-managed state.
2024-09-06 21:27:57 +10:00
psychedelicious
618473db02 fix(ui): transform should ignore konva filters (e.g. transparency effect) 2024-09-06 21:27:57 +10:00
psychedelicious
4f4eee76b9 feat(ui): add fit to bbox as transform helper 2024-09-06 21:27:57 +10:00
psychedelicious
1628eb5900 tidy(ui): transformer organisation 2024-09-06 21:27:57 +10:00
psychedelicious
92c754df79 fix(ui): disable merge visible when 1 or fewer layers of type 2024-09-06 21:27:57 +10:00
psychedelicious
921ade4b0a feat(ui): brush preview opacity at 0.5 when drawing on mask 2024-09-06 21:27:57 +10:00
psychedelicious
c38b05c002 chore(ui): lint 2024-09-06 21:27:57 +10:00
psychedelicious
277708d69e fix(ui): edge cases in quick switch, simpler logic 2024-09-06 21:27:57 +10:00
psychedelicious
02061faaf0 chore(ui): lint 2024-09-06 21:27:57 +10:00
psychedelicious
a4fb8eb171 feat(ui): add bookmark for quick switch 2024-09-06 21:27:57 +10:00
psychedelicious
d3c2432bfb fix(ui): force dims on scaled bbox when manual scaling + locked aspect ratio
Closes #5590
2024-09-06 21:27:57 +10:00
psychedelicious
0c3e2c45f1 feat(ui): "Control Layers" -> "Layers" 2024-09-06 21:27:57 +10:00
psychedelicious
d019dbdbd3 feat(ui): "IP Adapter" -> "Global IP Adapter" 2024-09-06 21:27:57 +10:00
psychedelicious
2538b348c7 tidy(ui): canvas hotkey hooks 2024-09-06 21:27:57 +10:00
psychedelicious
420178c09f feat(ui): add alt+[ and alt+] hotkeys to cycle through layers 2024-09-06 21:27:57 +10:00
psychedelicious
75ecc56ca5 feat(ui): add layer quick switch
Q toggles between the last-selected layers.
2024-09-06 21:27:57 +10:00
psychedelicious
874b96c67d feat(ui): bbox hotkey is c 2024-09-06 21:27:57 +10:00
psychedelicious
b0eddd19d4 fix(ui): select nonexistent entity 2024-09-06 21:27:57 +10:00
psychedelicious
cb9d0bce56 feat(ui): brush & eraser width ui/ux
Use same pattern as canvas scale & opacity sliders w/ scaled slider values for precision at low values.
2024-09-06 21:27:57 +10:00
psychedelicious
fd6d080fb0 tidy(ui): canvas scale & entity opacity sliders 2024-09-06 21:27:57 +10:00
psychedelicious
8f6e6960e0 feat(ui): hotkeys for brush/eraser size 2024-09-06 21:27:57 +10:00
psychedelicious
58064a1ea5 feat(ui): use default IP adapter when creating IP adapter 2024-09-06 21:27:57 +10:00
psychedelicious
0d94a89d98 tidy(ui): organise files 2024-09-06 21:27:57 +10:00
psychedelicious
eb4d447e2f feat(ui): remove object count from entity title
This was used for troubleshooting only.
2024-09-06 21:27:57 +10:00
psychedelicious
487422a53f tidy(ui): misc cleanup 2024-09-06 21:27:57 +10:00
psychedelicious
b24f8e2b26 docs(ui): docstrings for classes (wip) 2024-09-06 21:27:57 +10:00
psychedelicious
fd22ff7c9f feat(ui): revised canvas module base class
Big cleanup. Makes these classes easier to implement, lots of comments and docstrings to clarify how it all works.

- Add default implementations for `destroy`, `repr` and `getLoggingContext`
- Tidy individual module configs
- Update `CanvasManager.buildLogger` to accept a canvas module as the arg
- Add `CanvasManager.buildPath`
2024-09-06 21:27:57 +10:00
psychedelicious
9cd5107955 feat(ui): split canvas tool previews into modules 2024-09-06 21:27:57 +10:00
psychedelicious
fd7f0056e3 fix(ui): reject on dataURLToImageData 2024-09-06 21:27:57 +10:00
psychedelicious
b1673b9222 fix(ui): correctly set last cursor pos to null 2024-09-06 21:27:57 +10:00
psychedelicious
f4e8799561 chore: release v4.2.9.dev10 2024-09-06 21:27:57 +10:00
psychedelicious
6f85743996 feat(ui): remove entity list context menu (again)
stupid events
2024-09-06 21:27:57 +10:00
psychedelicious
243e7c954e fix(ui): entity groups not collapsing 2024-09-06 21:27:57 +10:00
psychedelicious
33d25c7977 chore: release v4.2.9.dev9 2024-09-06 21:27:57 +10:00
psychedelicious
d11251f380 fix(ui): entity opacity number input focus prevents slider from opening 2024-09-06 21:27:57 +10:00
psychedelicious
d5e5cae398 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-09-06 21:27:57 +10:00
psychedelicious
fa80452abf fix(ui): save to gallery rect too large
Was including all layer types in the rect - only want the raster layers.
2024-09-06 21:27:57 +10:00
psychedelicious
c9e396bee9 fix(ui): canvasToBlob not raising error correctly 2024-09-06 21:27:57 +10:00
psychedelicious
fd6de78746 feat(ui): add save to gallery button 2024-09-06 21:27:57 +10:00
psychedelicious
6096c8ed0a fix(ui): fix getRectUnion util, add some tests 2024-09-06 21:27:57 +10:00
psychedelicious
5126ec114a 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-09-06 21:27:57 +10:00
psychedelicious
221c51b498 fix(ui): correct labels for generation tab origin 2024-09-06 21:27:57 +10:00
psychedelicious
28db39a932 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-09-06 21:27:57 +10:00
psychedelicious
5512f80066 tidy(ui): organise tool module 2024-09-06 21:27:57 +10:00
psychedelicious
70e27d6fbb fix(ui): staging hotkeys enabled at wrong times 2024-09-06 21:27:57 +10:00
psychedelicious
e91149111e fix(ui): incorrect batch origin preventing progress/staging 2024-09-06 21:27:57 +10:00
psychedelicious
ad7919b3ec feat(ui): restore minimal HUD 2024-09-06 21:27:57 +10:00
psychedelicious
908f4117fc feat(ui): remove unused asPreview for StageComponent 2024-09-06 21:27:57 +10:00
psychedelicious
87b294708a chore(ui): lint 2024-09-06 21:27:57 +10:00
psychedelicious
a0be7c47cd chore: release v4.2.9.dev8 2024-09-06 21:27:57 +10:00
psychedelicious
87bc3f5f0a 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-09-06 21:27:57 +10:00
psychedelicious
14dea8bad3 chore(ui): typegen 2024-09-06 21:27:57 +10:00
psychedelicious
9ced9efc64 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-09-06 21:27:57 +10:00
psychedelicious
b0414a81ec tidy(ui): ViewerToggleMenu -> ViewerToggle 2024-09-06 21:27:57 +10:00
psychedelicious
f078e325f8 feat(ui): alt quick switches to color picker 2024-09-06 21:27:57 +10:00
psychedelicious
1a5dc202cb feat(ui): tweak add entity button layout 2024-09-06 21:27:57 +10:00
psychedelicious
79d7f023e8 feat(ui): restore context menu for entity list 2024-09-06 21:27:57 +10:00
psychedelicious
913912d96b feat(ui): add delete button to each layer 2024-09-06 21:27:57 +10:00
psychedelicious
cfa22b345a feat(ui): add + buttons to entity categories 2024-09-06 21:27:57 +10:00
psychedelicious
ce4948ec23 feat(ui): tweak brush fill UI 2024-09-06 21:27:57 +10:00
psychedelicious
86d10ca87b feat(ui): do not select layer on staging accept 2024-09-06 21:27:57 +10:00
psychedelicious
057b5ec646 fix(ui): more fiddly queue count layout stuff 2024-09-06 21:27:57 +10:00
psychedelicious
16eede4288 fix(ui): floating params panel invoke button loading state 2024-09-06 21:27:57 +10:00
psychedelicious
e3c0c638c1 feat(ui): move canvas undo/redo to hook 2024-09-06 21:27:57 +10:00
psychedelicious
e57ee8db35 fix(ui): queue count badge positioning 2024-09-06 21:27:57 +10:00
psychedelicious
cd2a80cf77 fix(ui): add node cmdk only enabled on workflows tab 2024-09-06 21:27:57 +10:00
psychedelicious
300ecd9b16 chore: release v4.2.9.dev7 2024-09-06 21:27:57 +10:00
psychedelicious
a95fbfe6c6 fix(ui): pending node connection stuck 2024-09-06 21:27:57 +10:00
psychedelicious
79168b637e chore(ui): lint 2024-09-06 21:27:57 +10:00
psychedelicious
42c7ddebaa chore: release v4.2.9.dev6 2024-09-06 21:27:56 +10:00
psychedelicious
ac349573f2 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-09-06 21:27:56 +10:00
psychedelicious
28feb013d7 fix(ui): schema parsing now that node_pack is guaranteed to be present 2024-09-06 21:27:56 +10:00
psychedelicious
4129cddbeb chore(ui): typegen 2024-09-06 21:27:56 +10:00
psychedelicious
4463e6da5d fix(app): node_pack not added to openapi schema correctly 2024-09-06 21:27:56 +10:00
psychedelicious
db0b97cca1 fix(ui): unnecessary z-index on invoke button 2024-09-06 21:27:56 +10:00
psychedelicious
be58b03137 feat(ui): split settings modal 2024-09-06 21:27:56 +10:00
psychedelicious
2df0056ef1 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-09-06 21:27:56 +10:00
psychedelicious
eeca521baf 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-09-06 21:27:56 +10:00
psychedelicious
93b9792096 fix(ui): transparency effect not updating 2024-09-06 21:27:56 +10:00
psychedelicious
a8079e5854 feat(ui): tidy canvas toolbar buttons 2024-09-06 21:27:56 +10:00
psychedelicious
61f9ba5b46 feat(ui): revised viewer toggle @joshistoast 2024-09-06 21:27:56 +10:00
psychedelicious
a99ff467f6 fix(ui): opacity reset value incorrect 2024-09-06 21:27:56 +10:00
psychedelicious
5abad87ce3 revert(ui): roll back flip, doesn't work with rotate yet 2024-09-06 21:27:56 +10:00
psychedelicious
931a4ddc88 fix(ui): disable opacity slider fully when no valid entity selected 2024-09-06 21:27:56 +10:00
psychedelicious
784a615864 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-09-06 21:27:56 +10:00
psychedelicious
697c81b74e feat(ui): tweak regional prompt box styles 2024-09-06 21:27:56 +10:00
psychedelicious
d6b8cb9e7c feat(ui): tweak enabled/locked toggle styles 2024-09-06 21:27:56 +10:00
psychedelicious
f4a031a412 feat(ui): tweak filter styling 2024-09-06 21:27:56 +10:00
psychedelicious
1749abbd97 feat(ui): add flip & reset to transform 2024-09-06 21:27:56 +10:00
psychedelicious
1497afcfda tidy(ui): use helper to sync scaled bbox size on model change 2024-09-06 21:27:56 +10:00
psychedelicious
ec9fdace22 fix(ui): randomize seed toggle linked to prompt concat 2024-09-06 21:27:56 +10:00
psychedelicious
c1a039ef91 chore: release v4.2.9.dev5 2024-09-06 21:27:56 +10:00
psychedelicious
69d1edc036 chore(ui): lint 2024-09-06 21:27:56 +10:00
psychedelicious
b69b001755 feat(ui): generalize mask fill, add to action bar 2024-09-06 21:27:56 +10:00
psychedelicious
97414f1886 feat(ui): implement interaction locking on layers 2024-09-06 21:27:56 +10:00
psychedelicious
478daf8614 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-09-06 21:27:56 +10:00
psychedelicious
c74b130303 feat(ui): collapsible entity groups 2024-09-06 21:27:56 +10:00
psychedelicious
11bc318d8d tidy(ui): rename some classes to be consistent 2024-09-06 21:27:56 +10:00
psychedelicious
77125bee7e 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-09-06 21:27:56 +10:00
psychedelicious
4354cd7c38 tidy(ui): move all undoable reducers back to canvas slice 2024-09-06 21:27:56 +10:00
psychedelicious
0e31ccaea0 fix(ui): dnd image count 2024-09-06 21:27:56 +10:00
psychedelicious
e32fa8bd35 fix(ui): canvas entity opacity scale 2024-09-06 21:27:56 +10:00
psychedelicious
d209652caa perf(ui): optimize all selectors 2
Mostly selector optimization. Still a few places to tidy up but I'll get to that later.
2024-09-06 21:27:56 +10:00
psychedelicious
fe672ba5e0 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-09-06 21:27:56 +10:00
psychedelicious
04d41085a3 feat(ui): rough out undo/redo on canvas 2024-09-06 21:27:56 +10:00
psychedelicious
36604f752e chore: release v4.2.9.dev4
Canvas dev build.
2024-09-06 21:27:56 +10:00
psychedelicious
9056f446bb 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-09-06 21:27:56 +10:00
psychedelicious
a33d1b979d feat(ui): split out loras state from canvas rendering state 2024-09-06 21:27:56 +10:00
psychedelicious
4670f82e65 feat(ui): split out session state from canvas rendering state 2024-09-06 21:27:56 +10:00
psychedelicious
a07346b364 feat(ui): split out settings state from canvas rendering state 2024-09-06 21:27:56 +10:00
psychedelicious
2b1e930cdf feat(ui): split out tool state from canvas rendering state 2024-09-06 21:27:56 +10:00
psychedelicious
6b75ea3b01 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-09-06 21:27:56 +10:00
psychedelicious
ff8bc93080 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-09-06 21:27:56 +10:00
psychedelicious
85eff566dd feat(ui): move selected tool and tool buffer out of redux
This ephemeral state can live in the canvas classes.
2024-09-06 21:27:56 +10:00
psychedelicious
9480691de5 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-09-06 21:27:56 +10:00
psychedelicious
463f3dbb35 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-09-06 21:27:56 +10:00
psychedelicious
72f304baa5 feat(ui): move events into modules who care about them 2024-09-06 21:27:56 +10:00
psychedelicious
b4d01659e0 fix(ui): color picker resets brush opacity 2024-09-06 21:27:56 +10:00
psychedelicious
a733d72089 fix(ui): scaled bbox loses sync 2024-09-06 21:27:56 +10:00
psychedelicious
8d0a75cb5f feat(ui): add context menu to entity list 2024-09-06 21:27:56 +10:00
psychedelicious
8a56702341 chore(ui): bump @invoke-ai/ui-library 2024-09-06 21:27:56 +10:00
psychedelicious
af638cf5ce fix(ui): missing vae precision in graph builders 2024-09-06 21:27:56 +10:00
psychedelicious
839248c74c chore: release v4.2.9.dev3
Instead of using dates, just going to increment.
2024-09-06 21:27:56 +10:00
psychedelicious
4b488de10e feat(ui): use new Result utils for enqueueing 2024-09-06 21:27:56 +10:00
psychedelicious
112d6ead91 fix(ui): graph building issue w/ controlnet 2024-09-06 21:27:56 +10:00
psychedelicious
a5e2e78dee 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-09-06 21:27:56 +10:00
psychedelicious
b91f79b0bb chore: release v4.2.9.dev20240824 2024-09-06 21:27:56 +10:00
psychedelicious
bc75b0259b fix(ui): lint & fix issues with adding regional ip adapters 2024-09-06 21:27:56 +10:00
psychedelicious
51663c5439 feat(ui): add knipignore tag
I'm not ready to delete some things but still want to build the app.
2024-09-06 21:27:56 +10:00
psychedelicious
86a5e2538c feat(ui): duplicate entity 2024-09-06 21:27:56 +10:00
psychedelicious
ee94b0ce17 feat(ui): autocomplete on getPrefixeId 2024-09-06 21:27:56 +10:00
psychedelicious
03b3139705 feat(ui): paste canvas gens back on source in generate mode 2024-09-06 21:27:56 +10:00
psychedelicious
44b8480832 chore(ui): typegen 2024-09-06 21:27:56 +10:00
psychedelicious
e6960320dd feat(nodes): CanvasV2MaskAndCropInvocation can paste generated image back on source
This is needed for `Generate` mode.
2024-09-06 21:27:56 +10:00
psychedelicious
775353fe82 fix(ui): extraneous entity preview updates 2024-09-06 21:27:56 +10:00
psychedelicious
e013aff9fe fix(ui): newly-added entities are selected 2024-09-06 21:27:56 +10:00
psychedelicious
a22bf4c296 feat(ui): add crosshair to color picker 2024-09-06 21:27:56 +10:00
psychedelicious
12a00de2ec fix(ui): color picker ignores alpha 2024-09-06 21:27:56 +10:00
psychedelicious
6cbaf7e0ae fix(ui): calculate renderable entities correctly in tool module 2024-09-06 21:27:56 +10:00
psychedelicious
9b8d814082 feat(ui): better color picker 2024-09-06 21:27:56 +10:00
psychedelicious
20a0ed81c5 feat(ui): colored mask preview image 2024-09-06 21:27:56 +10:00
psychedelicious
a040d1d2a6 fix(ui): new rectangles don't trigger rerender 2024-09-06 21:27:56 +10:00
psychedelicious
7c68b889eb chore: bump version v4.2.9.dev20240823 2024-09-06 21:27:56 +10:00
psychedelicious
5ea3c11883 feat(ui): disable most interaction while filtering 2024-09-06 21:27:37 +10:00
psychedelicious
f355d907e6 fix(ui): filter preview offset 2024-09-06 21:27:37 +10:00
psychedelicious
da5b99c840 feat(ui): tweak layout of staging area toolbar 2024-09-06 21:27:37 +10:00
psychedelicious
4f9c4f7b40 chore(ui): typegen 2024-09-06 21:27:37 +10:00
psychedelicious
143c47c887 tidy(app): clean up app changes for canvas v2 2024-09-06 21:27:37 +10:00
psychedelicious
2ca5aeda96 feat(ui): use singleton for clear q confirm dialog 2024-09-06 21:27:37 +10:00
psychedelicious
9b1ee633e6 fix(ui): rip out broken recall logic, NO TS ERRORS 2024-09-06 21:27:37 +10:00
psychedelicious
c06998184e chore(ui): lint 2024-09-06 21:27:37 +10:00
psychedelicious
84aa2b94e5 fix(ui): staging area interaction scopes 2024-09-06 21:27:37 +10:00
psychedelicious
64f7c7bf36 fix(ui): staging area actions 2024-09-06 21:27:37 +10:00
psychedelicious
18d9263d60 tidy(ui): more cleanup 2024-09-06 21:27:37 +10:00
psychedelicious
e0e8165992 fix(ui): upscale tab graph 2024-09-06 21:27:37 +10:00
psychedelicious
e826f8a020 fix(ui): sdxl graph builder 2024-09-06 21:27:37 +10:00
psychedelicious
9486c50e67 fix(ui): select next entity in the list when deleting 2024-09-06 21:27:37 +10:00
psychedelicious
66424c3c93 feat(ui): fix delete layer hotkey 2024-09-06 21:27:37 +10:00
psychedelicious
cff871e8a6 tidy(ui): "eye dropper" -> "color picker" 2024-09-06 21:27:37 +10:00
psychedelicious
51cd435ad8 tidy(ui): regional guidance buttons 2024-09-06 21:27:37 +10:00
psychedelicious
bc8bf989f3 feat(ui): update entity list menu 2024-09-06 21:27:37 +10:00
psychedelicious
87150b7c6b feat(ui): add log debug button 2024-09-06 21:27:37 +10:00
psychedelicious
5797797904 chore(ui): lint 2024-09-06 21:27:37 +10:00
psychedelicious
ab7b9c4523 chore(ui): prettier 2024-09-06 21:27:37 +10:00
psychedelicious
68bf4459c3 chore(ui): eslint 2024-09-06 21:27:37 +10:00
psychedelicious
05052fd21c tidy(ui): remove unused stuff 4 2024-09-06 21:27:37 +10:00
psychedelicious
f4e3f87c2e tidy(ui): remove unused stuff 3 2024-09-06 21:27:37 +10:00
psychedelicious
f47e1afd57 tidy(ui): remove unused pkg @chakra-ui/react-use-size 2024-09-06 21:27:37 +10:00
psychedelicious
aa4be01bdf feat(ui): revise graph building for control layers, fix issues w/ invocation complete events 2024-09-06 21:27:37 +10:00
psychedelicious
f8d4d5e546 feat(ui): use unique id for metadata in Graph class 2024-09-06 21:27:37 +10:00
psychedelicious
f628b8ad9d tidy(ui): remove unused stuff 2 2024-09-06 21:27:37 +10:00
psychedelicious
bb4eb70a4a tidy(ui): remove unused stuff 2024-09-06 21:27:37 +10:00
psychedelicious
7b1a533bf2 tidy(ui): reduce use of parseify util 2024-09-06 21:27:37 +10:00
psychedelicious
d32526f04a feat(ui): refine canvas entity list items & menus 2024-09-06 21:27:37 +10:00
psychedelicious
76bc2cf5db feat(ui): canvas layer preview, revised reactivity for adapters 2024-09-06 21:27:37 +10:00
psychedelicious
90b3ebb27d feat(ui): add SyncableMap
Can be used with useSyncExternal store to make a `Map` reactive.
2024-09-06 21:27:37 +10:00
psychedelicious
4b65d9145e tidy(ui): removed unused transform methods from canvasmanager 2024-09-06 21:27:37 +10:00
psychedelicious
9badbb8dff feat(ui): transform tool ux 2024-09-06 21:27:37 +10:00
psychedelicious
70bcca23a4 feat(ui): rough out canvas mode 2024-09-06 21:27:37 +10:00
psychedelicious
5ba15d0db5 feat(ui): add canvas autosave checkbox 2024-09-06 21:27:37 +10:00
psychedelicious
fef70fdac0 fix(ui): memory leak when getting image DTO
must unsubscribe!
2024-09-06 21:27:37 +10:00
psychedelicious
62eb00aacf feat(ui): rework settings menu 2024-09-06 21:27:37 +10:00
psychedelicious
3f41ea574b feat(ui): no entities fallback buttons 2024-09-06 21:27:37 +10:00
psychedelicious
e865c1a2b5 perf(ui): optimize gallery image delete button rendering 2024-09-06 21:27:37 +10:00
psychedelicious
f4a798c64f feat(ui): remove "solid" background option 2024-09-06 21:27:37 +10:00
psychedelicious
7806604666 tidy(ui): organise files and classes 2024-09-06 21:27:37 +10:00
psychedelicious
917f8087b4 tidy(ui): abstract compositing logic to module 2024-09-06 21:27:37 +10:00
psychedelicious
b7a9a6153a fix(ui): fix canvas cache property access 2024-09-06 21:27:37 +10:00
psychedelicious
fdc5f89060 tidy(ui): clean up CanvasFilter class 2024-09-06 21:27:37 +10:00
psychedelicious
7fc0959c83 tidy(ui): clean up a few bits and bobs 2024-09-06 21:27:37 +10:00
psychedelicious
95ff64d1d6 tidy(ui): abstract canvas rendering logic to module 2024-09-06 21:27:37 +10:00
psychedelicious
67f94007d0 tidy(ui): abstract caching logic to module 2024-09-06 21:27:37 +10:00
psychedelicious
930f27d009 tidy(ui): abstract worker logic to module 2024-09-06 21:27:37 +10:00
psychedelicious
6125e0fd13 tidy(ui): abstract stage logic into module 2024-09-06 21:27:37 +10:00
psychedelicious
c8e8cf9dfb feat(ui): add entity group hiding 2024-09-06 21:27:37 +10:00
psychedelicious
ac8122a154 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-09-06 21:27:37 +10:00
psychedelicious
aa7c909f14 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-09-06 21:27:37 +10:00
psychedelicious
40d294b29c feat(ui): revise filter implementation 2024-09-06 21:27:36 +10:00
psychedelicious
ff1fd5b5b5 fix(ui): add button to delete inpaint mask 2024-09-06 21:27:36 +10:00
psychedelicious
6aa87d7df2 feat(ui): add contexts/hooks to access entity adapters directly 2024-09-06 21:27:36 +10:00
psychedelicious
7732eeb815 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-09-06 21:27:36 +10:00
psychedelicious
9eb37feb6c feat(ui) do not set $canvasManager until ready 2024-09-06 21:27:36 +10:00
psychedelicious
e3b38339a0 fix(ui): inpaint mask naming 2024-09-06 21:27:36 +10:00
psychedelicious
f92695e677 feat(ui): efficient canvas compositing
Also solves issue of exporting layers at different opacities than what is visible
2024-09-06 21:27:36 +10:00
psychedelicious
9473d54fc7 feat(ui): allow multiple inpaint masks
This is easier than making it a nullable singleton
2024-09-06 21:27:36 +10:00
psychedelicious
33783eae4f fix(ui): missing rasterization cache invalidations 2024-09-06 21:27:36 +10:00
psychedelicious
f6c3570c26 feat(ui): iterate on filter UI, flow 2024-09-06 21:27:36 +10:00
psychedelicious
96a76a763e fix(ui): rehydration data loss 2024-09-06 21:27:36 +10:00
psychedelicious
209bc925c0 feat(ui): sort log namespaces 2024-09-06 21:27:36 +10:00
psychedelicious
a77fb427d0 fix(ui): do not merge arrays by index during rehydration 2024-09-06 21:27:36 +10:00
psychedelicious
6fc358b9ea 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-09-06 21:27:36 +10:00
psychedelicious
742ac3088d fix(ui): fix logger filter
was accidetnally replacing the filter instead of appending to it.
2024-09-06 21:27:36 +10:00
psychedelicious
8b958e5280 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-09-06 21:27:36 +10:00
psychedelicious
71c727e586 fix(ui): handle opacity for masks 2024-09-06 21:27:36 +10:00
psychedelicious
c590e8bd6f feat(ui): default background to checkerboard 2024-09-06 21:27:36 +10:00
psychedelicious
71a13c3e91 feat(ui): clean up logging namespaces, allow skipping namespaces 2024-09-06 21:27:36 +10:00
psychedelicious
d7e7f049e7 chore(ui): bump ui library 2024-09-06 21:27:36 +10:00
psychedelicious
b23332e167 fix(ui): do not allow drawing if layer disabled 2024-09-06 21:27:36 +10:00
psychedelicious
21c637c365 fix(ui): stale state causing race conditions & extraneous renders 2024-09-06 21:27:36 +10:00
psychedelicious
2b7aecb346 fix(ui): do not clear buffer when rendering "real" objects 2024-09-06 21:27:36 +10:00
psychedelicious
5c669f7925 tidy(ui): remove "filter" from CanvasImageState 2024-09-06 21:27:36 +10:00
psychedelicious
748f445de5 feat(ui): better editable title 2024-09-06 21:27:36 +10:00
psychedelicious
2127ee9a09 fix(ui): stroke eraserline 2024-09-06 21:27:36 +10:00
psychedelicious
7ccb1c5938 feat(ui): restore transparency effect for control layers 2024-09-06 21:27:36 +10:00
psychedelicious
26d1a2ebfe feat(ui): use text cursor for entity title 2024-09-06 21:27:36 +10:00
psychedelicious
e0bd683d88 tidy(ui): remove extraneous logging in CanvasStateApi 2024-09-06 21:27:36 +10:00
psychedelicious
3c5332b236 feat(ui): better buffer commit logic 2024-09-06 21:27:36 +10:00
psychedelicious
d4e847bd9a feat(ui): render buffer separately from "real" objects 2024-09-06 21:27:36 +10:00
psychedelicious
cee16536cd fix(ui): pixelRect should always be integer 2024-09-06 21:27:36 +10:00
psychedelicious
811e75dc67 fix(ui): only update stage attrs when stage itself is dragged 2024-09-06 21:27:36 +10:00
psychedelicious
a4a77d70f7 feat(ui): add line simplification
This fixes some awkward issues where line segments stack up.
2024-09-06 21:27:36 +10:00
psychedelicious
13e2de3cb4 fix(ui): various things listening when they need not listen 2024-09-06 21:27:36 +10:00
psychedelicious
7925bbf454 feat(ui): layer opacity via caching 2024-09-06 21:27:36 +10:00
psychedelicious
39eb0a01d2 feat(ui): reset view fits all visible objects 2024-09-06 21:27:36 +10:00
psychedelicious
305e50004a fix(ui): rerenders when changing canvas scale 2024-09-06 21:27:36 +10:00
psychedelicious
9974784596 fix(ui): do not render rasterized layer unless renderObjects=true 2024-09-06 21:27:36 +10:00
psychedelicious
105c5a7fd4 feat(ui): revise app layout strategy, add interaction scopes for hotkeys 2024-09-06 21:27:36 +10:00
psychedelicious
7baad9c72e feat(ui): tweak mask patterns 2024-09-06 21:27:36 +10:00
psychedelicious
a5e8705ea3 fix(ui): dynamic prompts recalcs when presets are loaded 2024-09-06 21:27:36 +10:00
psychedelicious
7b2a5c3a30 fix(ui): use style preset prompts correctly 2024-09-06 21:27:36 +10:00
psychedelicious
3dcb33026a fix(ui): discard selected staging image not all other images 2024-09-06 21:27:36 +10:00
psychedelicious
e956ea5482 fix(ui): respect image size in staging preview 2024-09-06 21:27:36 +10:00
psychedelicious
64f50ab278 tidy(ui): cleanup after events change 2024-09-06 21:27:36 +10:00
psychedelicious
b152937f30 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-09-06 21:27:36 +10:00
psychedelicious
4e6a9d990c fix(ui): rebase conflicts 2024-09-06 21:27:36 +10:00
psychedelicious
d6fb220b2c fix(ui): update compositing rect when fill changes 2024-09-06 21:27:36 +10:00
psychedelicious
13fc539f8b feat(ui): add canvas background style 2024-09-06 21:27:36 +10:00
psychedelicious
d2b5d6342c feat(ui): mask layers choose own opacity 2024-09-06 21:27:36 +10:00
psychedelicious
eb644d4e6a feat(ui): mask fill patterns 2024-09-06 21:27:36 +10:00
psychedelicious
d8a2efc691 build(ui): add vite types to tsconfig 2024-09-06 21:27:36 +10:00
psychedelicious
163687aef3 fix(ui): do not smooth pixel data when using eyeDropper 2024-09-06 21:27:36 +10:00
psychedelicious
b41f1a897d tidy(ui): tool components & translations 2024-09-06 21:27:36 +10:00
psychedelicious
006a5723da 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-09-06 21:27:36 +10:00
psychedelicious
daede9c9cf fix(ui): ip adapters work 2024-09-06 21:27:36 +10:00
psychedelicious
af7d14cd59 feat(ui): rename layers 2024-09-06 21:27:36 +10:00
psychedelicious
ba14fe3600 feat(ui): revise entity menus 2024-09-06 21:27:36 +10:00
psychedelicious
af726d1f15 feat(ui): split control layers from raster layers for UI and internal state, same rendering as raster layers 2024-09-06 21:27:36 +10:00
psychedelicious
3a2efb351d feat(ui): implement cache for image rasterization, rip out some old controladapters code 2024-09-06 21:27:36 +10:00
psychedelicious
c9dc61c311 feat(ui, app): use layer as control (wip) 2024-09-06 21:27:36 +10:00
psychedelicious
500f151d96 feat(ui): add contextmenu for canvas entities 2024-09-06 21:27:36 +10:00
psychedelicious
9708fc5d6c feat(ui): more better logging & naming 2024-09-06 21:27:36 +10:00
psychedelicious
c697501285 feat(ui): better logging w/ path 2024-09-06 21:27:36 +10:00
psychedelicious
e213cfc2ba feat(ui): always show marks on canvas scale slider 2024-09-06 21:27:36 +10:00
psychedelicious
270c1304a8 fix(ui): do not import button from chakra 2024-09-06 21:27:36 +10:00
psychedelicious
b887cf4612 fix(ui): scaled bbox preview 2024-09-06 21:27:36 +10:00
psychedelicious
e0573b721e feat(ui): tidy up atoms 2024-09-06 21:27:36 +10:00
psychedelicious
22712c5dac feat(ui): convert all my pubsubs to atoms
its the same but better
2024-09-06 21:27:36 +10:00
psychedelicious
7339b3d8cc feat(ui): add trnalsation 2024-09-06 21:27:36 +10:00
psychedelicious
0eda34b41f fix(ui): give up on thumbnail loading, causes flash during transformer 2024-09-06 21:27:36 +10:00
psychedelicious
9d9e845198 fix(ui): depth anything v2 2024-09-06 21:27:36 +10:00
psychedelicious
6b7ead4461 tidy(ui): remove unused code, comments 2024-09-06 21:27:36 +10:00
psychedelicious
3ce3056c4a fix(ui): staging area works 2024-09-06 21:27:36 +10:00
psychedelicious
aa73cbf459 feat(nodes): temp disable canvas output crop 2024-09-06 21:27:36 +10:00
psychedelicious
fa3f109eb9 fix(ui): max scale 1 when reset view 2024-09-06 21:27:36 +10:00
psychedelicious
c22afa5725 feat(ui): better scale changer component, reset view functionality 2024-09-06 21:27:36 +10:00
psychedelicious
318086571d fix(ui): img2img 2024-09-06 21:27:36 +10:00
psychedelicious
260ef8edd5 feat(ui): add manual scale controls 2024-09-06 21:27:36 +10:00
psychedelicious
a3a933a797 fix(ui): do not await clearBuffer 2024-09-06 21:27:36 +10:00
psychedelicious
d2d298604c feat(ui): dnd image into layer 2024-09-06 21:27:36 +10:00
psychedelicious
318f2ee003 fix(ui): do not await commitBuffer 2024-09-06 21:27:36 +10:00
psychedelicious
657d59268c fix(ui): properly destroy entities in manager cleanup 2024-09-06 21:27:36 +10:00
psychedelicious
54b7931779 tidy(ui): clearer component names for regional guidance 2024-09-06 21:27:36 +10:00
psychedelicious
aadba55796 tidy(ui): clearer component names for ip adapter 2024-09-06 21:27:36 +10:00
psychedelicious
1a8d65d7a9 tidy(ui): clearer component names for inpaint mask 2024-09-06 21:27:36 +10:00
psychedelicious
36f7d0957a tidy(ui): clearer component names for control adapters 2024-09-06 21:27:36 +10:00
psychedelicious
85a33ff6aa feat(ui): simplify canvas list item headers 2024-09-06 21:27:36 +10:00
psychedelicious
ebef4feddb fix(ui): ip adapter list item 2024-09-06 21:27:36 +10:00
psychedelicious
0a34bebd9c tidy(ui): clean up unused logic 2024-09-06 21:27:36 +10:00
psychedelicious
d6acd96dec feat(ui): clean up state, add mutex for image loading, add thumbnail loading 2024-09-06 21:27:36 +10:00
psychedelicious
3ac88acf11 chore(ui): add async-mutex dep 2024-09-06 21:27:36 +10:00
psychedelicious
2a7cffed2a feat(ui): txt2img, img2img, inpaint & outpaint working 2024-09-06 21:27:36 +10:00
psychedelicious
08c2089b3d feat(ui): no padding on transformer outlines 2024-09-06 21:27:36 +10:00
psychedelicious
87521b07ce feat(ui): restore object count to layer titles 2024-09-06 21:27:36 +10:00
psychedelicious
5de7efc1dc tidy(ui): "useIsEntitySelected" -> "useEntityIsSelected" 2024-09-06 21:27:36 +10:00
psychedelicious
d4fb80772e tidy(ui): move transformer statics into class 2024-09-06 21:27:36 +10:00
psychedelicious
97886bf62e 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-09-06 21:27:36 +10:00
psychedelicious
83657e0a68 perf(ui): do not add duplicate points to lines 2024-09-06 21:27:36 +10:00
psychedelicious
adf293f6c9 feat(ui): up line tension to 0.3 2024-09-06 21:27:36 +10:00
psychedelicious
d35120feb9 perf(ui): disable stroke, perfect draw on compositing rect 2024-09-06 21:27:36 +10:00
psychedelicious
00020f49fe tidy(ui): remove unused code, initial image 2024-09-06 21:27:36 +10:00
psychedelicious
206d1b231a tidy(ui): remove unused state & actions 2024-09-06 21:27:36 +10:00
psychedelicious
008d8f491c feat(ui): region mask rendering 2024-09-06 21:27:36 +10:00
psychedelicious
ae5543b6fa feat(ui): esc cancels drawing buffer
maybe this is not wanted? we'll see
2024-09-06 21:27:36 +10:00
psychedelicious
2c2e6c5c25 fix(ui): render transformer over objects, fix issue w/ inpaint rect color 2024-09-06 21:27:36 +10:00
psychedelicious
718bed5758 fix(ui): brush preview fill for inpaint/region 2024-09-06 21:27:36 +10:00
psychedelicious
d2c524e7fd fix(ui): no objects rendered until vis toggled 2024-09-06 21:27:36 +10:00
psychedelicious
c172221038 feat(ui): inpaint mask transform 2024-09-06 21:27:36 +10:00
psychedelicious
b006edf9c4 fix(ui): layer accidental early set isFirstRender=false 2024-09-06 21:27:36 +10:00
psychedelicious
9f2dc4d6d1 fix(ui): inpaint mask rendering 2024-09-06 21:27:36 +10:00
psychedelicious
8816d6c0c5 feat(ui): wip inpaint mask uses new API 2024-09-06 21:27:36 +10:00
psychedelicious
6dddad01fc feat(ui): move updatePosition to transformer 2024-09-06 21:27:36 +10:00
psychedelicious
7b7f0a7380 feat(ui): move resetScale to transformer 2024-09-06 21:27:36 +10:00
psychedelicious
8f780e79e7 tidy(ui): more imperative naming 2024-09-06 21:27:36 +10:00
psychedelicious
5a2b00fb95 tidy(ui): use imperative names for setters in stateapi 2024-09-06 21:27:36 +10:00
psychedelicious
25158d4d92 fix(ui): commit drawing buffer on tool change, fixing bbox not calculating 2024-09-06 21:27:36 +10:00
psychedelicious
3c9e9fa746 fix(ui): sync transformer when requesting bbox calc 2024-09-06 21:27:36 +10:00
psychedelicious
e5a7e932cf tidy(ui): rename union CanvasEntity -> CanvasEntityState 2024-09-06 21:27:36 +10:00
psychedelicious
84d15b0c7f fix(ui): request rect calc immediately on transform, hiding rect 2024-09-06 21:27:36 +10:00
psychedelicious
79f216a491 feat(ui): move bbox calculation to transformer 2024-09-06 21:27:36 +10:00
psychedelicious
b86645c1a7 feat(ui): use set for transformer subscriptions 2024-09-06 21:27:36 +10:00
psychedelicious
3f359f1059 tidy(ui): clean up worker tasks when complete 2024-09-06 21:27:36 +10:00
psychedelicious
7c7117c39a tidy(ui): remove unused code in CanvasTool 2024-09-06 21:27:36 +10:00
psychedelicious
92b8c68b94 feat(ui): use pubsub for isTransforming on manager 2024-09-06 21:27:36 +10:00
psychedelicious
7b7d722bae docs(ui): update transformer docstrings 2024-09-06 21:27:36 +10:00
psychedelicious
227c3197b4 feat(ui): revised event pubsub, transformer logic split out 2024-09-06 21:27:35 +10:00
psychedelicious
cab1ba8970 feat(ui): add simple pubsub 2024-09-06 21:27:35 +10:00
psychedelicious
7395c94d00 feat(ui): document & clean up object renderer 2024-09-06 21:27:35 +10:00
psychedelicious
2a2dadb952 feat(ui): split out object renderer 2024-09-06 21:27:35 +10:00
psychedelicious
b60dcf8036 fix(ui): unable to hold shit while transforming to retain ratio 2024-09-06 21:27:35 +10:00
psychedelicious
edeb706d19 tidy(ui): rename canvas stuff 2024-09-06 21:27:35 +10:00
psychedelicious
cd76a2d217 tidy(ui): consolidate getLoggingContext builders 2024-09-06 21:27:35 +10:00
psychedelicious
2aaa6c3986 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-09-06 21:27:35 +10:00
psychedelicious
dfdd56dbec feat(ui): disable image smoothing on layers 2024-09-06 21:27:35 +10:00
psychedelicious
f521704ade fix(ui): round position when rasterizing layer 2024-09-06 21:27:35 +10:00
psychedelicious
0e0cf9cd3e feat(ui): continue modularizing transform 2024-09-06 21:27:35 +10:00
psychedelicious
60484bd45e feat(ui): fix a few things that didn't unsubscribe correctly, add helper to manage subscriptions 2024-09-06 21:27:35 +10:00
psychedelicious
5978ba32c0 feat(ui): merge bbox outline into transformer 2024-09-06 21:27:35 +10:00
psychedelicious
45efa8f40d fix(ui): update parent's pos not transformers 2024-09-06 21:27:35 +10:00
psychedelicious
3392e1f0bf feat(ui): merge interaction rect into transformer class 2024-09-06 21:27:35 +10:00
psychedelicious
c734385f18 feat(ui): prepare staging area 2024-09-06 21:27:35 +10:00
psychedelicious
148434d833 feat(ui): typing for logging context 2024-09-06 21:27:35 +10:00
psychedelicious
d5e4a965cc feat(ui): remove inheritance of CanvasObject
JS is terrible
2024-09-06 21:27:35 +10:00
psychedelicious
c8ac4d9e1b feat(ui): split & document transformer logic, iterate on class structures 2024-09-06 21:27:35 +10:00
psychedelicious
1e17461601 feat(ui): rotation snap to nearest 45deg when holding shift 2024-09-06 21:27:35 +10:00
psychedelicious
4432244bc3 feat(ui): expose subscribe method for nanostores 2024-09-06 21:27:35 +10:00
psychedelicious
0d087f84a5 tidy(ui): remove layer scaling reducers 2024-09-06 21:27:35 +10:00
psychedelicious
286deb277b fix(ui): pixel-perfect transforms 2024-09-06 21:27:35 +10:00
psychedelicious
3f2c1139ea fix(ui): layer visibility toggle 2024-09-06 21:27:35 +10:00
psychedelicious
1f3163942a fix(nodes): fix canvas mask erode
it wasn't eroding enough and caused incorrect transparency in result images
2024-09-06 21:27:35 +10:00
psychedelicious
28affcc60a fix(ui): do not reset layer on first render 2024-09-06 21:27:35 +10:00
psychedelicious
f59c2cf7f5 feat(ui): revised logging and naming setup, fix staging area 2024-09-06 21:27:35 +10:00
psychedelicious
86ff52ec36 feat(ui): add repr methods to layer and object classes 2024-09-06 21:27:35 +10:00
psychedelicious
17e7364bdb feat(ui): use nanoid(10) instead of uuidv4 for canvas
Shorter ids makes it much more readable
2024-09-06 21:27:35 +10:00
psychedelicious
71e4b60a30 build(ui): add nanoid as explicit dep 2024-09-06 21:27:35 +10:00
psychedelicious
a3e1ff637d fix(ui): move CanvasImage's konva image to correct object 2024-09-06 21:27:35 +10:00
psychedelicious
a3d2ba1444 fix(ui): prevent flash when applying transform 2024-09-06 21:27:35 +10:00
psychedelicious
9baa594a56 build(ui): add eslint rules for async stuff 2024-09-06 21:27:35 +10:00
psychedelicious
49de11c3ae feat(ui): trying to fix flicker after transform 2024-09-06 21:27:35 +10:00
psychedelicious
690fbdc73d feat(ui): transform cleanup 2024-09-06 21:27:35 +10:00
psychedelicious
4d52824895 feat(ui): fix transform when rotated 2024-09-06 21:27:35 +10:00
psychedelicious
2e13e75fc6 fix(ui): use pixel bbox when image is in layer 2024-09-06 21:27:35 +10:00
psychedelicious
f6f6462590 fix(ui): transforming when axes flipped 2024-09-06 21:27:35 +10:00
psychedelicious
79fee16629 feat(ui): hallelujah (???) 2024-09-06 21:27:35 +10:00
psychedelicious
073f63251a feat(ui): add debug button 2024-09-06 21:27:35 +10:00
psychedelicious
20125dc04b fix(ui): transformer padding 2024-09-06 21:27:35 +10:00
psychedelicious
9f1f8d62f0 feat(ui): wip transform mode 2 2024-09-06 21:27:35 +10:00
psychedelicious
99d432785c feat(ui): wip transform mode 2024-09-06 21:27:35 +10:00
psychedelicious
54e5401a96 feat(ui): wip transform mode 2024-09-06 21:27:35 +10:00
psychedelicious
6b5d7406d6 fix(ui): dnd to canvas broke 2024-09-06 21:27:35 +10:00
psychedelicious
c6bfeba61a fix(ui): conflicts after rebasing 2024-09-06 21:27:35 +10:00
psychedelicious
af1c8cc7e0 fix(ui): imageDropped listener 2024-09-06 21:27:35 +10:00
psychedelicious
879161ed4c wip 2024-09-06 21:27:35 +10:00
psychedelicious
a15944774c fix(ui): transform tool seems to be working 2024-09-06 21:27:35 +10:00
psychedelicious
42612a4f92 fix(ui): move tool fixes, add transform tool 2024-09-06 21:27:35 +10:00
psychedelicious
5c39935e88 feat(ui): move tool now only moves 2024-09-06 21:27:35 +10:00
psychedelicious
35c941c540 feat(ui): layer bbox calc in worker 2024-09-06 21:27:35 +10:00
psychedelicious
92931b0d4d feat(ui): tweaked entity & group selection styles 2024-09-06 21:27:35 +10:00
psychedelicious
7c3d2f5578 feat(ui): canvas entity list headers 2024-09-06 21:27:35 +10:00
psychedelicious
9c809ba147 tidy(ui): CanvasRegion 2024-09-06 21:27:35 +10:00
psychedelicious
25fb1bb837 tidy(ui): CanvasRect 2024-09-06 21:27:35 +10:00
psychedelicious
2d01086a3e tidy(ui): CanvasLayer 2024-09-06 21:27:35 +10:00
psychedelicious
f8af1e9014 tidy(ui): CanvasInpaintMask 2024-09-06 21:27:35 +10:00
psychedelicious
2b34a5c646 tidy(ui): CanvasInitialImage 2024-09-06 21:27:35 +10:00
psychedelicious
22ca3db870 tidy(ui): CanvasImage 2024-09-06 21:27:35 +10:00
psychedelicious
247ca97fbd tidy(ui): CanvasEraserLine 2024-09-06 21:27:35 +10:00
psychedelicious
b346b25a7b tidy(ui): CanvasControlAdapter 2024-09-06 21:27:35 +10:00
psychedelicious
496cf3da4f tidy(ui): CanvasBrushLine 2024-09-06 21:27:35 +10:00
psychedelicious
f1b0130389 tidy(ui): CanvasBbox 2024-09-06 21:27:35 +10:00
psychedelicious
39db3be151 tidy(ui): CanvasBackground 2024-09-06 21:27:35 +10:00
psychedelicious
517ad7e77c tidy(ui): update canvas classes, organise location of konva nodes 2024-09-06 21:27:35 +10:00
psychedelicious
5fa10a3f8e feat(ui): add names to all konva objects
Makes troubleshooting much simpler
2024-09-06 21:27:35 +10:00
psychedelicious
aa3986e9f2 fix(ui): do not await creating new canvas image
If you await this, it causes a race condition where multiple images are created.
2024-09-06 21:27:35 +10:00
psychedelicious
9105c02681 feat(ui): use position and dimensions instead of separate x,y,width,height attrs 2024-09-06 21:27:35 +10:00
psychedelicious
6632727d00 fix(ui): remove weird rtkq hook wrapper
I do not understand why I did that initially but it doesn't work with TS.
2024-09-06 21:27:35 +10:00
psychedelicious
34ccd5aa86 feat(ui): rename types size and position to dimensions and coordinate 2024-09-06 21:27:35 +10:00
psychedelicious
23979bdbee tidy(ui): hide layer settings by default 2024-09-06 21:27:35 +10:00
psychedelicious
dda53292bf fix(ui): layer rendering when starting as disabled 2024-09-06 21:27:35 +10:00
psychedelicious
c98c5f13f7 feat(invocation): reduce canvas v2 mask & crop mask dilation 2024-09-06 21:27:35 +10:00
psychedelicious
2a69967863 feat(ui): de-jank staging area and progress images 2024-09-06 21:27:35 +10:00
psychedelicious
781ef806de feat(ui): update staging handling to work w/ cropped mask 2024-09-06 21:27:35 +10:00
psychedelicious
8794c51e42 chore(ui): typegen 2024-09-06 21:27:35 +10:00
psychedelicious
9e89ddf2f1 feat(app): update CanvasV2MaskAndCropInvocation 2024-09-06 21:27:35 +10:00
psychedelicious
f480a89e7a feat(ui): use new canvas output node 2024-09-06 21:27:35 +10:00
psychedelicious
df44fb9827 chore(ui): typegen 2024-09-06 21:27:35 +10:00
psychedelicious
fca9cacc4e feat(app): add CanvasV2MaskAndCropInvocation & CanvasV2MaskAndCropOutput
This handles some masking and cropping that the canvas needs.
2024-09-06 21:27:35 +10:00
psychedelicious
fa7783972b fix(ui): restore nodes output tracking 2024-09-06 21:27:35 +10:00
psychedelicious
e50b4280f2 feat(ui): rip out document size
barely knew ye
2024-09-06 21:27:35 +10:00
psychedelicious
17c9ccfdb0 feat(ui): convert initial image to layer when starting canvas session 2024-09-06 21:27:35 +10:00
psychedelicious
d35af4c048 fix(ui): fix layer transparency calculation 2024-09-06 21:27:35 +10:00
psychedelicious
5fef9bbceb fix(ui): reset initial image when resetting canvas 2024-09-06 21:27:35 +10:00
psychedelicious
507acaa7c9 fix(ui): reset node executions states when loading workflow 2024-09-06 21:27:35 +10:00
psychedelicious
28eb9b62a8 fix(ui): entity display list 2024-09-06 21:27:35 +10:00
psychedelicious
061eeb809f feat(ui): img2img working 2024-09-06 21:27:35 +10:00
psychedelicious
0db5c6ac8e feat(ui): rough out img2img on canvas 2024-09-06 21:27:35 +10:00
psychedelicious
2fd6fd4624 UNDO ME WIP 2024-09-06 21:27:35 +10:00
psychedelicious
db67ae2de4 feat(ui): log invocation source id on socket event 2024-09-06 21:27:35 +10:00
psychedelicious
fedcebbe4d feat(ui): restore document size overlay renderer 2024-09-06 21:27:35 +10:00
psychedelicious
eaaeb356d7 feat(ui): make documnet size a rect 2024-09-06 21:27:35 +10:00
psychedelicious
0cddbc2420 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-09-06 21:27:35 +10:00
psychedelicious
1b44520ff7 feat(ui): initialState is for generation mode 2024-09-06 21:27:35 +10:00
psychedelicious
ee4dc86c15 feat(ui): split out canvas entity list component 2024-09-06 21:27:35 +10:00
psychedelicious
cbc61daa56 feat(ui): hide bbox button when no canvas session active 2024-09-06 21:27:35 +10:00
psychedelicious
f1cd6a06ec 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-09-06 21:27:35 +10:00
psychedelicious
70248b9684 feat(ui): split up tool chooser buttons
Prep for distinct toolbars for generation vs canvas modes
2024-09-06 21:27:35 +10:00
psychedelicious
0a5cf8ae42 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-09-06 21:27:35 +10:00
psychedelicious
4dc5b18671 feat(ui): "stagingArea" -> "session" 2024-09-06 21:27:35 +10:00
psychedelicious
daffb950c3 feat(ui): add reset button to canvas 2024-09-06 21:27:35 +10:00
psychedelicious
1a6ebb9606 feat(ui): add snapToRect util 2024-09-06 21:27:35 +10:00
psychedelicious
e397efb622 fix(ui): fiddle with control adapter filters
some jank still
2024-09-06 21:27:35 +10:00
psychedelicious
748cb0bb8d feat(ui): temp disable doc size overlay 2024-09-06 21:27:35 +10:00
psychedelicious
233a449e53 feat(ui): no animation on layer selection
Felt sluggish
2024-09-06 21:27:35 +10:00
psychedelicious
09ad8b6238 feat(ui): use canvas as source for control images (wip) 2024-09-06 21:27:35 +10:00
psychedelicious
778f703d3d fix(ui): control adapter translate & scale 2024-09-06 21:27:35 +10:00
psychedelicious
8234e4e168 tidy(ui): removed unused state related to non-buffered drawing 2024-09-06 21:27:35 +10:00
psychedelicious
08e588a6c4 feat(ui): control adapter image rendering 2024-09-06 21:27:35 +10:00
psychedelicious
43f550e48b fix(ui): do not floor bbox calc, it cuts off the last pixels 2024-09-06 21:27:35 +10:00
psychedelicious
5663029ba6 feat(ui): fix issue where creating line needs 2 points 2024-09-06 21:27:35 +10:00
psychedelicious
0869e23dc1 fix(ui): edge cases when holding shift and drawing lines 2024-09-06 21:27:35 +10:00
psychedelicious
fc4779e095 fix(ui): set buffered rect color to full alpha 2024-09-06 21:27:35 +10:00
psychedelicious
1667603d1c fix(ui): handle mouseup correctly 2024-09-06 21:27:35 +10:00
psychedelicious
81b210cf14 feat(ui): buffered rect drawing 2024-09-06 21:27:35 +10:00
psychedelicious
ca5fde8ef5 fix(ui): buffered drawing edge cases 2024-09-06 21:27:35 +10:00
psychedelicious
fef88734d0 perf(ui): do not use stage.find 2024-09-06 21:27:35 +10:00
psychedelicious
aa1727d16f perf(ui): object groups do not listen 2024-09-06 21:27:35 +10:00
psychedelicious
7c1afb6493 perf(ui): buffered drawing (wip) 2024-09-06 21:27:35 +10:00
psychedelicious
64e7757872 tidy(ui): organise files 2024-09-06 21:27:35 +10:00
psychedelicious
3b08250331 tidy(ui): organise files 2024-09-06 21:27:35 +10:00
psychedelicious
cd02638db6 tidy(ui): organise files 2024-09-06 21:27:35 +10:00
psychedelicious
5109811182 fix(ui): background rendering 2024-09-06 21:27:35 +10:00
psychedelicious
33ba8cabd1 pkg(ui): remove unused deps react-konva & use-image 2024-09-06 21:27:35 +10:00
psychedelicious
dcac6028a2 feat(ui): organize konva state and files 2024-09-06 21:27:35 +10:00
psychedelicious
25ab435129 fix(ui): merge conflicts in image deletion listener 2024-09-06 21:27:35 +10:00
psychedelicious
cea5dc6216 fix(ui): region rendering 2024-09-06 21:27:35 +10:00
psychedelicious
bc525d29e1 fix(ui): inpaint mask rendering 2024-09-06 21:27:35 +10:00
psychedelicious
cdfe0ca150 fix(ui): staging area rendering 2024-09-06 21:27:35 +10:00
psychedelicious
bd679e018d fix(ui): stale selected entity 2024-09-06 21:27:35 +10:00
psychedelicious
a6ee18448a fix(ui): staging area image offset 2024-09-06 21:27:35 +10:00
psychedelicious
7d96b3e89e feat(ui): tweak layer ui component 2024-09-06 21:27:35 +10:00
psychedelicious
e6723f194a fix(ui): resetting layer resets position 2024-09-06 21:27:35 +10:00
psychedelicious
e55541ea87 feat(ui): updated layer list component styling 2024-09-06 21:27:35 +10:00
psychedelicious
2676ff8ee3 feat(ui): transformable layers 2024-09-06 21:27:34 +10:00
psychedelicious
f024fe4488 feat(ui): move tool icon is pointer like in other apps 2024-09-06 21:27:34 +10:00
psychedelicious
0d3dfb8d0f feat(ui): do not floor cursor position 2024-09-06 21:27:34 +10:00
psychedelicious
65e1951f5d feat(ui): disable gallery hotkeys while staging 2024-09-06 21:27:34 +10:00
psychedelicious
fd6eb91f79 feat(ui): revised canvas progress & staging image handling 2024-09-06 21:27:34 +10:00
psychedelicious
2aea0f2ac5 feat(ui): show queue item origin in queue list 2024-09-06 21:27:34 +10:00
psychedelicious
e1b5aa7011 chore(ui): typegen 2024-09-06 21:27:34 +10:00
psychedelicious
cdc4d29745 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-09-06 21:27:34 +10:00
psychedelicious
5d00792e1f fix(ui): denoise start on outpainting 2024-09-06 21:27:34 +10:00
psychedelicious
1c334f3231 feat(ui): add redux events for queue cleared & batch enqueued socket events 2024-09-06 21:27:34 +10:00
psychedelicious
9ba3182d19 feat(ui): canvas staging area works 2024-09-06 21:27:34 +10:00
psychedelicious
285ad448d7 feat(ui): switch to view tool when staging 2024-09-06 21:27:34 +10:00
psychedelicious
e81fedba43 tidy(ui): disable preview images on every enqueue 2024-09-06 21:27:34 +10:00
psychedelicious
6a47f973b7 feat(ui): rough out save staging image 2024-09-06 21:27:34 +10:00
psychedelicious
7aa918cd46 feat(ui): staging area image visibility toggle 2024-09-06 21:27:34 +10:00
psychedelicious
183c9dd736 fix(ui): batch building after removing canvas files 2024-09-06 21:27:34 +10:00
psychedelicious
5621075cb7 feat(ui): make Graph class's getMetadataNode public 2024-09-06 21:27:34 +10:00
psychedelicious
b13d2087c2 tidy(ui): remove old canvas graphs 2024-09-06 21:27:34 +10:00
psychedelicious
89740af2ab fix(ui): do not select already-selected entity 2024-09-06 21:27:34 +10:00
psychedelicious
4329dfd128 tidy(ui): naming things 2024-09-06 21:27:34 +10:00
psychedelicious
bb18a82a9c tidy(ui): file organisation 2024-09-06 21:27:34 +10:00
psychedelicious
d073fe467d fix(ui): reset cursor pos when fitting document 2024-09-06 21:27:34 +10:00
psychedelicious
e37e885546 feat(ui): staging area works more better 2024-09-06 21:27:34 +10:00
psychedelicious
471ded85f7 feat(ui): staging area barely works 2024-09-06 21:27:34 +10:00
psychedelicious
e084655e69 feat(ui): consolidate konva API 2024-09-06 21:27:34 +10:00
psychedelicious
330acb55f4 feat(ui): consolidate konva API 2024-09-06 21:27:34 +10:00
psychedelicious
e1ace99e05 feat(ui): staging area (rendering wip) 2024-09-06 21:27:34 +10:00
psychedelicious
c0bfa07ea7 tidy(ui): type "Dimensions" -> "Size" 2024-09-06 21:27:34 +10:00
psychedelicious
f4fceac372 feat(ui): add updateNode to Graph 2024-09-06 21:27:34 +10:00
psychedelicious
c1f5345987 feat(ui): sdxl graphs 2024-09-06 21:27:34 +10:00
psychedelicious
05992130d0 feat(ui): sd1 outpaint graph 2024-09-06 21:27:34 +10:00
psychedelicious
9c646712e0 tests(ui): add missing tests for Graph class 2024-09-06 21:27:34 +10:00
psychedelicious
0edff49957 feat(ui): add Graph.getid() util 2024-09-06 21:27:34 +10:00
psychedelicious
0f709cb06a feat(ui): outpaint graph, organize builder a bit 2024-09-06 21:27:34 +10:00
psychedelicious
656dbbb9f1 feat(ui): inpaint sd1 graph 2024-09-06 21:27:34 +10:00
psychedelicious
9497a75c95 feat(ui): temp disable image caching while testing 2024-09-06 21:27:34 +10:00
psychedelicious
30c4ed87b5 feat(ui): txt2img & img2img graphs 2024-09-06 21:27:34 +10:00
psychedelicious
e839765ddc feat(ui): minor change to canvas bbox state type 2024-09-06 21:27:34 +10:00
psychedelicious
055737a6e8 feat(ui): simplified konva node to blob/imagedata utils 2024-09-06 21:27:34 +10:00
psychedelicious
fbc609230a feat(ui): node manager getter/setter 2024-09-06 21:27:34 +10:00
psychedelicious
46f86a54c1 feat(ui): generation mode calculation, fudged graphs 2024-09-06 21:27:34 +10:00
psychedelicious
70f5231020 feat(ui): add utils for getting images from canvas 2024-09-06 21:27:34 +10:00
psychedelicious
0ec0feed2c feat(ui): even more simplified API - lean on the konva node manager to abstract imperative state API & rendering 2024-09-06 21:27:34 +10:00
psychedelicious
60cd505ee1 feat(ui): revised docstrings for renderers & simplified api 2024-09-06 21:27:34 +10:00
psychedelicious
205a719649 feat(ui): inpaint mask UI components 2024-09-06 21:27:34 +10:00
psychedelicious
af7d222a1e feat(ui): inpaint mask rendering (wip) 2024-09-06 21:27:34 +10:00
psychedelicious
70f430f635 fix(ui): models loaded handler 2024-09-06 21:27:34 +10:00
psychedelicious
2a92a223f6 feat(ui): internal state for inpaint mask 2024-09-06 21:27:34 +10:00
psychedelicious
1ed43614c2 refactor(ui): divvy up canvas state a bit 2024-09-06 21:27:34 +10:00
psychedelicious
3a5295574f feat(ui): get region and base layer canvas to blob logic working 2024-09-06 21:27:34 +10:00
psychedelicious
475b1cb1b8 refactor(ui): node manager handles more tedious annoying stuff 2024-09-06 21:27:34 +10:00
psychedelicious
55260a886d feat(ui): use node manager for addRegions 2024-09-06 21:27:34 +10:00
psychedelicious
f77e03ceb0 feat(ui): persist bbox 2024-09-06 21:27:34 +10:00
psychedelicious
4f7bf5ad58 fix(ui): fix generation graphs 2024-09-06 21:27:34 +10:00
psychedelicious
460ea1aa07 feat(ui): add toggle for clipToBbox 2024-09-06 21:27:34 +10:00
psychedelicious
7aa5f5cb80 feat(ui): rename konva node manager 2024-09-06 21:27:34 +10:00
psychedelicious
9cc4133184 refactor(ui): create classes to abstract mgmt of konva nodes 2024-09-06 21:27:34 +10:00
psychedelicious
0d3721324d tidy(ui): organise renderers 2024-09-06 21:27:34 +10:00
psychedelicious
510249d282 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-09-06 21:27:34 +10:00
psychedelicious
77d840593b perf(ui): fix lag w/ region rendering
Needed to memoize these selectors
2024-09-06 21:27:34 +10:00
psychedelicious
8c9a5c4ab5 feat(ui): move canvas fill color picker to right 2024-09-06 21:27:34 +10:00
psychedelicious
ce497fff27 refactor(ui): remove unused ellipse & polygon objects 2024-09-06 21:27:34 +10:00
psychedelicious
c3cada6bd5 fix(ui): incorrect rect/brush/eraser positions 2024-09-06 21:27:34 +10:00
psychedelicious
bbacfe403c refactor(ui): enable global debugging flag 2024-09-06 21:27:34 +10:00
psychedelicious
159031a071 refactor(ui): disable the preview renderer for now 2024-09-06 21:27:34 +10:00
psychedelicious
4067927a23 tweak(ui): canvas editor layout 2024-09-06 21:27:34 +10:00
psychedelicious
d090343083 perf(ui): memoize layeractionsmenu valid actions 2024-09-06 21:27:34 +10:00
psychedelicious
ed130366db 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-09-06 21:27:34 +10:00
psychedelicious
d4ae40fec4 feat(ui): clip lines to bbox 2024-09-06 21:27:34 +10:00
psychedelicious
17c864dba3 fix(ui): document fit positioning 2024-09-06 21:27:34 +10:00
psychedelicious
398d6d1efd feat(ui): document bounds overlay 2024-09-06 21:27:34 +10:00
psychedelicious
30b46b0f9b tidy(ui): background layer 2024-09-06 21:27:34 +10:00
psychedelicious
2d123fa11c refactor(ui): use "entity" instead of "data" for canvas 2024-09-06 21:27:34 +10:00
psychedelicious
352a651ac0 feat(ui): brush size border radius = 1 2024-09-06 21:27:34 +10:00
psychedelicious
26b971978d fix(ui): canvas HUD doesn't interrupt tool 2024-09-06 21:27:34 +10:00
psychedelicious
478324ea62 refactor(ui): split up canvas entity renderers, temp disable preview 2024-09-06 21:27:34 +10:00
psychedelicious
fa447b2813 fix(ui): delete all layers button 2024-09-06 21:27:34 +10:00
psychedelicious
322790bfdb fix(ui): ignore keyboard shortcuts in input/textarea elements 2024-09-06 21:27:34 +10:00
psychedelicious
9ea7c18d66 fix(ui): canvas entity ids getting clobbered 2024-09-06 21:27:34 +10:00
psychedelicious
2a0d16d57d fix(ui): move lora followup fixes 2024-09-06 21:27:34 +10:00
psychedelicious
033e2b27a9 chore(ui): lint 2024-09-06 21:27:34 +10:00
psychedelicious
c7af454bf5 refactor(ui): move loras to canvas slice 2024-09-06 21:27:34 +10:00
psychedelicious
d2be9df7a7 fix(ui): layer is selected when added 2024-09-06 21:27:34 +10:00
psychedelicious
ae81f4e20a feat(ui): r to center & fit stage on document 2024-09-06 21:27:34 +10:00
psychedelicious
261c24b704 feat(ui): better HUD 2024-09-06 21:27:34 +10:00
psychedelicious
cc1995170e fix(ui): always use current brush width when making straight lines 2024-09-06 21:27:34 +10:00
psychedelicious
86c785fded feat(ui): hold shift w/ brush to draw straight line 2024-09-06 21:27:34 +10:00
psychedelicious
31c2b1af19 fix(ui): update bg on canvas resize 2024-09-06 21:27:34 +10:00
psychedelicious
90b973f529 refactor(ui): better hud 2024-09-06 21:27:34 +10:00
psychedelicious
1164763e1a refactor(ui): scaled tool preview border 2024-09-06 21:27:34 +10:00
psychedelicious
b4319630b1 refactor(ui): port remaining canvasV1 rendering logic to V2, remove old code 2024-09-06 21:27:34 +10:00
psychedelicious
0586c6bdf2 refactor(ui): fix more types 2024-09-06 21:27:34 +10:00
psychedelicious
c2f60f33e6 refactor(ui): metadata recall (wip)
just enough let the app run
2024-09-06 21:27:34 +10:00
psychedelicious
b118bc959d refactor(ui): undo/redo button temp fix 2024-09-06 21:27:34 +10:00
psychedelicious
6ef5c2e0c3 refactor(ui): fix renderer stuff 2024-09-06 21:27:34 +10:00
psychedelicious
728fd5b758 refactor(ui): fix misc types 2024-09-06 21:27:34 +10:00
psychedelicious
5bae455e4e refactor(ui): fix gallery stuff 2024-09-06 21:27:34 +10:00
psychedelicious
9387903491 refactor(ui): fix delete image stuff 2024-09-06 21:27:34 +10:00
psychedelicious
12b67bd556 refactor(ui): fix useIsReadyToEnqueue for new adapterType field 2024-09-06 21:27:34 +10:00
psychedelicious
9a30bcbd94 refactor(ui): update generation tab graphs 2024-09-06 21:27:34 +10:00
psychedelicious
ab0f096bf2 refactor(ui): add adapterType to ControlAdapterData 2024-09-06 21:27:34 +10:00
psychedelicious
2d8a29d2fd refactor(ui): update components & logic to use new unified slice (again) 2024-09-06 21:27:34 +10:00
psychedelicious
8d7de1543b refactor(ui): update components & logic to use new unified slice 2024-09-06 21:27:34 +10:00
psychedelicious
b1b41a9b0c refactor(ui): merge compositing, params into canvasV2 slice 2024-09-06 21:27:34 +10:00
psychedelicious
e9033040f6 refactor(ui): add scaled bbox state 2024-09-06 21:27:34 +10:00
psychedelicious
4388f00607 refactor(ui): update dnd/image upload 2024-09-06 21:27:34 +10:00
psychedelicious
9b8db06349 refactor(ui): update size/prompts state 2024-09-06 21:27:34 +10:00
psychedelicious
a4f55f6e5d refactor(ui): rip out old control adapter implementation 2024-09-06 21:27:34 +10:00
psychedelicious
d0cde66e92 refactor(ui): canvas v2 (wip)
fix entity count select
2024-09-06 21:27:34 +10:00
psychedelicious
f636c0eb88 refactor(ui): canvas v2 (wip)
delete unused file
2024-09-06 21:27:34 +10:00
psychedelicious
5760d3180e 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-09-06 21:27:34 +10:00
psychedelicious
8c4f98131b refactor(ui): canvas v2 (wip)
Fix a few more components
2024-09-06 21:27:34 +10:00
psychedelicious
959a433e61 refactor(ui): canvas v2 (wip)
missed a spot
2024-09-06 21:27:34 +10:00
psychedelicious
f72845a1b4 refactor(ui): canvas v2 (wip)
Redo all UI components for different canvas entity types
2024-09-06 21:27:34 +10:00
psychedelicious
254f4ba574 refactor(ui): canvas v2 (wip) 2024-09-06 21:27:34 +10:00
psychedelicious
d6f3b1b85f refactor(ui): canvas v2 (wip) 2024-09-06 21:27:34 +10:00
psychedelicious
47b5b7c4b4 refactor(ui): canvas v2 (wip) 2024-09-06 21:27:34 +10:00
psychedelicious
bcfdae62e3 refactor(ui): canvas v2 (wip) 2024-09-06 21:27:34 +10:00
psychedelicious
c60f1c0031 feat(ui): bbox tool 2024-09-06 21:27:34 +10:00
psychedelicious
d76802f563 fix(ui): rect tool preview 2024-09-06 21:27:34 +10:00
psychedelicious
7b39b31f6c fix(ui): multiple stages 2024-09-06 21:27:34 +10:00
psychedelicious
a2585a8bb1 feat(ui): decouple konva logic from nanostores 2024-09-06 21:27:34 +10:00
psychedelicious
fe0c4767c7 feat(ui): store all stage attrs in nanostores 2024-09-06 21:27:34 +10:00
psychedelicious
e0b8b82a15 feat(ui): round stage scale 2024-09-06 21:27:34 +10:00
psychedelicious
f90fa85e77 chore(ui): bump konva 2024-09-06 21:27:34 +10:00
psychedelicious
6d5b4e4471 feat(ui): generation bbox transformation working
whew
2024-09-06 21:27:34 +10:00
psychedelicious
1172a33117 feat(ui): wip generation bbox 2024-09-06 21:27:34 +10:00
psychedelicious
8530f8ddcc feat(ui): wip generation bbox 2024-09-06 21:27:34 +10:00
psychedelicious
e66e4fefed feat(ui): CL zoom and pan, some rendering optimizations 2024-09-06 21:27:34 +10:00
psychedelicious
1237e839ca Revert "feat(ui): add x,y,scaleX,scaleY,rotation to objects"
This reverts commit 53318b396c967c72326a7e4dea09667b2ab20bdd.
2024-09-06 21:27:34 +10:00
psychedelicious
8ec08063f4 feat(ui): layers manage their own bbox 2024-09-06 21:27:34 +10:00
psychedelicious
0d0004018b docs(ui): konva image object docstrings 2024-09-06 21:27:34 +10:00
psychedelicious
9124604dc4 feat(ui): add x,y,scaleX,scaleY,rotation to objects 2024-09-06 21:27:34 +10:00
psychedelicious
4f05a7b8d0 fix(ui): show color picker when using rect tool 2024-09-06 21:27:34 +10:00
psychedelicious
db766bd9ae feat(ui): image loading fallback for raster layers 2024-09-06 21:27:34 +10:00
psychedelicious
557603d2ae feat(ui): bbox calc for raster layers 2024-09-06 21:27:34 +10:00
psychedelicious
b46ca55c7d feat(ui): do not fill brush preview when drawing 2024-09-06 21:27:33 +10:00
psychedelicious
4788172206 fix(ui): brush spacing handling 2024-09-06 21:27:33 +10:00
psychedelicious
6a118f172e fix(ui): jank when starting a shape when not already focused on stage 2024-09-06 21:27:33 +10:00
psychedelicious
d5abdfa3b0 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-09-06 21:27:33 +10:00
psychedelicious
1d24cb94b4 feat(ui): support image objects on raster layers
Just the UI and internal state, not rendering yet.
2024-09-06 21:27:33 +10:00
psychedelicious
86e7f24238 tidy(ui): clean up event handlers
Separate logic for each tool in preparation for ellipse and polygon tools.
2024-09-06 21:27:33 +10:00
psychedelicious
0356f970f3 feat(ui): raster layer reset, object group util 2024-09-06 21:27:33 +10:00
psychedelicious
d340038f48 feat(ui): rect shape preview now has fill 2024-09-06 21:27:33 +10:00
psychedelicious
1d5e8e07c6 feat(ui): cancel shape drawing on esc 2024-09-06 21:27:33 +10:00
psychedelicious
24f17479de feat(ui): temp disable history on CL 2024-09-06 21:27:33 +10:00
psychedelicious
84b8096cc9 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-09-06 21:27:33 +10:00
psychedelicious
5c38241e33 feat(ui): add raster layer rendering and interaction (WIP) 2024-09-06 21:27:33 +10:00
psychedelicious
b590c73c08 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-09-06 21:27:33 +10:00
psychedelicious
68fcf9d2df 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-09-06 21:27:33 +10:00
Brandon Rising
bda579577c chore: 4.2.9 version bump 2024-09-05 16:17:48 -04:00
Brandon Rising
a16b555d47 Simplify flux model dtype conversion in model loader 2024-09-05 15:47:14 -04:00
Brandon Rising
6667c39c73 Remove dependency of asizeof 2024-09-05 15:47:14 -04:00
Brandon Rising
5219ac12a6 Add comment explaining the cache make room call 2024-09-05 15:47:14 -04:00
Brandon Rising
445f813fb9 Update flux transformer loader to more efficiently use and release memory during upcasting 2024-09-05 15:47:14 -04:00
Brandon Rising
87f9e59cfb Cast tensors in unquantized flux models to bfloat16 during loading 2024-09-05 15:47:14 -04:00
Phrixus2023
8b03b39aa8 translationBot(ui): update translation (Chinese (Simplified Han script))
Currently translated at 97.6% (1342 of 1374 strings)

Co-authored-by: Phrixus2023 <920414016@qq.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/zh_Hans/
Translation: InvokeAI/Web UI
2024-09-05 15:34:13 -04:00
Tobias Lechner
e59b6bb971 translationBot(ui): update translation (German)
Currently translated at 63.3% (870 of 1374 strings)

Co-authored-by: Tobias Lechner <me@tobias-lechner.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/
Translation: InvokeAI/Web UI
2024-09-05 15:34:13 -04:00
Riccardo Giovanetti
24a7ed467c translationBot(ui): update translation (Italian)
Currently translated at 98.2% (1350 of 1374 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.2% (1350 of 1374 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.2% (1350 of 1374 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.4% (1349 of 1370 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.4% (1348 of 1369 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2024-09-05 15:34:13 -04:00
Васянатор
f01f1033ac translationBot(ui): update translation (Russian)
Currently translated at 100.0% (1370 of 1370 strings)

translationBot(ui): update translation (Russian)

Currently translated at 100.0% (1369 of 1369 strings)

Co-authored-by: Васянатор <ilabulanov339@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/
Translation: InvokeAI/Web UI
2024-09-05 15:34:13 -04:00
smk-e
d35f515413 translationBot(ui): update translation (Spanish)
Currently translated at 33.0% (452 of 1369 strings)

Co-authored-by: smk-e <jit-r8@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/es/
Translation: InvokeAI/Web UI
2024-09-05 15:34:13 -04:00
101 changed files with 1539 additions and 890 deletions

View File

@@ -11,7 +11,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
Batch,
BatchStatus,
CancelByBatchIDsResult,
CancelByOriginResult,
CancelByDestinationResult,
ClearResult,
EnqueueBatchResult,
PruneResult,
@@ -107,16 +107,18 @@ async def cancel_by_batch_ids(
@session_queue_router.put(
"/{queue_id}/cancel_by_origin",
operation_id="cancel_by_origin",
"/{queue_id}/cancel_by_destination",
operation_id="cancel_by_destination",
responses={200: {"model": CancelByBatchIDsResult}},
)
async def cancel_by_origin(
async def cancel_by_destination(
queue_id: str = Path(description="The queue id to perform this operation on"),
origin: str = Query(description="The origin to cancel all queue items for"),
) -> CancelByOriginResult:
destination: str = Query(description="The destination to cancel all queue items for"),
) -> CancelByDestinationResult:
"""Immediately cancels all queue items with the given origin"""
return ApiDependencies.invoker.services.session_queue.cancel_by_origin(queue_id=queue_id, origin=origin)
return ApiDependencies.invoker.services.session_queue.cancel_by_destination(
queue_id=queue_id, destination=destination
)
@session_queue_router.put(

View File

@@ -6,7 +6,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
Batch,
BatchStatus,
CancelByBatchIDsResult,
CancelByOriginResult,
CancelByDestinationResult,
CancelByQueueIDResult,
ClearResult,
EnqueueBatchResult,
@@ -97,8 +97,8 @@ class SessionQueueBase(ABC):
pass
@abstractmethod
def cancel_by_origin(self, queue_id: str, origin: str) -> CancelByOriginResult:
"""Cancels all queue items with the given batch origin"""
def cancel_by_destination(self, queue_id: str, destination: str) -> CancelByDestinationResult:
"""Cancels all queue items with the given batch destination"""
pass
@abstractmethod

View File

@@ -346,10 +346,10 @@ class CancelByBatchIDsResult(BaseModel):
canceled: int = Field(..., description="Number of queue items canceled")
class CancelByOriginResult(BaseModel):
"""Result of canceling by list of batch ids"""
class CancelByDestinationResult(CancelByBatchIDsResult):
"""Result of canceling by a destination"""
canceled: int = Field(..., description="Number of queue items canceled")
pass
class CancelByQueueIDResult(CancelByBatchIDsResult):

View File

@@ -10,7 +10,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
Batch,
BatchStatus,
CancelByBatchIDsResult,
CancelByOriginResult,
CancelByDestinationResult,
CancelByQueueIDResult,
ClearResult,
EnqueueBatchResult,
@@ -426,19 +426,19 @@ class SqliteSessionQueue(SessionQueueBase):
self.__lock.release()
return CancelByBatchIDsResult(canceled=count)
def cancel_by_origin(self, queue_id: str, origin: str) -> CancelByOriginResult:
def cancel_by_destination(self, queue_id: str, destination: str) -> CancelByDestinationResult:
try:
current_queue_item = self.get_current(queue_id)
self.__lock.acquire()
where = """--sql
WHERE
queue_id == ?
AND origin == ?
AND destination == ?
AND status != 'canceled'
AND status != 'completed'
AND status != 'failed'
"""
params = (queue_id, origin)
params = (queue_id, destination)
self.__cursor.execute(
f"""--sql
SELECT COUNT(*)
@@ -457,14 +457,14 @@ class SqliteSessionQueue(SessionQueueBase):
params,
)
self.__conn.commit()
if current_queue_item is not None and current_queue_item.origin == origin:
if current_queue_item is not None and current_queue_item.destination == destination:
self._set_queue_item_status(current_queue_item.item_id, "canceled")
except Exception:
self.__conn.rollback()
raise
finally:
self.__lock.release()
return CancelByOriginResult(canceled=count)
return CancelByDestinationResult(canceled=count)
def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
try:

View File

@@ -32,7 +32,9 @@ from invokeai.backend.model_manager.config import (
)
from invokeai.backend.model_manager.load.load_default import ModelLoader
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
from invokeai.backend.model_manager.util.model_util import convert_bundle_to_flux_transformer_checkpoint
from invokeai.backend.model_manager.util.model_util import (
convert_bundle_to_flux_transformer_checkpoint,
)
from invokeai.backend.util.silence_warnings import SilenceWarnings
try:
@@ -193,6 +195,11 @@ class FluxCheckpointModel(ModelLoader):
sd = load_file(model_path)
if "model.diffusion_model.double_blocks.0.img_attn.norm.key_norm.scale" in sd:
sd = convert_bundle_to_flux_transformer_checkpoint(sd)
new_sd_size = sum([ten.nelement() * torch.bfloat16.itemsize for ten in sd.values()])
self._ram_cache.make_room(new_sd_size)
for k in sd.keys():
# We need to cast to bfloat16 due to it being the only currently supported dtype for inference
sd[k] = sd[k].to(torch.bfloat16)
model.load_state_dict(sd, assign=True)
return model

View File

@@ -11,6 +11,8 @@ const config: KnipConfig = {
'src/features/nodes/types/v2/**',
// TODO(psyche): maybe we can clean up these utils after canvas v2 release
'src/features/controlLayers/konva/util.ts',
// TODO(psyche): restore HRF functionality?
'src/features/hrf/**',
],
ignoreBinaries: ['only-allow'],
paths: {

View File

@@ -127,7 +127,14 @@
"bulkDownloadRequestedDesc": "Dein Download wird vorbereitet. Dies kann ein paar Momente dauern.",
"bulkDownloadRequestFailed": "Problem beim Download vorbereiten",
"bulkDownloadFailed": "Download fehlgeschlagen",
"alwaysShowImageSizeBadge": "Zeige immer Bilder Größe Abzeichen"
"alwaysShowImageSizeBadge": "Zeige immer Bilder Größe Abzeichen",
"selectForCompare": "Zum Vergleichen auswählen",
"compareImage": "Bilder vergleichen",
"exitSearch": "Suche beenden",
"newestFirst": "Neueste zuerst",
"oldestFirst": "Älteste zuerst",
"openInViewer": "Im Viewer öffnen",
"swapImages": "Bilder tauschen"
},
"hotkeys": {
"keyboardShortcuts": "Tastenkürzel",
@@ -631,7 +638,8 @@
"archived": "Archiviert",
"noBoards": "Kein {boardType}} Ordner",
"hideBoards": "Ordner verstecken",
"viewBoards": "Ordner ansehen"
"viewBoards": "Ordner ansehen",
"deletedPrivateBoardsCannotbeRestored": "Gelöschte Boards können nicht wiederhergestellt werden. Wenn Sie „Nur Board löschen“ wählen, werden die Bilder in einen privaten, nicht kategorisierten Status für den Ersteller des Bildes versetzt."
},
"controlnet": {
"showAdvanced": "Zeige Erweitert",
@@ -781,7 +789,9 @@
"batchFieldValues": "Stapelverarbeitungswerte",
"batchQueued": "Stapelverarbeitung eingereiht",
"graphQueued": "Graph eingereiht",
"graphFailedToQueue": "Fehler beim Einreihen des Graphen"
"graphFailedToQueue": "Fehler beim Einreihen des Graphen",
"generations_one": "Generation",
"generations_other": "Generationen"
},
"metadata": {
"negativePrompt": "Negativ Beschreibung",
@@ -1146,5 +1156,10 @@
"noMatchingTriggers": "Keine passenden Trigger",
"addPromptTrigger": "Prompt-Trigger hinzufügen",
"compatibleEmbeddings": "Kompatible Einbettungen"
},
"ui": {
"tabs": {
"queue": "Warteschlange"
}
}
}

View File

@@ -134,6 +134,7 @@
"nodes": "Workflows",
"notInstalled": "Not $t(common.installed)",
"openInNewTab": "Open in New Tab",
"openInViewer": "Open in Viewer",
"orderBy": "Order By",
"outpaint": "outpaint",
"outputs": "Outputs",
@@ -1014,6 +1015,8 @@
"noModelForControlAdapter": "Control Adapter #{{number}} has no model selected.",
"incompatibleBaseModelForControlAdapter": "Control Adapter #{{number}} model is incompatible with main model.",
"noModelSelected": "No model selected",
"canvasManagerNotLoaded": "Canvas Manager not loaded",
"canvasBusy": "Canvas is busy",
"noPrompts": "No prompts generated",
"noNodesInGraph": "No nodes in graph",
"systemDisconnected": "System disconnected",
@@ -1049,8 +1052,7 @@
"seamlessYAxis": "Seamless Tiling Y Axis",
"seed": "Seed",
"imageActions": "Image Actions",
"sendToImg2Img": "Send to Image to Image",
"sendToUnifiedCanvas": "Send To Unified Canvas",
"sendToCanvas": "Send To Canvas",
"sendToUpscale": "Send To Upscale",
"showOptionsPanel": "Show Side Panel (O or T)",
"shuffle": "Shuffle Seed",
@@ -1191,8 +1193,8 @@
"problemSavingMaskDesc": "Unable to export mask",
"prunedQueue": "Pruned Queue",
"resetInitialImage": "Reset Initial Image",
"sentToImageToImage": "Sent To Image To Image",
"sentToUnifiedCanvas": "Sent to Unified Canvas",
"sentToCanvas": "Sent to Canvas",
"sentToUpscale": "Sent to Upscale",
"serverError": "Server Error",
"sessionRef": "Session: {{sessionId}}",
"setAsCanvasInitialImage": "Set as canvas initial image",
@@ -1655,6 +1657,7 @@
},
"controlLayers": {
"bookmark": "Bookmark for Quick Switch",
"fitBboxToLayers": "Fit Bbox To Layers",
"removeBookmark": "Remove Bookmark",
"saveCanvasToGallery": "Save Canvas To Gallery",
"saveBboxToGallery": "Save Bbox To Gallery",

View File

@@ -86,15 +86,15 @@
"loadMore": "Cargar más",
"noImagesInGallery": "No hay imágenes para mostrar",
"deleteImage_one": "Eliminar Imagen",
"deleteImage_many": "",
"deleteImage_other": "",
"deleteImage_many": "Eliminar {{count}} Imágenes",
"deleteImage_other": "Eliminar {{count}} Imágenes",
"deleteImagePermanent": "Las imágenes eliminadas no se pueden restaurar.",
"assets": "Activos",
"autoAssignBoardOnClick": "Asignación automática de tableros al hacer clic"
},
"hotkeys": {
"keyboardShortcuts": "Atajos de teclado",
"appHotkeys": "Atajos de applicación",
"appHotkeys": "Atajos de aplicación",
"generalHotkeys": "Atajos generales",
"galleryHotkeys": "Atajos de galería",
"unifiedCanvasHotkeys": "Atajos de lienzo unificado",
@@ -535,7 +535,7 @@
"bottomMessage": "Al eliminar este panel y las imágenes que contiene, se restablecerán las funciones que los estén utilizando actualmente.",
"deleteBoardAndImages": "Borrar el panel y las imágenes",
"loading": "Cargando...",
"deletedBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar",
"deletedBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar. Al Seleccionar 'Borrar Solo el Panel' transferirá las imágenes a un estado sin categorizar.",
"move": "Mover",
"menuItemAutoAdd": "Agregar automáticamente a este panel",
"searchBoard": "Buscando paneles…",
@@ -549,7 +549,13 @@
"imagesWithCount_other": "{{count}} imágenes",
"assetsWithCount_one": "{{count}} activo",
"assetsWithCount_many": "{{count}} activos",
"assetsWithCount_other": "{{count}} activos"
"assetsWithCount_other": "{{count}} activos",
"hideBoards": "Ocultar Paneles",
"addPrivateBoard": "Agregar un tablero privado",
"addSharedBoard": "Agregar Panel Compartido",
"boards": "Paneles",
"archiveBoard": "Archivar Panel",
"archived": "Archivado"
},
"accordions": {
"compositing": {

View File

@@ -496,7 +496,9 @@
"main": "Principali",
"noModelsInstalledDesc1": "Installa i modelli con",
"ipAdapters": "Adattatori IP",
"noMatchingModels": "Nessun modello corrispondente"
"noMatchingModels": "Nessun modello corrispondente",
"starterModelsInModelManager": "I modelli iniziali possono essere trovati in Gestione Modelli",
"spandrelImageToImage": "Immagine a immagine (Spandrel)"
},
"parameters": {
"images": "Immagini",
@@ -510,7 +512,7 @@
"perlinNoise": "Rumore Perlin",
"type": "Tipo",
"strength": "Forza",
"upscaling": "Ampliamento",
"upscaling": "Amplia",
"scale": "Scala",
"imageFit": "Adatta l'immagine iniziale alle dimensioni di output",
"scaleBeforeProcessing": "Scala prima dell'elaborazione",
@@ -593,7 +595,7 @@
"globalPositivePromptPlaceholder": "Prompt positivo globale",
"globalNegativePromptPlaceholder": "Prompt negativo globale",
"processImage": "Elabora Immagine",
"sendToUpscale": "Invia a Ampliare",
"sendToUpscale": "Invia a Amplia",
"postProcessing": "Post-elaborazione (Shift + U)"
},
"settings": {
@@ -1420,7 +1422,7 @@
"paramUpscaleMethod": {
"heading": "Metodo di ampliamento",
"paragraphs": [
"Metodo utilizzato per eseguire l'ampliamento dell'immagine per la correzione ad alta risoluzione."
"Metodo utilizzato per ampliare l'immagine per la correzione ad alta risoluzione."
]
},
"patchmatchDownScaleSize": {
@@ -1528,7 +1530,7 @@
},
"upscaleModel": {
"paragraphs": [
"Il modello di ampliamento (Upscale), scala l'immagine alle dimensioni di uscita prima di aggiungere i dettagli. È possibile utilizzare qualsiasi modello di ampliamento supportato, ma alcuni sono specializzati per diversi tipi di immagini, come foto o disegni al tratto."
"Il modello di ampliamento, scala l'immagine alle dimensioni di uscita prima di aggiungere i dettagli. È possibile utilizzare qualsiasi modello di ampliamento supportato, ma alcuni sono specializzati per diversi tipi di immagini, come foto o disegni al tratto."
],
"heading": "Modello di ampliamento"
},
@@ -1720,26 +1722,27 @@
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
"queue": "Coda",
"queueTab": "$t(ui.tabs.queue) $t(common.tab)",
"upscaling": "Ampliamento",
"upscaling": "Amplia",
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
}
},
"upscaling": {
"creativity": "Creatività",
"structure": "Struttura",
"upscaleModel": "Modello di Ampliamento",
"upscaleModel": "Modello di ampliamento",
"scale": "Scala",
"missingModelsWarning": "Visita <LinkComponent>Gestione modelli</LinkComponent> per installare i modelli richiesti:",
"mainModelDesc": "Modello principale (architettura SD1.5 o SDXL)",
"tileControlNetModelDesc": "Modello Tile ControlNet per l'architettura del modello principale scelto",
"upscaleModelDesc": "Modello per l'ampliamento (da immagine a immagine)",
"upscaleModelDesc": "Modello per l'ampliamento (immagine a immagine)",
"missingUpscaleInitialImage": "Immagine iniziale mancante per l'ampliamento",
"missingUpscaleModel": "Modello per lampliamento mancante",
"missingTileControlNetModel": "Nessun modello ControlNet Tile valido installato",
"postProcessingModel": "Modello di post-elaborazione",
"postProcessingMissingModelWarning": "Visita <LinkComponent>Gestione modelli</LinkComponent> per installare un modello di post-elaborazione (da immagine a immagine).",
"exceedsMaxSize": "Le impostazioni di ampliamento superano il limite massimo delle dimensioni",
"exceedsMaxSizeDetails": "Il limite massimo di ampliamento è {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixel. Prova un'immagine più piccola o diminuisci la scala selezionata."
"exceedsMaxSizeDetails": "Il limite massimo di ampliamento è {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixel. Prova un'immagine più piccola o diminuisci la scala selezionata.",
"upscale": "Amplia"
},
"upsell": {
"inviteTeammates": "Invita collaboratori",
@@ -1789,6 +1792,7 @@
"positivePromptColumn": "'prompt' o 'positive_prompt'",
"noTemplates": "Nessun modello",
"acceptedColumnsKeys": "Colonne/chiavi accettate:",
"templateActions": "Azioni modello"
"templateActions": "Azioni modello",
"promptTemplateCleared": "Modello di prompt cancellato"
}
}

View File

@@ -501,7 +501,8 @@
"noModelsInstalled": "Нет установленных моделей",
"noModelsInstalledDesc1": "Установите модели с помощью",
"noMatchingModels": "Нет подходящих моделей",
"ipAdapters": "IP адаптеры"
"ipAdapters": "IP адаптеры",
"starterModelsInModelManager": "Стартовые модели можно найти в Менеджере моделей"
},
"parameters": {
"images": "Изображения",
@@ -1758,7 +1759,8 @@
"postProcessingModel": "Модель постобработки",
"tileControlNetModelDesc": "Модель ControlNet для выбранной архитектуры основной модели",
"missingModelsWarning": "Зайдите в <LinkComponent>Менеджер моделей</LinkComponent> чтоб установить необходимые модели:",
"postProcessingMissingModelWarning": "Посетите <LinkComponent>Менеджер моделей</LinkComponent>, чтобы установить модель постобработки (img2img)."
"postProcessingMissingModelWarning": "Посетите <LinkComponent>Менеджер моделей</LinkComponent>, чтобы установить модель постобработки (img2img).",
"upscale": "Увеличить"
},
"stylePresets": {
"noMatchingTemplates": "Нет подходящих шаблонов",
@@ -1804,7 +1806,8 @@
"noTemplates": "Нет шаблонов",
"promptTemplatesDesc2": "Используйте строку-заполнитель <Pre>{{placeholder}}</Pre>, чтобы указать место, куда должен быть включен ваш запрос в шаблоне.",
"searchByName": "Поиск по имени",
"shared": "Общий"
"shared": "Общий",
"promptTemplateCleared": "Шаблон запроса создан"
},
"upsell": {
"inviteTeammates": "Пригласите членов команды",

View File

@@ -154,7 +154,8 @@
"displaySearch": "显示搜索",
"stretchToFit": "拉伸以适应",
"exitCompare": "退出对比",
"compareHelp1": "在点击图库中的图片或使用箭头键切换比较图片时,请按住<Kbd>Alt</Kbd> 键。"
"compareHelp1": "在点击图库中的图片或使用箭头键切换比较图片时,请按住<Kbd>Alt</Kbd> 键。",
"go": "运行"
},
"hotkeys": {
"keyboardShortcuts": "快捷键",
@@ -494,7 +495,9 @@
"huggingFacePlaceholder": "所有者或模型名称",
"huggingFaceRepoID": "HuggingFace仓库ID",
"loraTriggerPhrases": "LoRA 触发词",
"ipAdapters": "IP适配器"
"ipAdapters": "IP适配器",
"spandrelImageToImage": "图生图(Spandrel)",
"starterModelsInModelManager": "您可以在模型管理器中找到初始模型"
},
"parameters": {
"images": "图像",
@@ -695,7 +698,9 @@
"outOfMemoryErrorDesc": "您当前的生成设置已超出系统处理能力.请调整设置后再次尝试.",
"parametersSet": "参数已恢复",
"errorCopied": "错误信息已复制",
"modelImportCanceled": "模型导入已取消"
"modelImportCanceled": "模型导入已取消",
"importFailed": "导入失败",
"importSuccessful": "导入成功"
},
"unifiedCanvas": {
"layer": "图层",
@@ -1705,12 +1710,55 @@
"missingModelsWarning": "请访问<LinkComponent>模型管理器</LinkComponent> 安装所需的模型:",
"mainModelDesc": "主模型SD1.5或SDXL架构",
"exceedsMaxSize": "放大设置超出了最大尺寸限制",
"exceedsMaxSizeDetails": "最大放大限制是 {{maxUpscaleDimension}}x{{maxUpscaleDimension}} 像素.请尝试一个较小的图像或减少您的缩放选择."
"exceedsMaxSizeDetails": "最大放大限制是 {{maxUpscaleDimension}}x{{maxUpscaleDimension}} 像素.请尝试一个较小的图像或减少您的缩放选择.",
"upscale": "放大"
},
"upsell": {
"inviteTeammates": "邀请团队成员",
"professional": "专业",
"professionalUpsell": "可在 Invoke 的专业版中使用.点击此处或访问 invoke.com/pricing 了解更多详情.",
"shareAccess": "共享访问权限"
},
"stylePresets": {
"positivePrompt": "正向提示词",
"preview": "预览",
"deleteImage": "删除图像",
"deleteTemplate": "删除模版",
"deleteTemplate2": "您确定要删除这个模板吗?请注意,删除后无法恢复.",
"importTemplates": "导入提示模板支持CSV或JSON格式",
"insertPlaceholder": "插入一个占位符",
"myTemplates": "我的模版",
"name": "名称",
"type": "类型",
"unableToDeleteTemplate": "无法删除提示模板",
"updatePromptTemplate": "更新提示词模版",
"exportPromptTemplates": "导出我的提示模板为CSV格式",
"exportDownloaded": "导出已下载",
"noMatchingTemplates": "无匹配的模版",
"promptTemplatesDesc1": "提示模板可以帮助您在编写提示时添加预设的文本内容.",
"promptTemplatesDesc3": "如果您没有使用占位符,那么模板的内容将会被添加到您提示的末尾.",
"searchByName": "按名称搜索",
"shared": "已分享",
"sharedTemplates": "已分享的模版",
"templateActions": "模版操作",
"templateDeleted": "提示模版已删除",
"toggleViewMode": "切换显示模式",
"uploadImage": "上传图像",
"active": "激活",
"choosePromptTemplate": "选择提示词模板",
"clearTemplateSelection": "清除模版选择",
"copyTemplate": "拷贝模版",
"createPromptTemplate": "创建提示词模版",
"defaultTemplates": "默认模版",
"editTemplate": "编辑模版",
"exportFailed": "无法生成并下载CSV文件",
"flatten": "将选定的模板内容合并到当前提示中",
"negativePrompt": "反向提示词",
"promptTemplateCleared": "提示模板已清除",
"useForTemplate": "用于提示词模版",
"viewList": "预览模版列表",
"viewModeTooltip": "这是您的提示在当前选定的模板下的预览效果。如需编辑提示,请直接在文本框中点击进行修改.",
"noTemplates": "无模版",
"private": "私密"
}
}

View File

@@ -1,10 +1,11 @@
import { isAnyOf } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import {
sessionStagingAreaImageAccepted,
sessionStagingAreaReset,
} from 'features/controlLayers/store/canvasSessionSlice';
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import { canvasReset, rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
@@ -16,14 +17,16 @@ import { assert } from 'tsafe';
const log = logger('canvas');
const matchCanvasOrStagingAreaRest = isAnyOf(sessionStagingAreaReset, canvasReset);
export const addStagingListeners = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: sessionStagingAreaReset,
matcher: matchCanvasOrStagingAreaRest,
effect: async (_, { dispatch }) => {
try {
const req = dispatch(
queueApi.endpoints.cancelByBatchOrigin.initiate(
{ origin: 'canvas' },
queueApi.endpoints.cancelByBatchDestination.initiate(
{ destination: 'canvas' },
{ fixedCacheKey: 'cancelByBatchOrigin' }
)
);

View File

@@ -13,7 +13,7 @@ import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
import { modelChanged, refinerModelChanged, vaeSelected } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
import { calculateNewSize } from 'features/parameters/components/Bbox/calculateNewSize';
import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice';
import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas';
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';

View File

@@ -2,6 +2,7 @@ import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
@@ -17,6 +18,7 @@ import { selectSystemSlice } from 'features/system/store/systemSlice';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import i18n from 'i18next';
import { forEach, upperFirst } from 'lodash-es';
import { atom } from 'nanostores';
import { useMemo } from 'react';
import { getConnectedEdges } from 'reactflow';
@@ -28,7 +30,7 @@ const LAYER_TYPE_TO_TKEY = {
control_layer: 'controlLayers.globalControlAdapter',
} as const;
const createSelector = (templates: Templates, isConnected: boolean) =>
const createSelector = (templates: Templates, isConnected: boolean, canvasIsBusy: boolean) =>
createMemoizedSelector(
[
selectSystemSlice,
@@ -117,6 +119,10 @@ const createSelector = (templates: Templates, isConnected: boolean) =>
reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') });
}
} else {
if (canvasIsBusy) {
reasons.push({ content: i18n.t('parameters.invoke.canvasBusy') });
}
if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
reasons.push({ content: i18n.t('parameters.invoke.noPrompts') });
}
@@ -240,10 +246,17 @@ const createSelector = (templates: Templates, isConnected: boolean) =>
}
);
const dummyAtom = atom(true);
export const useIsReadyToEnqueue = () => {
const templates = useStore($templates);
const isConnected = useStore($isConnected);
const selector = useMemo(() => createSelector(templates, isConnected), [templates, isConnected]);
const canvasManager = useCanvasManagerSafe();
const canvasIsBusy = useStore(canvasManager?.$isBusy ?? dummyAtom);
const selector = useMemo(
() => createSelector(templates, isConnected, canvasIsBusy),
[templates, isConnected, canvasIsBusy]
);
const value = useAppSelector(selector);
return value;
};

View File

@@ -1,6 +1,9 @@
export const roundDownToMultiple = (num: number, multiple: number): number => {
return Math.floor(num / multiple) * multiple;
};
export const roundUpToMultiple = (num: number, multiple: number): number => {
return Math.ceil(num / multiple) * multiple;
};
export const roundToMultiple = (num: number, multiple: number): number => {
return Math.round(num / multiple) * multiple;

View File

@@ -1,20 +0,0 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { EntityListActionBarAddLayerButton } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuButton';
import { EntityListActionBarDeleteButton } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarDeleteButton';
import { EntityListActionBarSelectedEntityFill } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityFill';
import { SelectedEntityOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity';
import { memo } from 'react';
export const EntityListActionBar = memo(() => {
return (
<Flex w="full" py={1} px={1} gap={2} alignItems="center">
<SelectedEntityOpacity />
<Spacer />
<EntityListActionBarSelectedEntityFill />
<EntityListActionBarAddLayerButton />
<EntityListActionBarDeleteButton />
</Flex>
);
});
EntityListActionBar.displayName = 'EntityListActionBar';

View File

@@ -1,28 +0,0 @@
import { IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
export const EntityListActionBarAddLayerButton = memo(() => {
const { t } = useTranslation();
return (
<Menu>
<MenuButton
as={IconButton}
size="sm"
tooltip={t('controlLayers.addLayer')}
aria-label={t('controlLayers.addLayer')}
icon={<PiPlusBold />}
variant="ghost"
data-testid="control-layers-add-layer-menu-button"
/>
<MenuList>
<CanvasEntityListMenuItems />
</MenuList>
</Menu>
);
});
EntityListActionBarAddLayerButton.displayName = 'EntityListActionBarAddLayerButton';

View File

@@ -1,57 +0,0 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
import {
controlLayerAdded,
inpaintMaskAdded,
ipaAdded,
rasterLayerAdded,
rgAdded,
} from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
export const CanvasEntityListMenuItems = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const defaultIPAdapter = useDefaultIPAdapter();
const addInpaintMask = useCallback(() => {
dispatch(inpaintMaskAdded({ isSelected: true }));
}, [dispatch]);
const addRegionalGuidance = useCallback(() => {
dispatch(rgAdded({ isSelected: true }));
}, [dispatch]);
const addRasterLayer = useCallback(() => {
dispatch(rasterLayerAdded({ isSelected: true }));
}, [dispatch]);
const addControlLayer = useCallback(() => {
dispatch(controlLayerAdded({ isSelected: true }));
}, [dispatch]);
const addIPAdapter = useCallback(() => {
const overrides = { ipAdapter: defaultIPAdapter };
dispatch(ipaAdded({ isSelected: true, overrides }));
}, [defaultIPAdapter, dispatch]);
return (
<>
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask}>
{t('controlLayers.inpaintMask')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidance}>
{t('controlLayers.regionalGuidance')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
{t('controlLayers.rasterLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addControlLayer}>
{t('controlLayers.controlLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
{t('controlLayers.globalIPAdapter')}
</MenuItem>
</>
);
});
CanvasEntityListMenuItems.displayName = 'CanvasEntityListMenu';

View File

@@ -1,39 +0,0 @@
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { allEntitiesDeleted, entityDeleted } from 'features/controlLayers/store/canvasSlice';
import { selectEntityCount, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleFill } from 'react-icons/pi';
export const EntityListActionBarDeleteButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const entityCount = useAppSelector(selectEntityCount);
const shift = useShiftModifier();
const onClick = useCallback(() => {
if (shift) {
dispatch(allEntitiesDeleted());
return;
}
if (!selectedEntityIdentifier) {
return;
}
dispatch(entityDeleted({ entityIdentifier: selectedEntityIdentifier }));
}, [dispatch, selectedEntityIdentifier, shift]);
return (
<IconButton
onClick={onClick}
isDisabled={shift ? entityCount === 0 : !selectedEntityIdentifier}
size="sm"
variant="ghost"
aria-label={shift ? t('controlLayers.deleteAll') : t('controlLayers.deleteSelected')}
tooltip={shift ? t('controlLayers.deleteAll') : t('controlLayers.deleteSelected')}
icon={<PiTrashSimpleFill />}
/>
);
});
EntityListActionBarDeleteButton.displayName = 'EntityListActionBarDeleteButton';

View File

@@ -0,0 +1,20 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { EntityListGlobalActionBarAddLayerMenu } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu';
import { EntityListGlobalActionBarDenoisingStrength } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarDenoisingStrength';
import { EntityListGlobalActionBarFitBboxToLayers } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarFitBboxToLayers';
import { memo } from 'react';
export const EntityListGlobalActionBar = memo(() => {
return (
<Flex w="full" py={1} px={1} gap={2} alignItems="center">
<EntityListGlobalActionBarDenoisingStrength />
<Spacer />
<Flex>
<EntityListGlobalActionBarFitBboxToLayers />
<EntityListGlobalActionBarAddLayerMenu />
</Flex>
</Flex>
);
});
EntityListGlobalActionBar.displayName = 'EntityListGlobalActionBar';

View File

@@ -0,0 +1,69 @@
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
import {
controlLayerAdded,
inpaintMaskAdded,
ipaAdded,
rasterLayerAdded,
rgAdded,
} from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
export const EntityListGlobalActionBarAddLayerMenu = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const defaultIPAdapter = useDefaultIPAdapter();
const addInpaintMask = useCallback(() => {
dispatch(inpaintMaskAdded({ isSelected: true }));
}, [dispatch]);
const addRegionalGuidance = useCallback(() => {
dispatch(rgAdded({ isSelected: true }));
}, [dispatch]);
const addRasterLayer = useCallback(() => {
dispatch(rasterLayerAdded({ isSelected: true }));
}, [dispatch]);
const addControlLayer = useCallback(() => {
dispatch(controlLayerAdded({ isSelected: true }));
}, [dispatch]);
const addIPAdapter = useCallback(() => {
const overrides = { ipAdapter: defaultIPAdapter };
dispatch(ipaAdded({ isSelected: true, overrides }));
}, [defaultIPAdapter, dispatch]);
return (
<Menu>
<MenuButton
as={IconButton}
size="sm"
variant="link"
alignSelf="stretch"
tooltip={t('controlLayers.addLayer')}
aria-label={t('controlLayers.addLayer')}
icon={<PiPlusBold />}
data-testid="control-layers-add-layer-menu-button"
/>
<MenuList>
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask}>
{t('controlLayers.inpaintMask')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidance}>
{t('controlLayers.regionalGuidance')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
{t('controlLayers.rasterLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addControlLayer}>
{t('controlLayers.controlLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
{t('controlLayers.globalIPAdapter')}
</MenuItem>
</MenuList>
</Menu>
);
});
EntityListGlobalActionBarAddLayerMenu.displayName = 'EntityListGlobalActionBarAddLayerMenu';

View File

@@ -0,0 +1,124 @@
import {
CompositeSlider,
FormControl,
FormLabel,
IconButton,
NumberInput,
NumberInputField,
Popover,
PopoverAnchor,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { selectImg2imgStrength, setImg2imgStrength } from 'features/controlLayers/store/paramsSlice';
import { selectImg2imgStrengthConfig } from 'features/system/store/configSlice';
import { clamp } from 'lodash-es';
import type { KeyboardEvent } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretDownBold } from 'react-icons/pi';
const marks = [0, 0.25, 0.5, 0.75, 1];
export const EntityListGlobalActionBarDenoisingStrength = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const strength = useAppSelector(selectImg2imgStrength);
const config = useAppSelector(selectImg2imgStrengthConfig);
const [localStrength, setLocalStrength] = useState(strength);
const onChangeSlider = useCallback(
(value: number) => {
dispatch(setImg2imgStrength(value));
},
[dispatch]
);
const onBlur = useCallback(() => {
if (isNaN(Number(localStrength))) {
setLocalStrength(config.initial);
return;
}
dispatch(setImg2imgStrength(clamp(localStrength, 0, 1)));
}, [config.initial, dispatch, localStrength]);
const onChangeNumberInput = useCallback((valueAsString: string, valueAsNumber: number) => {
setLocalStrength(valueAsNumber);
}, []);
const onKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
onBlur();
}
},
[onBlur]
);
useEffect(() => {
setLocalStrength(strength);
}, [strength]);
return (
<Popover>
<FormControl w="min-content" gap={2}>
<InformationalPopover feature="paramDenoisingStrength">
<FormLabel m={0}>{`${t('parameters.denoisingStrength')}`}</FormLabel>
</InformationalPopover>
<PopoverAnchor>
<NumberInput
display="flex"
alignItems="center"
step={config.coarseStep}
min={config.numberInputMin}
max={config.numberInputMax}
defaultValue={config.initial}
value={localStrength}
onChange={onChangeNumberInput}
onBlur={onBlur}
w="60px"
onKeyDown={onKeyDown}
clampValueOnBlur={false}
variant="outline"
>
<NumberInputField paddingInlineEnd={7} _focusVisible={{ zIndex: 0 }} />
<PopoverTrigger>
<IconButton
aria-label="open-slider"
icon={<PiCaretDownBold />}
size="sm"
variant="link"
position="absolute"
insetInlineEnd={0}
h="full"
/>
</PopoverTrigger>
</NumberInput>
</PopoverAnchor>
</FormControl>
<PopoverContent w={200} pt={0} pb={2} px={4}>
<PopoverArrow />
<PopoverBody>
<CompositeSlider
step={config.coarseStep}
fineStep={config.fineStep}
min={config.sliderMin}
max={config.sliderMax}
defaultValue={config.initial}
onChange={onChangeSlider}
value={localStrength}
marks={marks}
alwaysShowMarks
/>
</PopoverBody>
</PopoverContent>
</Popover>
);
});
EntityListGlobalActionBarDenoisingStrength.displayName = 'EntityListGlobalActionBarDenoisingStrength';

View File

@@ -0,0 +1,27 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowsOut } from 'react-icons/pi';
export const EntityListGlobalActionBarFitBboxToLayers = memo(() => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const onClick = useCallback(() => {
canvasManager.bbox.fitToLayers();
}, [canvasManager.bbox]);
return (
<IconButton
onClick={onClick}
size="sm"
variant="link"
alignSelf="stretch"
aria-label={t('controlLayers.fitBboxToLayers')}
tooltip={t('controlLayers.fitBboxToLayers')}
icon={<PiArrowsOut />}
/>
);
});
EntityListGlobalActionBarFitBboxToLayers.displayName = 'EntityListGlobalActionBarFitBboxToLayers';

View File

@@ -0,0 +1,24 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { EntityListSelectedEntityActionBarDuplicateButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton';
import { EntityListSelectedEntityActionBarFill } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFill';
import { EntityListSelectedEntityActionBarFilterButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton';
import { EntityListSelectedEntityActionBarOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity';
import { EntityListSelectedEntityActionBarTransformButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton';
import { memo } from 'react';
export const EntityListSelectedEntityActionBar = memo(() => {
return (
<Flex w="full" py={1} px={1} gap={2} alignItems="center">
<EntityListSelectedEntityActionBarOpacity />
<Spacer />
<EntityListSelectedEntityActionBarFill />
<Flex>
<EntityListSelectedEntityActionBarFilterButton />
<EntityListSelectedEntityActionBarTransformButton />
<EntityListSelectedEntityActionBarDuplicateButton />
</Flex>
</Flex>
);
});
EntityListSelectedEntityActionBar.displayName = 'EntityListSelectedEntityActionBar';

View File

@@ -0,0 +1,34 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { entityDuplicated } from 'features/controlLayers/store/canvasSlice';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCopyFill } from 'react-icons/pi';
export const EntityListSelectedEntityActionBarDuplicateButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const onClick = useCallback(() => {
if (!selectedEntityIdentifier) {
return;
}
dispatch(entityDuplicated({ entityIdentifier: selectedEntityIdentifier }));
}, [dispatch, selectedEntityIdentifier]);
return (
<IconButton
onClick={onClick}
isDisabled={!selectedEntityIdentifier}
size="sm"
variant="link"
alignSelf="stretch"
aria-label={t('controlLayers.duplicate')}
tooltip={t('controlLayers.duplicate')}
icon={<PiCopyFill />}
/>
);
});
EntityListSelectedEntityActionBarDuplicateButton.displayName = 'EntityListSelectedEntityActionBarDuplicateButton';

View File

@@ -9,7 +9,7 @@ import { type FillStyle, isMaskEntityIdentifier, type RgbColor } from 'features/
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const EntityListActionBarSelectedEntityFill = memo(() => {
export const EntityListSelectedEntityActionBarFill = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
@@ -67,4 +67,4 @@ export const EntityListActionBarSelectedEntityFill = memo(() => {
);
});
EntityListActionBarSelectedEntityFill.displayName = 'EntityListActionBarSelectedEntityFill';
EntityListSelectedEntityActionBarFill.displayName = 'EntityListSelectedEntityActionBarFill';

View File

@@ -0,0 +1,52 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { isFilterableEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiShootingStarBold } from 'react-icons/pi';
export const EntityListSelectedEntityActionBarFilterButton = memo(() => {
const { t } = useTranslation();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const canvasManager = useCanvasManager();
const isStaging = useAppSelector(selectIsStaging);
const isBusy = useCanvasIsBusy();
const onClick = useCallback(() => {
if (!selectedEntityIdentifier) {
return;
}
if (!isFilterableEntityIdentifier(selectedEntityIdentifier)) {
return;
}
canvasManager.filter.startFilter(selectedEntityIdentifier);
}, [canvasManager, selectedEntityIdentifier]);
if (!selectedEntityIdentifier) {
return null;
}
if (!isFilterableEntityIdentifier(selectedEntityIdentifier)) {
return null;
}
return (
<IconButton
onClick={onClick}
isDisabled={isBusy || isStaging}
size="sm"
variant="link"
alignSelf="stretch"
aria-label={t('controlLayers.filter.filter')}
tooltip={t('controlLayers.filter.filter')}
icon={<PiShootingStarBold />}
/>
);
});
EntityListSelectedEntityActionBarFilterButton.displayName = 'EntityListSelectedEntityActionBarFilterButton';

View File

@@ -77,7 +77,7 @@ const selectOpacity = createSelector(selectCanvasSlice, (canvas) => {
return selectedEntity.opacity;
});
export const SelectedEntityOpacity = memo(() => {
export const EntityListSelectedEntityActionBarOpacity = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
@@ -193,4 +193,4 @@ export const SelectedEntityOpacity = memo(() => {
);
});
SelectedEntityOpacity.displayName = 'SelectedEntityOpacity';
EntityListSelectedEntityActionBarOpacity.displayName = 'EntityListSelectedEntityActionBarOpacity';

View File

@@ -0,0 +1,55 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { isTransformableEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFrameCornersBold } from 'react-icons/pi';
export const EntityListSelectedEntityActionBarTransformButton = memo(() => {
const { t } = useTranslation();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const canvasManager = useCanvasManager();
const isStaging = useAppSelector(selectIsStaging);
const isBusy = useCanvasIsBusy();
const onClick = useCallback(() => {
if (!selectedEntityIdentifier) {
return;
}
if (!isTransformableEntityIdentifier(selectedEntityIdentifier)) {
return;
}
const adapter = canvasManager.getAdapter(selectedEntityIdentifier);
if (!adapter) {
return;
}
adapter.transformer.startTransform();
}, [canvasManager, selectedEntityIdentifier]);
if (!selectedEntityIdentifier) {
return null;
}
if (!isTransformableEntityIdentifier(selectedEntityIdentifier)) {
return null;
}
return (
<IconButton
onClick={onClick}
isDisabled={isBusy || isStaging}
size="sm"
variant="link"
alignSelf="stretch"
aria-label={t('controlLayers.transform.transform')}
tooltip={t('controlLayers.transform.transform')}
icon={<PiFrameCornersBold />}
/>
);
});
EntityListSelectedEntityActionBarTransformButton.displayName = 'EntityListSelectedEntityActionBarTransformButton';

View File

@@ -2,7 +2,8 @@ import { Divider, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
import { EntityListActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBar';
import { EntityListGlobalActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBar';
import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectHasEntities } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
@@ -13,7 +14,9 @@ export const CanvasPanelContent = memo(() => {
return (
<CanvasManagerProviderGate>
<Flex flexDir="column" gap={2} w="full" h="full">
<EntityListActionBar />
<EntityListGlobalActionBar />
<Divider py={0} />
<EntityListSelectedEntityActionBar />
<Divider py={0} />
{!hasEntities && <CanvasAddEntityButtons />}
{hasEntities && <CanvasEntityList />}

View File

@@ -1,145 +0,0 @@
import { Flex, Grid, GridItem, IconButton } from '@invoke-ai/ui-library';
import { memo, useCallback, useState } from 'react';
import {
PiArrowDownBold,
PiArrowDownLeftBold,
PiArrowDownRightBold,
PiArrowLeftBold,
PiArrowRightBold,
PiArrowUpBold,
PiArrowUpLeftBold,
PiArrowUpRightBold,
PiSquareBold,
} from 'react-icons/pi';
type ResizeDirection =
| 'up-left'
| 'up'
| 'up-right'
| 'left'
| 'center-out'
| 'right'
| 'down-left'
| 'down'
| 'down-right';
export const CanvasResizer = memo(() => {
const [resizeDirection, setResizeDirection] = useState<ResizeDirection>('center-out');
const setDirUpLeft = useCallback(() => {
setResizeDirection('up-left');
}, []);
const setDirUp = useCallback(() => {
setResizeDirection('up');
}, []);
const setDirUpRight = useCallback(() => {
setResizeDirection('up-right');
}, []);
const setDirLeft = useCallback(() => {
setResizeDirection('left');
}, []);
const setDirCenterOut = useCallback(() => {
setResizeDirection('center-out');
}, []);
const setDirRight = useCallback(() => {
setResizeDirection('right');
}, []);
const setDirDownLeft = useCallback(() => {
setResizeDirection('down-left');
}, []);
const setDirDown = useCallback(() => {
setResizeDirection('down');
}, []);
const setDirDownRight = useCallback(() => {
setResizeDirection('down-right');
}, []);
return (
<Flex p={2}>
<Grid gridTemplateRows="1fr 1fr 1fr" gridTemplateColumns="1fr 1fr 1fr" gap={2}>
<GridItem>
<IconButton
onClick={setDirUpLeft}
aria-label="up-left"
icon={<PiArrowUpLeftBold />}
variant={resizeDirection === 'up-left' ? 'solid' : 'ghost'}
/>
</GridItem>
<GridItem>
<IconButton
onClick={setDirUp}
aria-label="up"
icon={<PiArrowUpBold />}
variant={resizeDirection === 'up' ? 'solid' : 'ghost'}
/>
</GridItem>
<GridItem>
<IconButton
onClick={setDirUpRight}
aria-label="up-right"
icon={<PiArrowUpRightBold />}
variant={resizeDirection === 'up-right' ? 'solid' : 'ghost'}
/>
</GridItem>
<GridItem>
<IconButton
onClick={setDirLeft}
aria-label="left"
icon={<PiArrowLeftBold />}
variant={resizeDirection === 'left' ? 'solid' : 'ghost'}
/>
</GridItem>
<GridItem>
<IconButton
onClick={setDirCenterOut}
aria-label="center-out"
icon={<PiSquareBold />}
variant={resizeDirection === 'center-out' ? 'solid' : 'ghost'}
/>
</GridItem>
<GridItem>
<IconButton
onClick={setDirRight}
aria-label="right"
icon={<PiArrowRightBold />}
variant={resizeDirection === 'right' ? 'solid' : 'ghost'}
/>
</GridItem>
<GridItem>
<IconButton
onClick={setDirDownLeft}
aria-label="down-left"
icon={<PiArrowDownLeftBold />}
variant={resizeDirection === 'down-left' ? 'solid' : 'ghost'}
/>
</GridItem>
<GridItem>
<IconButton
onClick={setDirDown}
aria-label="down"
icon={<PiArrowDownBold />}
variant={resizeDirection === 'down' ? 'solid' : 'ghost'}
/>
</GridItem>
<GridItem>
<IconButton
onClick={setDirDownRight}
aria-label="down-right"
icon={<PiArrowDownRightBold />}
variant={resizeDirection === 'down-right' ? 'solid' : 'ghost'}
/>
</GridItem>
</Grid>
</Flex>
);
});
CanvasResizer.displayName = 'CanvasResizer';

View File

@@ -4,7 +4,12 @@ import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectBase } from 'features/controlLayers/store/paramsSlice';
import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types';
import {
IMAGE_FILTERS,
isControlLayerEntityIdentifier,
isFilterType,
isRasterLayerEntityIdentifier,
} from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useControlNetAndT2IAdapterModels } from 'services/api/hooks/modelsByType';
@@ -39,6 +44,11 @@ export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onCha
// Open the filter popup by setting this entity as the filtering entity
if (!canvasManager.filter.$adapter.get()) {
// Can only filter raster and control layers
if (!isRasterLayerEntityIdentifier(entityIdentifier) && !isControlLayerEntityIdentifier(entityIdentifier)) {
return;
}
// Update the filter, preferring the model's default
if (isFilterType(modelConfig.default_settings?.preprocessor)) {
canvasManager.filter.$config.set(
@@ -47,6 +57,7 @@ export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onCha
} else {
canvasManager.filter.$config.set(IMAGE_FILTERS.canny_image_processor.buildDefaults(modelConfig.base));
}
canvasManager.filter.startFilter(entityIdentifier);
canvasManager.filter.previewFilter();
}

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 { FilterSettings } from 'features/controlLayers/components/Filters/FilterSettings';
import { FilterTypeSelect } from 'features/controlLayers/components/Filters/FilterTypeSelect';
@@ -51,7 +51,7 @@ export const Filter = memo(() => {
</Heading>
<FilterTypeSelect filterType={config.type} onChange={onChangeFilterType} />
<FilterSettings filterConfig={config} onChange={onChangeFilterConfig} />
<ButtonGroup isAttached={false} size="sm" alignSelf="self-end">
<ButtonGroup isAttached={false} size="sm" w="full">
<Button
variant="ghost"
leftIcon={<PiShootingStarBold />}
@@ -61,6 +61,7 @@ export const Filter = memo(() => {
>
{t('controlLayers.filter.preview')}
</Button>
<Spacer />
<Button
variant="ghost"
leftIcon={<PiCheckBold />}

View File

@@ -9,7 +9,7 @@ import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/stor
import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
import type { ImageWithDims } from 'features/controlLayers/store/types';
import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
import { calculateNewSize } from 'features/parameters/components/Bbox/calculateNewSize';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi';

View File

@@ -1,5 +1,6 @@
import { Button } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { canvasReset } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -7,9 +8,11 @@ import { useTranslation } from 'react-i18next';
export const CanvasSettingsResetButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const canvasManager = useCanvasManager();
const onClick = useCallback(() => {
dispatch(canvasReset());
}, [dispatch]);
canvasManager.stage.fitLayersToStage();
}, [canvasManager.stage, dispatch]);
return (
<Button onClick={onClick} colorScheme="error" size="sm">
{t('controlLayers.resetCanvas')}

View File

@@ -27,7 +27,7 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props
return (
<Flex flexDir="column" w="full">
<Flex w="full">
<Flex w="full" px={1}>
<Flex
flexGrow={1}
as={Button}
@@ -59,8 +59,8 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props
<Spacer />
</Flex>
{canMergeVisible && <CanvasEntityMergeVisibleButton type={type} />}
<CanvasEntityAddOfTypeButton type={type} />
{canHideAll && <CanvasEntityTypeIsHiddenToggle type={type} />}
<CanvasEntityAddOfTypeButton type={type} />
</Flex>
<Collapse in={collapse.isTrue}>
<Flex flexDir="column" gap={2} pt={2}>

View File

@@ -1,7 +1,10 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
import { isFilterableEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiShootingStarBold } from 'react-icons/pi';
@@ -10,14 +13,21 @@ export const CanvasEntityMenuItemsFilter = memo(() => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const entityIdentifier = useEntityIdentifierContext();
const isStaging = useAppSelector(selectIsStaging);
const isBusy = useCanvasIsBusy();
const onClick = useCallback(() => {
if (!entityIdentifier) {
return;
}
if (!isFilterableEntityIdentifier(entityIdentifier)) {
return;
}
canvasManager.filter.startFilter(entityIdentifier);
}, [canvasManager.filter, entityIdentifier]);
return (
<MenuItem onClick={onClick} icon={<PiShootingStarBold />} isDisabled={isBusy}>
<MenuItem onClick={onClick} icon={<PiShootingStarBold />} isDisabled={isBusy || isStaging}>
{t('controlLayers.filter.filter')}
</MenuItem>
);

View File

@@ -1,7 +1,10 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityAdapter } from 'features/controlLayers/hooks/useEntityAdapter';
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
import { isTransformableEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFrameCornersBold } from 'react-icons/pi';
@@ -10,14 +13,18 @@ export const CanvasEntityMenuItemsTransform = memo(() => {
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext();
const adapter = useEntityAdapter(entityIdentifier);
const isStaging = useAppSelector(selectIsStaging);
const isBusy = useCanvasIsBusy();
const onClick = useCallback(() => {
if (!isTransformableEntityIdentifier(entityIdentifier)) {
return;
}
adapter.transformer.startTransform();
}, [adapter.transformer]);
}, [adapter.transformer, entityIdentifier]);
return (
<MenuItem onClick={onClick} icon={<PiFrameCornersBold />} isDisabled={isBusy}>
<MenuItem onClick={onClick} icon={<PiFrameCornersBold />} isDisabled={isBusy || isStaging}>
{t('controlLayers.transform.transform')}
</MenuItem>
);

View File

@@ -1,9 +1,11 @@
import { IconButton } from '@invoke-ai/ui-library';
import { logger } from 'app/logging/logger';
import { useAppDispatch } from 'app/store/storeHooks';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isOk, withResultAsync } from 'common/util/result';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityTypeCount } from 'features/controlLayers/hooks/useEntityTypeCount';
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
import { inpaintMaskAdded, rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
@@ -23,6 +25,8 @@ export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const canvasManager = useCanvasManager();
const isStaging = useAppSelector(selectIsStaging);
const isBusy = useCanvasIsBusy();
const entityCount = useEntityTypeCount(type);
const onClick = useCallback(async () => {
if (type === 'raster_layer') {
@@ -83,7 +87,7 @@ export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
icon={<PiStackBold />}
onClick={onClick}
alignSelf="stretch"
isDisabled={entityCount <= 1}
isDisabled={entityCount <= 1 || isStaging || isBusy}
/>
);
});

View File

@@ -17,7 +17,7 @@ export const CanvasEntityTypeIsHiddenToggle = memo(({ type }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isHidden = useEntityTypeIsHidden(type);
const typeString = useEntityTypeString(type);
const typeString = useEntityTypeString(type, true);
const onClick = useCallback<MouseEventHandler>(
(e) => {
e.stopPropagation();

View File

@@ -18,8 +18,20 @@ export const CanvasManagerProviderGate = memo(({ children }: PropsWithChildren)
CanvasManagerProviderGate.displayName = 'CanvasManagerProviderGate';
/**
* Consumes the CanvasManager from the context. This hook must be used within the CanvasManagerProviderGate, otherwise
* it will throw an error.
*/
export const useCanvasManager = (): CanvasManager => {
const canvasManager = useContext(CanvasManagerContext);
assert(canvasManager, 'useCanvasManagerContext must be used within a CanvasManagerProviderGate');
return canvasManager;
};
/**
* Consumes the CanvasManager from the context. If the CanvasManager is not available, it will return null.
*/
export const useCanvasManagerSafe = (): CanvasManager | null => {
const canvasManager = useStore($canvasManager);
return canvasManager;
};

View File

@@ -2,25 +2,25 @@ import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export const useEntityTypeString = (type: CanvasEntityIdentifier['type']): string => {
export const useEntityTypeString = (type: CanvasEntityIdentifier['type'], plural: boolean = false): string => {
const { t } = useTranslation();
const typeString = useMemo(() => {
switch (type) {
case 'control_layer':
return t('controlLayers.controlLayer');
return plural ? t('controlLayers.controlLayer_withCount_other') : t('controlLayers.controlLayer');
case 'raster_layer':
return t('controlLayers.rasterLayer');
return plural ? t('controlLayers.rasterLayer_withCount_other') : t('controlLayers.rasterLayer');
case 'inpaint_mask':
return t('controlLayers.inpaintMask');
return plural ? t('controlLayers.inpaintMask_withCount_other') : t('controlLayers.inpaintMask');
case 'regional_guidance':
return t('controlLayers.regionalGuidance');
return plural ? t('controlLayers.regionalGuidance_withCount_other') : t('controlLayers.regionalGuidance');
case 'ip_adapter':
return t('controlLayers.globalIPAdapter');
return plural ? t('controlLayers.globalIPAdapter_withCount_other') : t('controlLayers.globalIPAdapter');
default:
return '';
}
}, [type, t]);
}, [type, plural, t]);
return typeString;
};

View File

@@ -1,4 +1,9 @@
import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple';
import {
roundDownToMultiple,
roundToMultiple,
roundToMultipleMin,
roundUpToMultiple,
} from 'common/util/roundDownToMultiple';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import { getPrefixedId } from 'features/controlLayers/konva/util';
@@ -295,6 +300,29 @@ export class CanvasBboxModule extends CanvasModuleBase {
}
};
fitToLayers = (): void => {
const visibleRect = this.manager.stage.getVisibleRect();
// Can't fit the bbox to nothing
if (visibleRect.height === 0 || visibleRect.width === 0) {
return;
}
// Determine the bbox size that fits within the visible rect. The bbox must be at least 64px in width and height,
// and its width and height must be multiples of 8px.
const gridSize = 8;
// To be conservative, we will round up the x and y to the nearest grid size, and round down the width and height.
// This ensures the bbox is never _larger_ than the visible rect. If the bbox is larger than the visible, we
// will always trigger the outpainting workflow, which is not what the user wants.
const x = roundUpToMultiple(visibleRect.x, gridSize);
const y = roundUpToMultiple(visibleRect.y, gridSize);
const width = roundDownToMultiple(visibleRect.width, gridSize);
const height = roundDownToMultiple(visibleRect.height, gridSize);
this.manager.stateApi.setGenerationBbox({ x, y, width, height });
};
/**
* This function is called for each anchor on the transformer. It sets the drag bounds for the anchor based on the
* stage's position and the grid size. Care is taken to ensure the anchor snaps to the grid correctly.

View File

@@ -16,8 +16,8 @@ export class CanvasEntityAdapterControlLayer extends CanvasEntityAdapterBase<Can
constructor(entityIdentifier: CanvasEntityIdentifier<'control_layer'>, manager: CanvasManager) {
super(entityIdentifier, manager, CanvasEntityAdapterControlLayer.TYPE);
this.transformer = new CanvasEntityTransformer(this);
this.renderer = new CanvasEntityObjectRenderer(this);
this.transformer = new CanvasEntityTransformer(this);
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectState, this.sync));
}
@@ -63,7 +63,7 @@ export class CanvasEntityAdapterControlLayer extends CanvasEntityAdapterBase<Can
// The opacity may have been changed in response to user selecting a different entity category, so we must restore
// the original opacity before rendering the canvas
const attrs: GroupConfig = { opacity: this.state.opacity };
const canvas = this.renderer.getCanvas(rect, attrs);
const canvas = this.renderer.getCanvas({ rect, attrs });
return canvas;
};

View File

@@ -16,8 +16,8 @@ export class CanvasEntityAdapterInpaintMask extends CanvasEntityAdapterBase<Canv
constructor(entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>, manager: CanvasManager) {
super(entityIdentifier, manager, CanvasEntityAdapterInpaintMask.TYPE);
this.transformer = new CanvasEntityTransformer(this);
this.renderer = new CanvasEntityObjectRenderer(this);
this.transformer = new CanvasEntityTransformer(this);
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectState, this.sync));
}
@@ -74,7 +74,7 @@ export class CanvasEntityAdapterInpaintMask extends CanvasEntityAdapterBase<Canv
// The opacity may have been changed in response to user selecting a different entity category, and the mask regions
// should be fully opaque - set opacity to 1 before rendering the canvas
const attrs: GroupConfig = { opacity: 1 };
const canvas = this.renderer.getCanvas(rect, attrs);
const canvas = this.renderer.getCanvas({ rect, attrs });
return canvas;
};
}

View File

@@ -16,8 +16,8 @@ export class CanvasEntityAdapterRasterLayer extends CanvasEntityAdapterBase<Canv
constructor(entityIdentifier: CanvasEntityIdentifier<'raster_layer'>, manager: CanvasManager) {
super(entityIdentifier, manager, CanvasEntityAdapterRasterLayer.TYPE);
this.transformer = new CanvasEntityTransformer(this);
this.renderer = new CanvasEntityObjectRenderer(this);
this.transformer = new CanvasEntityTransformer(this);
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectState, this.sync));
}
@@ -56,7 +56,7 @@ export class CanvasEntityAdapterRasterLayer extends CanvasEntityAdapterBase<Canv
// The opacity may have been changed in response to user selecting a different entity category, so we must restore
// the original opacity before rendering the canvas
const attrs: GroupConfig = { opacity: this.state.opacity };
const canvas = this.renderer.getCanvas(rect, attrs);
const canvas = this.renderer.getCanvas({ rect, attrs });
return canvas;
};

View File

@@ -16,8 +16,8 @@ export class CanvasEntityAdapterRegionalGuidance extends CanvasEntityAdapterBase
constructor(entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>, manager: CanvasManager) {
super(entityIdentifier, manager, CanvasEntityAdapterRegionalGuidance.TYPE);
this.transformer = new CanvasEntityTransformer(this);
this.renderer = new CanvasEntityObjectRenderer(this);
this.transformer = new CanvasEntityTransformer(this);
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectState, this.sync));
}
@@ -74,7 +74,7 @@ export class CanvasEntityAdapterRegionalGuidance extends CanvasEntityAdapterBase
// The opacity may have been changed in response to user selecting a different entity category, and the mask regions
// should be fully opaque - set opacity to 1 before rendering the canvas
const attrs: GroupConfig = { opacity: 1 };
const canvas = this.renderer.getCanvas(rect, attrs);
const canvas = this.renderer.getCanvas({ rect, attrs });
return canvas;
};
}

View File

@@ -172,7 +172,7 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
// to pan _before_ releasing the mouse button, which would cause the buffer to be lost if we didn't commit it.
this.subscriptions.add(
this.manager.tool.$tool.listen(() => {
if (this.hasBuffer()) {
if (this.hasBuffer() && !this.manager.$isBusy.get()) {
this.commitBuffer();
}
})
@@ -447,6 +447,7 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
this.bufferRenderer?.destroy();
this.bufferRenderer = null;
this.bufferState = null;
this.syncCache(true);
}
};
@@ -538,10 +539,16 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
* @param rect The rect to rasterize. If omitted, the entity's full rect will be used.
* @returns A promise that resolves to the rasterized image DTO.
*/
rasterize = async (options: { rect: Rect; replaceObjects?: boolean; attrs?: GroupConfig }): Promise<ImageDTO> => {
const { rect, replaceObjects, attrs } = { replaceObjects: false, attrs: {}, ...options };
rasterize = async (options: {
rect: Rect;
replaceObjects?: boolean;
attrs?: GroupConfig;
bg?: string;
}): Promise<ImageDTO> => {
const { rect, replaceObjects, attrs, bg } = { replaceObjects: false, attrs: {}, ...options };
let imageDTO: ImageDTO | null = null;
const hash = this.parent.hash({ rect, attrs });
const rasterizeArgs = { rect, attrs, bg };
const hash = this.parent.hash(rasterizeArgs);
const cachedImageName = this.manager.cache.imageNameCache.get(hash);
if (cachedImageName) {
@@ -552,9 +559,9 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
}
}
this.log.trace({ rect }, 'Rasterizing entity');
this.log.trace({ rasterizeArgs }, 'Rasterizing entity');
const blob = await this.getBlob(rect, attrs);
const blob = await this.getBlob(rasterizeArgs);
if (this.manager._isDebugging) {
previewBlob(blob, 'Rasterized entity');
}
@@ -607,32 +614,35 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
}
}, 300);
cloneObjectGroup = (attrs?: GroupConfig): Konva.Group => {
cloneObjectGroup = (arg: { attrs?: GroupConfig } = {}): Konva.Group => {
const { attrs } = arg;
const clone = this.konva.objectGroup.clone();
clone.cache();
if (attrs) {
clone.setAttrs(attrs);
}
clone.cache();
return clone;
};
getCanvas = (rect?: Rect, attrs?: GroupConfig): HTMLCanvasElement => {
const clone = this.cloneObjectGroup(attrs);
const canvas = konvaNodeToCanvas(clone, rect);
getCanvas = (arg: { rect?: Rect; attrs?: GroupConfig; bg?: string } = {}): HTMLCanvasElement => {
const { rect, attrs, bg } = arg;
const clone = this.cloneObjectGroup({ attrs });
const canvas = konvaNodeToCanvas({ node: clone, rect, bg });
clone.destroy();
return canvas;
};
getBlob = async (rect?: Rect, attrs?: GroupConfig): Promise<Blob> => {
const clone = this.cloneObjectGroup(attrs);
const blob = await konvaNodeToBlob(clone, rect);
clone.destroy();
getBlob = async (arg: { rect?: Rect; attrs?: GroupConfig; bg?: string } = {}): Promise<Blob> => {
const { rect, attrs, bg } = arg;
const clone = this.cloneObjectGroup({ attrs });
const blob = await konvaNodeToBlob({ node: clone, rect, bg });
return blob;
};
getImageData = (rect?: Rect, attrs?: GroupConfig): ImageData => {
const clone = this.cloneObjectGroup(attrs);
const imageData = konvaNodeToImageData(clone, rect);
getImageData = (arg: { rect?: Rect; attrs?: GroupConfig; bg?: string } = {}): ImageData => {
const { rect, attrs, bg } = arg;
const clone = this.cloneObjectGroup({ attrs });
const imageData = konvaNodeToImageData({ node: clone, rect, bg });
clone.destroy();
return imageData;
};

View File

@@ -172,8 +172,9 @@ export class CanvasEntityTransformer extends CanvasModuleBase {
rotateEnabled: true,
// When dragging a transform anchor across either the x or y axis, the nodes will be flipped across the axis
flipEnabled: true,
// Transforming will retain aspect ratio only when shift is held
keepRatio: false,
// Transforming will allow free aspect ratio only when shift is held
keepRatio: true,
shiftBehavior: 'inverted',
// The padding is the distance between the transformer bbox and the nodes
padding: this.config.OUTLINE_PADDING,
// This is `invokeBlue.400`
@@ -705,7 +706,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase {
}
// We have eraser strokes - we must calculate the bbox using pixel data
const canvas = this.parent.renderer.getCanvas(undefined, { opacity: 1 });
const canvas = this.parent.renderer.getCanvas({ attrs: { opacity: 1, filters: [] } });
const imageData = canvasToImageData(canvas);
this.manager.worker.requestBbox(
{ buffer: imageData.data.buffer, width: imageData.width, height: imageData.height },

View File

@@ -46,7 +46,7 @@ export class CanvasFilterModule extends CanvasModuleBase {
this.log.debug('Creating filter module');
}
startFilter = (entityIdentifier: CanvasEntityIdentifier) => {
startFilter = (entityIdentifier: CanvasEntityIdentifier<'raster_layer' | 'control_layer'>) => {
this.log.trace('Initializing filter');
const adapter = this.manager.getAdapter(entityIdentifier);
if (!adapter) {
@@ -70,7 +70,7 @@ export class CanvasFilterModule extends CanvasModuleBase {
const config = this.$config.get();
this.log.trace({ config }, 'Previewing filter');
const rect = adapter.transformer.getRelativeRect();
const imageDTO = await adapter.renderer.rasterize({ rect });
const imageDTO = await adapter.renderer.rasterize({ rect, attrs: { filters: [] } });
const nodeId = getPrefixedId('filter_node');
const batch = this.buildBatchConfig(imageDTO, config, nodeId);
@@ -145,6 +145,7 @@ export class CanvasFilterModule extends CanvasModuleBase {
adapter.renderer.clearBuffer();
adapter.renderer.showObjects();
adapter.transformer.updatePosition();
adapter.renderer.syncCache(true);
this.$adapter.set(null);
}
this.imageState = null;
@@ -162,9 +163,8 @@ export class CanvasFilterModule extends CanvasModuleBase {
nodes: {
[node.id]: {
...node,
// Control images are always intermediate - do not save to gallery
// is_intermediate: true,
is_intermediate: false, // false for testing
// filtered images are always intermediate - do not save to gallery
is_intermediate: true,
},
},
edges: [],

View File

@@ -20,8 +20,8 @@ import { CanvasStagingAreaModule } from 'features/controlLayers/konva/CanvasStag
import { CanvasToolModule } from 'features/controlLayers/konva/CanvasToolModule';
import { CanvasWorkerModule } from 'features/controlLayers/konva/CanvasWorkerModule.js';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import type { CanvasEntityIdentifier, CanvasEntityType } from 'features/controlLayers/store/types';
import {
type CanvasEntityIdentifier,
isControlLayerEntityIdentifier,
isInpaintMaskEntityIdentifier,
isRasterLayerEntityIdentifier,
@@ -133,16 +133,38 @@ export class CanvasManager extends CanvasModuleBase {
});
}
getAdapter = (entityIdentifier: CanvasEntityIdentifier): CanvasEntityAdapter | null => {
getAdapter = <T extends CanvasEntityType = CanvasEntityType>(
entityIdentifier: CanvasEntityIdentifier<T>
): Extract<CanvasEntityAdapter, { state: { type: T } }> | null => {
switch (entityIdentifier.type) {
case 'raster_layer':
return this.adapters.rasterLayers.get(entityIdentifier.id) ?? null;
return (
(this.adapters.rasterLayers.get(entityIdentifier.id) as Extract<
CanvasEntityAdapter,
{ state: { type: T } }
>) ?? null
);
case 'control_layer':
return this.adapters.controlLayers.get(entityIdentifier.id) ?? null;
return (
(this.adapters.controlLayers.get(entityIdentifier.id) as Extract<
CanvasEntityAdapter,
{ state: { type: T } }
>) ?? null
);
case 'regional_guidance':
return this.adapters.regionMasks.get(entityIdentifier.id) ?? null;
return (
(this.adapters.regionMasks.get(entityIdentifier.id) as Extract<
CanvasEntityAdapter,
{ state: { type: T } }
>) ?? null
);
case 'inpaint_mask':
return this.adapters.inpaintMasks.get(entityIdentifier.id) ?? null;
return (
(this.adapters.inpaintMasks.get(entityIdentifier.id) as Extract<
CanvasEntityAdapter,
{ state: { type: T } }
>) ?? null
);
default:
return null;
}

View File

@@ -100,10 +100,26 @@ export class CanvasStageModule extends CanvasModuleBase {
});
};
fitStageToContainer = () => {
/**
* Fits the stage to the container element.
*/
fitStageToContainer = (): void => {
this.log.trace('Fitting stage to container');
this.konva.stage.width(this.konva.stage.container().offsetWidth);
this.konva.stage.height(this.konva.stage.container().offsetHeight);
const containerWidth = this.konva.stage.container().offsetWidth;
const containerHeight = this.konva.stage.container().offsetHeight;
// If the container has no size, the following calculations will be reaallll funky and bork the stage
if (containerWidth === 0 || containerHeight === 0) {
return;
}
// If the stage _had_ no size just before this function was called, that means we've just mounted the stage or
// maybe un-hidden it. In that case, the user is about to see the stage for the first time, so we should fit the
// layers to the stage. If we don't do this, the layers will not be centered.
const shouldFitLayersAfterFittingStage = this.konva.stage.width() === 0 || this.konva.stage.height() === 0;
this.konva.stage.width(containerWidth);
this.konva.stage.height(containerHeight);
this.$stageAttrs.set({
x: this.konva.stage.x(),
y: this.konva.stage.y(),
@@ -111,6 +127,10 @@ export class CanvasStageModule extends CanvasModuleBase {
height: this.konva.stage.height(),
scale: this.konva.stage.scaleX(),
});
if (shouldFitLayersAfterFittingStage) {
this.fitLayersToStage();
}
};
getVisibleRect = (type?: Exclude<CanvasEntityIdentifier['type'], 'ip_adapter'>): Rect => {
@@ -129,13 +149,19 @@ export class CanvasStageModule extends CanvasModuleBase {
return getRectUnion(...rects);
};
fitBboxToStage = () => {
/**
* Fits the bbox to the stage. This will center the bbox and scale it to fit the stage with some padding.
*/
fitBboxToStage = (): void => {
const { rect } = this.manager.stateApi.getBbox();
this.log.trace({ rect }, 'Fitting bbox to stage');
this.fitRect(rect);
};
fitLayersToStage() {
/**
* Fits the visible canvas to the stage. This will center the canvas and scale it to fit the stage with some padding.
*/
fitLayersToStage = (): void => {
const rect = this.getVisibleRect();
if (rect.width === 0 || rect.height === 0) {
this.fitBboxToStage();
@@ -143,17 +169,31 @@ export class CanvasStageModule extends CanvasModuleBase {
this.log.trace({ rect }, 'Fitting layers to stage');
this.fitRect(rect);
}
}
};
fitRect = (rect: Rect) => {
/**
* Fits a rectangle to the stage. The rectangle will be centered and scaled to fit the stage with some padding.
*
* The max scale is 1, but the stage can be scaled down to fit the rect.
*/
fitRect = (rect: Rect): void => {
const { width, height } = this.getSize();
// If the stage has no size, we can't fit anything to it
if (width === 0 || height === 0) {
return;
}
const padding = 20; // Padding in absolute pixels
const availableWidth = width - padding * 2;
const availableHeight = height - padding * 2;
const scale = Math.min(Math.min(availableWidth / rect.width, availableHeight / rect.height), 1);
// Make sure we don't accidentally set the scale to something nonsensical, like a negative number, 0 or something
// outside the valid range
const scale = this.constrainScale(
Math.min(Math.min(availableWidth / rect.width, availableHeight / rect.height), 1)
);
const x = -rect.x * scale + padding + (availableWidth - rect.width * scale) / 2;
const y = -rect.y * scale + padding + (availableHeight - rect.height * scale) / 2;
@@ -193,14 +233,21 @@ export class CanvasStageModule extends CanvasModuleBase {
return this.konva.stage.getAbsoluteTransform().point(center);
};
/**
* Constrains a scale to be within the valid range
*/
constrainScale = (scale: number): number => {
return clamp(Math.round(scale * 100) / 100, this.config.MIN_SCALE, this.config.MAX_SCALE);
};
/**
* Sets the scale of the stage. If center is provided, the stage will zoom in/out on that point.
* @param scale The new scale to set
* @param center The center of the stage to zoom in/out on
*/
setScale = (scale: number, center: Coordinate = this.getCenter(true)) => {
setScale = (scale: number, center: Coordinate = this.getCenter(true)): void => {
this.log.trace('Setting scale');
const newScale = clamp(Math.round(scale * 100) / 100, this.config.MIN_SCALE, this.config.MAX_SCALE);
const newScale = this.constrainScale(scale);
const { x, y } = this.getPosition();
const oldScale = this.getScale();

View File

@@ -636,7 +636,7 @@ export class CanvasToolModule extends CanvasModuleBase {
this.$isMouseDown.set(false);
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
if (selectedEntity && selectedEntity.renderer.hasBuffer()) {
if (selectedEntity && selectedEntity.renderer.hasBuffer() && !this.manager.$isBusy.get()) {
selectedEntity.renderer.commitBuffer();
}
};

View File

@@ -14,8 +14,9 @@ export const LightnessToAlphaFilter = (imageData: ImageData): void => {
const r = imageData.data[i * 4 + 0] as number;
const g = imageData.data[i * 4 + 1] as number;
const b = imageData.data[i * 4 + 2] as number;
const a = imageData.data[i * 4 + 3] as number;
const cMin = Math.min(r, g, b);
const cMax = Math.max(r, g, b);
imageData.data[i * 4 + 3] = (cMin + cMax) / 2;
imageData.data[i * 4 + 3] = Math.min(a, (cMin + cMax) / 2);
}
};

View File

@@ -304,8 +304,24 @@ export const dataURLToImageData = (dataURL: string, width: number, height: numbe
});
};
export const konvaNodeToCanvas = (node: Konva.Node, bbox?: Rect): HTMLCanvasElement => {
return node.toCanvas({ ...(bbox ?? {}) });
export const konvaNodeToCanvas = (arg: { node: Konva.Node; rect?: Rect; bg?: string }): HTMLCanvasElement => {
const { node, rect, bg } = arg;
const canvas = node.toCanvas({ ...(rect ?? {}) });
if (!bg) {
return canvas;
}
// We need to draw the canvas onto a new canvas with the specified background color
const bgCanvas = document.createElement('canvas');
bgCanvas.width = canvas.width;
bgCanvas.height = canvas.height;
const bgCtx = bgCanvas.getContext('2d');
assert(bgCtx !== null, 'bgCtx is null');
bgCtx.fillStyle = bg;
bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height);
bgCtx.drawImage(canvas, 0, 0);
return bgCanvas;
};
/**
@@ -335,22 +351,24 @@ export const canvasToImageData = (canvas: HTMLCanvasElement): ImageData => {
/**
* Converts a Konva node to an ImageData object
* @param node - The Konva node to convert to an ImageData object
* @param bbox - The bounding box to crop to
* @param rect - The bounding box to crop to
* @returns A Promise that resolves with ImageData object of the node cropped to the bounding box
*/
export const konvaNodeToImageData = (node: Konva.Node, bbox?: Rect): ImageData => {
const canvas = konvaNodeToCanvas(node, bbox);
export const konvaNodeToImageData = (arg: { node: Konva.Node; rect?: Rect; bg?: string }): ImageData => {
const { node, rect, bg } = arg;
const canvas = konvaNodeToCanvas({ node, rect, bg });
return canvasToImageData(canvas);
};
/**
* Converts a Konva node to a Blob
* @param node - The Konva node to convert to a Blob
* @param bbox - The bounding box to crop to
* @param rect - The bounding box to crop to
* @returns A Promise that resolves to the Blob or null,
*/
export const konvaNodeToBlob = (node: Konva.Node, bbox?: Rect): Promise<Blob> => {
const canvas = konvaNodeToCanvas(node, bbox);
export const konvaNodeToBlob = (arg: { node: Konva.Node; rect?: Rect; bg?: string }): Promise<Blob> => {
const { node, rect, bg } = arg;
const canvas = konvaNodeToCanvas({ node, rect, bg });
return canvasToBlob(canvas);
};

View File

@@ -1,6 +1,7 @@
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 { deepClone } from 'common/util/deepClone';
import { canvasReset, canvasSlice } from 'features/controlLayers/store/canvasSlice';
import type { StagingAreaImage } from 'features/controlLayers/store/types';
type CanvasSessionState = {
@@ -50,6 +51,9 @@ export const canvasSessionSlice = createSlice({
state.selectedStagedImageIndex = 0;
},
},
extraReducers(builder) {
builder.addCase(canvasReset, () => deepClone(initialState));
},
});
export const {

View File

@@ -21,9 +21,9 @@ import type {
import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
import { simplifyFlatNumbersArray } from 'features/controlLayers/util/simplify';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants';
import type { AspectRatioID } from 'features/parameters/components/DocumentSize/types';
import { calculateNewSize } from 'features/parameters/components/Bbox/calculateNewSize';
import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/Bbox/constants';
import type { AspectRatioID } from 'features/parameters/components/Bbox/types';
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
import type { IRect } from 'konva/lib/types';
import { isEqual, merge, omit } from 'lodash-es';
@@ -1078,16 +1078,18 @@ export const canvasSlice = createSlice({
state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier);
},
canvasReset: (state) => {
// We need to keep the bbox state that is dependent on the model/optimal dimension
const { width, height } = state.bbox.rect;
const optimalDimension = state.bbox.optimalDimension;
const scaledSize = getScaledBoundingBoxDimensions({ width, height }, optimalDimension);
const newState = deepClone(initialState);
const rect = calculateNewSize(newState.bbox.aspectRatio.value, optimalDimension * optimalDimension);
// We need to retain the optimal dimension across resets, as it is changed only when the model changes. Copy it
// from the old state, then recalculate the bbox size & scaled size.
newState.bbox.optimalDimension = state.bbox.optimalDimension;
const rect = calculateNewSize(
newState.bbox.aspectRatio.value,
newState.bbox.optimalDimension * newState.bbox.optimalDimension
);
newState.bbox.rect.width = rect.width;
newState.bbox.rect.height = rect.height;
newState.bbox.optimalDimension = optimalDimension;
newState.bbox.scaledSize = scaledSize;
syncScaledSize(newState);
return newState;
},
@@ -1145,7 +1147,7 @@ export const {
entityArrangedBackwardOne,
entityArrangedToBack,
entityOpacityChanged,
allEntitiesDeleted,
// allEntitiesDeleted, // currently unused
allEntitiesOfTypeIsHiddenToggled,
// bbox
bboxChanged,

View File

@@ -29,7 +29,7 @@ export const selectCanvasSlice = (state: RootState) => state.canvas.present;
*
* It does not check for validity of the entities.
*/
export const selectEntityCount = createSelector(selectCanvasSlice, (canvas) => {
const selectEntityCount = createSelector(selectCanvasSlice, (canvas) => {
return (
canvas.regions.entities.length +
canvas.ipAdapters.entities.length +
@@ -230,3 +230,8 @@ export const getIsHiddenSelector = (type: CanvasEntityType) => {
assert(false, 'Unhandled entity type');
}
};
export const selectWidth = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.width);
export const selectHeight = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.height);
export const selectAspectRatioID = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.aspectRatio.id);
export const selectAspectRatioValue = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.aspectRatio.value);

View File

@@ -1,7 +1,7 @@
import type { SerializableObject } from 'common/types';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { AspectRatioState } from 'features/parameters/components/DocumentSize/types';
import type { AspectRatioState } from 'features/parameters/components/Bbox/types';
import type { ParameterHeight, ParameterLoRAModel, ParameterWidth } from 'features/parameters/types/parameterSchemas';
import { zParameterNegativePrompt, zParameterPositivePrompt } from 'features/parameters/types/parameterSchemas';
import type { AnyInvocation, BaseModelType, ImageDTO, S } from 'services/api/types';
@@ -778,7 +778,7 @@ export type EntityRasterizedPayload = EntityIdentifierPayload<{
export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint';
function isDrawableEntityType(
function isRenderableEntityType(
entityType: CanvasEntityState['type']
): entityType is CanvasRenderableEntityState['type'] {
return (
@@ -813,8 +813,29 @@ export function isRegionalGuidanceEntityIdentifier(
return entityIdentifier.type === 'regional_guidance';
}
export function isFilterableEntityIdentifier(
entityIdentifier: CanvasEntityIdentifier
): entityIdentifier is CanvasEntityIdentifier<'raster_layer'> | CanvasEntityIdentifier<'control_layer'> {
return isRasterLayerEntityIdentifier(entityIdentifier) || isControlLayerEntityIdentifier(entityIdentifier);
}
export function isTransformableEntityIdentifier(
entityIdentifier: CanvasEntityIdentifier
): entityIdentifier is
| CanvasEntityIdentifier<'raster_layer'>
| CanvasEntityIdentifier<'control_layer'>
| CanvasEntityIdentifier<'inpaint_mask'>
| CanvasEntityIdentifier<'regional_guidance'> {
return (
isRasterLayerEntityIdentifier(entityIdentifier) ||
isControlLayerEntityIdentifier(entityIdentifier) ||
isInpaintMaskEntityIdentifier(entityIdentifier) ||
isRegionalGuidanceEntityIdentifier(entityIdentifier)
);
}
export function isRenderableEntity(entity: CanvasEntityState): entity is CanvasRenderableEntityState {
return isDrawableEntityType(entity.type);
return isRenderableEntityType(entity.type);
}
export const getEntityIdentifier = <T extends CanvasEntityType>(

View File

@@ -0,0 +1,26 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFoldersBold } from 'react-icons/pi';
export const ImageMenuItemChangeBoard = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const imageDTO = useImageDTOContext();
const onClick = useCallback(() => {
dispatch(imagesToChangeSelected([imageDTO]));
dispatch(isModalOpenChanged(true));
}, [dispatch, imageDTO]);
return (
<MenuItem icon={<PiFoldersBold />} onClickCapture={onClick}>
{t('boards.changeBoard')}
</MenuItem>
);
});
ImageMenuItemChangeBoard.displayName = 'ImageMenuItemChangeBoard';

View File

@@ -0,0 +1,28 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCopyBold } from 'react-icons/pi';
export const ImageMenuItemCopy = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
const { isClipboardAPIAvailable, copyImageToClipboard } = useCopyImageToClipboard();
const onClick = useCallback(() => {
copyImageToClipboard(imageDTO.image_url);
}, [copyImageToClipboard, imageDTO.image_url]);
if (!isClipboardAPIAvailable) {
return null;
}
return (
<MenuItem icon={<PiCopyBold />} onClickCapture={onClick}>
{t('parameters.copyImage')}
</MenuItem>
);
});
ImageMenuItemCopy.displayName = 'ImageMenuItemCopy';

View File

@@ -0,0 +1,25 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';
export const ImageMenuItemDelete = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const imageDTO = useImageDTOContext();
const onClick = useCallback(() => {
dispatch(imagesToDeleteSelected([imageDTO]));
}, [dispatch, imageDTO]);
return (
<MenuItem isDestructive icon={<PiTrashSimpleBold />} onClickCapture={onClick}>
{t('gallery.deleteImage', { count: 1 })}
</MenuItem>
);
});
ImageMenuItemDelete.displayName = 'ImageMenuItemDelete';

View File

@@ -0,0 +1,24 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useDownloadImage } from 'common/hooks/useDownloadImage';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDownloadSimpleBold } from 'react-icons/pi';
export const ImageMenuItemDownload = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
const { downloadImage } = useDownloadImage();
const onClick = useCallback(() => {
downloadImage(imageDTO.image_url, imageDTO.image_name);
}, [downloadImage, imageDTO.image_name, imageDTO.image_url]);
return (
<MenuItem icon={<PiDownloadSimpleBold />} onClickCapture={onClick}>
{t('parameters.downloadImage')}
</MenuItem>
);
});
ImageMenuItemDownload.displayName = 'ImageMenuItemDownload';

View File

@@ -0,0 +1,32 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { SpinnerIcon } from 'features/gallery/components/ImageContextMenu/SpinnerIcon';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFlowArrowBold } from 'react-icons/pi';
export const ImageMenuItemLoadWorkflow = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
const hasTemplates = useStore($hasTemplates);
const onClick = useCallback(() => {
getAndLoadEmbeddedWorkflow(imageDTO.image_name);
}, [getAndLoadEmbeddedWorkflow, imageDTO.image_name]);
return (
<MenuItem
icon={getAndLoadEmbeddedWorkflowResult.isLoading ? <SpinnerIcon /> : <PiFlowArrowBold />}
onClickCapture={onClick}
isDisabled={!imageDTO.has_workflow || !hasTemplates}
>
{t('nodes.loadWorkflow')}
</MenuItem>
);
});
ImageMenuItemLoadWorkflow.displayName = 'ImageMenuItemLoadWorkflow';

View File

@@ -0,0 +1,72 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { SpinnerIcon } from 'features/gallery/components/ImageContextMenu/SpinnerIcon';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowsCounterClockwiseBold,
PiAsteriskBold,
PiPaintBrushBold,
PiPlantBold,
PiQuotesBold,
} from 'react-icons/pi';
export const ImageMenuItemMetadataRecallActions = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
const {
recallAll,
remix,
recallSeed,
recallPrompts,
hasMetadata,
hasSeed,
hasPrompts,
isLoadingMetadata,
createAsPreset,
} = useImageActions(imageDTO?.image_name);
return (
<>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiArrowsCounterClockwiseBold />}
onClickCapture={remix}
isDisabled={isLoadingMetadata || !hasMetadata}
>
{t('parameters.remixImage')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiQuotesBold />}
onClickCapture={recallPrompts}
isDisabled={isLoadingMetadata || !hasPrompts}
>
{t('parameters.usePrompt')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPlantBold />}
onClickCapture={recallSeed}
isDisabled={isLoadingMetadata || !hasSeed}
>
{t('parameters.useSeed')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiAsteriskBold />}
onClickCapture={recallAll}
isDisabled={isLoadingMetadata || !hasMetadata}
>
{t('parameters.useAll')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPaintBrushBold />}
onClickCapture={createAsPreset}
isDisabled={isLoadingMetadata || !hasPrompts}
>
{t('stylePresets.useForTemplate')}
</MenuItem>
</>
);
});
ImageMenuItemMetadataRecallActions.displayName = 'ImageMenuItemMetadataRecallActions';

View File

@@ -0,0 +1,18 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowSquareOutBold } from 'react-icons/pi';
export const ImageMenuItemOpenInNewTab = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
return (
<MenuItem as="a" href={imageDTO.image_url} target="_blank" icon={<PiArrowSquareOutBold />}>
{t('common.openInNewTab')}
</MenuItem>
);
});
ImageMenuItemOpenInNewTab.displayName = 'ImageMenuItemOpenInNewTab';

View File

@@ -0,0 +1,29 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiEyeBold } from 'react-icons/pi';
export const ImageMenuItemOpenInViewer = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const imageDTO = useImageDTOContext();
const imageViewer = useImageViewer();
const onClick = useCallback(() => {
dispatch(imageToCompareChanged(null));
dispatch(imageSelected(imageDTO));
imageViewer.onOpen();
}, [dispatch, imageDTO, imageViewer]);
return (
<MenuItem icon={<PiEyeBold />} onClick={onClick}>
{t('gallery.openInViewer')}
</MenuItem>
);
});
ImageMenuItemOpenInViewer.displayName = 'ImageMenuItemOpenInViewer';

View File

@@ -0,0 +1,31 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiImagesBold } from 'react-icons/pi';
export const ImageMenuItemSelectForCompare = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const imageDTO = useImageDTOContext();
const selectMaySelectForCompare = useMemo(
() => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare?.image_name !== imageDTO.image_name),
[imageDTO.image_name]
);
const maySelectForCompare = useAppSelector(selectMaySelectForCompare);
const onClick = useCallback(() => {
dispatch(imageToCompareChanged(imageDTO));
}, [dispatch, imageDTO]);
return (
<MenuItem icon={<PiImagesBold />} isDisabled={!maySelectForCompare} onClick={onClick}>
{t('gallery.selectForCompare')}
</MenuItem>
);
});
ImageMenuItemSelectForCompare.displayName = 'ImageMenuItemSelectForCompare';

View File

@@ -0,0 +1,50 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { sentImageToCanvas } from 'features/gallery/store/actions';
import { toast } from 'features/toast/toast';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiShareFatBold } from 'react-icons/pi';
const selectBboxRect = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect);
export const ImageMenuItemSendToCanvas = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const imageDTO = useImageDTOContext();
const bboxRect = useAppSelector(selectBboxRect);
const imageViewer = useImageViewer();
const handleSendToCanvas = useCallback(() => {
const imageObject = imageDTOToImageObject(imageDTO);
const overrides: Partial<CanvasRasterLayerState> = {
position: { x: bboxRect.x, y: bboxRect.y },
objects: [imageObject],
};
dispatch(sentImageToCanvas());
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
dispatch(setActiveTab('generation'));
imageViewer.onClose();
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [bboxRect.x, bboxRect.y, dispatch, imageDTO, imageViewer, t]);
return (
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToCanvas} id="send-to-canvas">
{t('parameters.sendToCanvas')}
</MenuItem>
);
});
ImageMenuItemSendToCanvas.displayName = 'ImageMenuItemSendToCanvas';

View File

@@ -0,0 +1,33 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { toast } from 'features/toast/toast';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiShareFatBold } from 'react-icons/pi';
export const ImageMenuItemSendToUpscale = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const imageDTO = useImageDTOContext();
const handleSendToCanvas = useCallback(() => {
dispatch(upscaleInitialImageChanged(imageDTO));
dispatch(setActiveTab('upscaling'));
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToUpscale'),
status: 'success',
});
}, [dispatch, imageDTO, t]);
return (
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToCanvas} id="send-to-upscale">
{t('parameters.sendToUpscale')}
</MenuItem>
);
});
ImageMenuItemSendToUpscale.displayName = 'ImageMenuItemSendToUpscale';

View File

@@ -0,0 +1,44 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $customStarUI } from 'app/store/nanostores/customStarUI';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiStarBold, PiStarFill } from 'react-icons/pi';
import { useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images';
export const ImageMenuItemStarUnstar = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
const customStarUi = useStore($customStarUI);
const [starImages] = useStarImagesMutation();
const [unstarImages] = useUnstarImagesMutation();
const starImage = useCallback(() => {
if (imageDTO) {
starImages({ imageDTOs: [imageDTO] });
}
}, [starImages, imageDTO]);
const unstarImage = useCallback(() => {
if (imageDTO) {
unstarImages({ imageDTOs: [imageDTO] });
}
}, [unstarImages, imageDTO]);
if (imageDTO.starred) {
return (
<MenuItem icon={customStarUi ? customStarUi.off.icon : <PiStarFill />} onClickCapture={unstarImage}>
{customStarUi ? customStarUi.off.text : t('gallery.unstarImage')}
</MenuItem>
);
}
return (
<MenuItem icon={customStarUi ? customStarUi.on.icon : <PiStarBold />} onClickCapture={starImage}>
{customStarUi ? customStarUi.on.text : t('gallery.starImage')}
</MenuItem>
);
});
ImageMenuItemStarUnstar.displayName = 'ImageMenuItemStarUnstar';

View File

@@ -1,234 +1,45 @@
import { Flex, MenuDivider, MenuItem, Spinner } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import { $customStarUI } from 'app/store/nanostores/customStarUI';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
import { useDownloadImage } from 'common/hooks/useDownloadImage';
import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/actions';
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
import { $templates } from 'features/nodes/store/nodesSlice';
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { toast } from 'features/toast/toast';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
import { size } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowsCounterClockwiseBold,
PiAsteriskBold,
PiCopyBold,
PiDownloadSimpleBold,
PiFlowArrowBold,
PiFoldersBold,
PiImagesBold,
PiPaintBrushBold,
PiPlantBold,
PiQuotesBold,
PiShareFatBold,
PiStarBold,
PiStarFill,
PiTrashSimpleBold,
} from 'react-icons/pi';
import { useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images';
import { MenuDivider } from '@invoke-ai/ui-library';
import { ImageMenuItemChangeBoard } from 'features/gallery/components/ImageContextMenu/ImageMenuItemChangeBoard';
import { ImageMenuItemCopy } from 'features/gallery/components/ImageContextMenu/ImageMenuItemCopy';
import { ImageMenuItemDelete } from 'features/gallery/components/ImageContextMenu/ImageMenuItemDelete';
import { ImageMenuItemDownload } from 'features/gallery/components/ImageContextMenu/ImageMenuItemDownload';
import { ImageMenuItemLoadWorkflow } from 'features/gallery/components/ImageContextMenu/ImageMenuItemLoadWorkflow';
import { ImageMenuItemMetadataRecallActions } from 'features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActions';
import { ImageMenuItemOpenInNewTab } from 'features/gallery/components/ImageContextMenu/ImageMenuItemOpenInNewTab';
import { ImageMenuItemOpenInViewer } from 'features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer';
import { ImageMenuItemSelectForCompare } from 'features/gallery/components/ImageContextMenu/ImageMenuItemSelectForCompare';
import { ImageMenuItemSendToCanvas } from 'features/gallery/components/ImageContextMenu/ImageMenuItemSendToCanvas';
import { ImageMenuItemSendToUpscale } from 'features/gallery/components/ImageContextMenu/ImageMenuItemSendToUpscale';
import { ImageMenuItemStarUnstar } from 'features/gallery/components/ImageContextMenu/ImageMenuItemStarUnstar';
import { ImageDTOContextProvider } from 'features/gallery/contexts/ImageDTOContext';
import { memo } from 'react';
import type { ImageDTO } from 'services/api/types';
type SingleSelectionMenuItemsProps = {
imageDTO: ImageDTO;
};
const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
const { imageDTO } = props;
const selectMaySelectForCompare = useMemo(
() => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare?.image_name !== imageDTO.image_name),
[imageDTO.image_name]
);
const maySelectForCompare = useAppSelector(selectMaySelectForCompare);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const customStarUi = useStore($customStarUI);
const { downloadImage } = useDownloadImage();
const templates = useStore($templates);
const {
recallAll,
remix,
recallSeed,
recallPrompts,
hasMetadata,
hasSeed,
hasPrompts,
isLoadingMetadata,
createAsPreset,
} = useImageActions(imageDTO?.image_name);
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
const handleLoadWorkflow = useCallback(() => {
getAndLoadEmbeddedWorkflow(imageDTO.image_name);
}, [getAndLoadEmbeddedWorkflow, imageDTO.image_name]);
const [starImages] = useStarImagesMutation();
const [unstarImages] = useUnstarImagesMutation();
const { isClipboardAPIAvailable, copyImageToClipboard } = useCopyImageToClipboard();
const handleDelete = useCallback(() => {
if (!imageDTO) {
return;
}
dispatch(imagesToDeleteSelected([imageDTO]));
}, [dispatch, imageDTO]);
const handleSendToImageToImage = useCallback(() => {
// TODO(psyche): restore send to img2img functionality
dispatch(sentImageToImg2Img());
dispatch(setActiveTab('generation'));
}, [dispatch]);
const handleSendToCanvas = useCallback(() => {
// TODO(psyche): restore send to canvas functionality
dispatch(sentImageToCanvas());
dispatch(setActiveTab('generation'));
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToUnifiedCanvas'),
status: 'success',
});
}, [dispatch, t]);
const handleChangeBoard = useCallback(() => {
dispatch(imagesToChangeSelected([imageDTO]));
dispatch(isModalOpenChanged(true));
}, [dispatch, imageDTO]);
const handleCopyImage = useCallback(() => {
copyImageToClipboard(imageDTO.image_url);
}, [copyImageToClipboard, imageDTO.image_url]);
const handleStarImage = useCallback(() => {
if (imageDTO) {
starImages({ imageDTOs: [imageDTO] });
}
}, [starImages, imageDTO]);
const handleUnstarImage = useCallback(() => {
if (imageDTO) {
unstarImages({ imageDTOs: [imageDTO] });
}
}, [unstarImages, imageDTO]);
const handleDownloadImage = useCallback(() => {
downloadImage(imageDTO.image_url, imageDTO.image_name);
}, [downloadImage, imageDTO.image_name, imageDTO.image_url]);
const handleSelectImageForCompare = useCallback(() => {
dispatch(imageToCompareChanged(imageDTO));
}, [dispatch, imageDTO]);
const handleSendToUpscale = useCallback(() => {
dispatch(upscaleInitialImageChanged(imageDTO));
dispatch(setActiveTab('upscaling'));
}, [dispatch, imageDTO]);
const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) => {
return (
<>
<MenuItem as="a" href={imageDTO.image_url} target="_blank" icon={<PiShareFatBold />}>
{t('common.openInNewTab')}
</MenuItem>
{isClipboardAPIAvailable && (
<MenuItem icon={<PiCopyBold />} onClickCapture={handleCopyImage}>
{t('parameters.copyImage')}
</MenuItem>
)}
<MenuItem icon={<PiDownloadSimpleBold />} onClickCapture={handleDownloadImage}>
{t('parameters.downloadImage')}
</MenuItem>
<MenuItem icon={<PiImagesBold />} isDisabled={!maySelectForCompare} onClick={handleSelectImageForCompare}>
{t('gallery.selectForCompare')}
</MenuItem>
<ImageDTOContextProvider value={imageDTO}>
<ImageMenuItemOpenInNewTab />
<ImageMenuItemCopy />
<ImageMenuItemDownload />
<ImageMenuItemOpenInViewer />
<ImageMenuItemSelectForCompare />
<MenuDivider />
<MenuItem
icon={getAndLoadEmbeddedWorkflowResult.isLoading ? <SpinnerIcon /> : <PiFlowArrowBold />}
onClickCapture={handleLoadWorkflow}
isDisabled={!imageDTO.has_workflow || !size(templates)}
>
{t('nodes.loadWorkflow')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiArrowsCounterClockwiseBold />}
onClickCapture={remix}
isDisabled={isLoadingMetadata || !hasMetadata}
>
{t('parameters.remixImage')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiQuotesBold />}
onClickCapture={recallPrompts}
isDisabled={isLoadingMetadata || !hasPrompts}
>
{t('parameters.usePrompt')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPlantBold />}
onClickCapture={recallSeed}
isDisabled={isLoadingMetadata || !hasSeed}
>
{t('parameters.useSeed')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiAsteriskBold />}
onClickCapture={recallAll}
isDisabled={isLoadingMetadata || !hasMetadata}
>
{t('parameters.useAll')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPaintBrushBold />}
onClickCapture={createAsPreset}
isDisabled={isLoadingMetadata || !hasPrompts}
>
{t('stylePresets.useForTemplate')}
</MenuItem>
<ImageMenuItemLoadWorkflow />
<ImageMenuItemMetadataRecallActions />
<MenuDivider />
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToImageToImage} id="send-to-img2img">
{t('parameters.sendToImg2Img')}
</MenuItem>
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToUpscale} id="send-to-upscale">
{t('parameters.sendToUpscale')}
</MenuItem>
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToCanvas} id="send-to-canvas">
{t('parameters.sendToUnifiedCanvas')}
</MenuItem>
<ImageMenuItemSendToUpscale />
<ImageMenuItemSendToCanvas />
<MenuDivider />
<MenuItem icon={<PiFoldersBold />} onClickCapture={handleChangeBoard}>
{t('boards.changeBoard')}
</MenuItem>
{imageDTO.starred ? (
<MenuItem icon={customStarUi ? customStarUi.off.icon : <PiStarFill />} onClickCapture={handleUnstarImage}>
{customStarUi ? customStarUi.off.text : t('gallery.unstarImage')}
</MenuItem>
) : (
<MenuItem icon={customStarUi ? customStarUi.on.icon : <PiStarBold />} onClickCapture={handleStarImage}>
{customStarUi ? customStarUi.on.text : t('gallery.starImage')}
</MenuItem>
)}
<ImageMenuItemChangeBoard />
<ImageMenuItemStarUnstar />
<MenuDivider />
<MenuItem color="error.300" icon={<PiTrashSimpleBold />} onClickCapture={handleDelete}>
{t('gallery.deleteImage', { count: 1 })}
</MenuItem>
</>
<ImageMenuItemDelete />
</ImageDTOContextProvider>
);
};
export default memo(SingleSelectionMenuItems);
const SpinnerIcon = () => (
<Flex w="14px" alignItems="center" justifyContent="center">
<Spinner size="xs" />
</Flex>
);

View File

@@ -0,0 +1,7 @@
import { Flex, Spinner } from '@invoke-ai/ui-library';
export const SpinnerIcon = () => (
<Flex w="14px" alignItems="center" justifyContent="center">
<Spinner size="xs" />
</Flex>
);

View File

@@ -9,14 +9,12 @@ import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteIm
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { sentImageToImg2Img } from 'features/gallery/store/actions';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
import { parseAndRecallImageDimensions } from 'features/metadata/util/handlers';
import { $templates } from 'features/nodes/store/nodesSlice';
import { PostProcessingPopover } from 'features/parameters/components/PostProcessing/PostProcessingPopover';
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
import { size } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
@@ -65,14 +63,6 @@ const CurrentImageButtons = () => {
const handleUseSize = useCallback(() => {
parseAndRecallImageDimensions(lastSelectedImage);
}, [lastSelectedImage]);
const handleSendToImageToImage = useCallback(() => {
if (!imageDTO) {
return;
}
// TODO(psyche): restore send to img2img functionality
dispatch(sentImageToImg2Img());
dispatch(setActiveTab('generation'));
}, [dispatch, imageDTO]);
const handleClickUpscale = useCallback(() => {
if (!imageDTO) {
return;
@@ -93,9 +83,8 @@ const CurrentImageButtons = () => {
useHotkeys('p', recallPrompts, { enabled: isImageViewerActive }, [recallPrompts, isImageViewerActive]);
useHotkeys('r', remix, { enabled: isImageViewerActive }, [remix, isImageViewerActive]);
useHotkeys('d', handleUseSize, { enabled: isImageViewerActive }, [handleUseSize, isImageViewerActive]);
useHotkeys('shift+i', handleSendToImageToImage, { enabled: isImageViewerActive }, [imageDTO, isImageViewerActive]);
useHotkeys(
'Shift+U',
'shift+u',
handleClickUpscale,
{ enabled: Boolean(isUpscalingEnabled && isImageViewerActive && isConnected) },
[isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected, isImageViewerActive]

View File

@@ -0,0 +1,13 @@
import { createContext, useContext } from 'react';
import type { ImageDTO } from 'services/api/types';
import { assert } from 'tsafe';
const ImageDTOContext = createContext<ImageDTO | null>(null);
export const ImageDTOContextProvider = ImageDTOContext.Provider;
export const useImageDTOContext = () => {
const imageDTO = useContext(ImageDTOContext);
assert(imageDTO !== null, 'useImageDTOContext must be used within ImageDTOContextProvider');
return imageDTO;
};

View File

@@ -2,6 +2,4 @@ import { createAction } from '@reduxjs/toolkit';
export const sentImageToCanvas = createAction('gallery/sentImageToCanvas');
export const sentImageToImg2Img = createAction('gallery/sentImageToImg2Img');
export const imageDownloaded = createAction('gallery/imageDownloaded');

View File

@@ -55,7 +55,7 @@ import {
} from 'features/nodes/types/field';
import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation';
import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation';
import { atom } from 'nanostores';
import { atom, computed } from 'nanostores';
import type { MouseEvent } from 'react';
import type { Edge, EdgeChange, NodeChange, Viewport, XYPosition } from 'reactflow';
import { applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from 'reactflow';
@@ -435,6 +435,7 @@ export const {
export const $cursorPos = atom<XYPosition | null>(null);
export const $templates = atom<Templates>({});
export const $hasTemplates = computed($templates, (templates) => Object.keys(templates).length > 0);
export const $copiedNodes = atom<AnyNode[]>([]);
export const $copiedEdges = atom<InvocationNodeEdge[]>([]);
export const $edgesToCopiedNodes = atom<InvocationNodeEdge[]>([]);

View File

@@ -17,7 +17,7 @@ export const addControlNets = async (
manager: CanvasManager,
layers: CanvasControlLayerState[],
g: Graph,
bbox: Rect,
rect: Rect,
collector: Invocation<'collect'>,
base: BaseModelType
): Promise<AddControlNetsResult> => {
@@ -35,7 +35,7 @@ export const addControlNets = async (
const adapter = manager.adapters.controlLayers.get(layer.id);
assert(adapter, 'Adapter not found');
const imageDTO = await adapter.renderer.rasterize({ rect: bbox, attrs: { opacity: 1, filters: [] } });
const imageDTO = await adapter.renderer.rasterize({ rect, attrs: { opacity: 1, filters: [] }, bg: 'black' });
addControlNetToGraph(g, layer, imageDTO, collector);
}
@@ -50,7 +50,7 @@ export const addT2IAdapters = async (
manager: CanvasManager,
layers: CanvasControlLayerState[],
g: Graph,
bbox: Rect,
rect: Rect,
collector: Invocation<'collect'>,
base: BaseModelType
): Promise<AddT2IAdaptersResult> => {
@@ -68,7 +68,7 @@ export const addT2IAdapters = async (
const adapter = manager.adapters.controlLayers.get(layer.id);
assert(adapter, 'Adapter not found');
const imageDTO = await adapter.renderer.rasterize({ rect: bbox, attrs: { opacity: 1, filters: [] } });
const imageDTO = await adapter.renderer.rasterize({ rect, attrs: { opacity: 1, filters: [], bg: 'black' } });
addT2IAdapterToGraph(g, layer, imageDTO, collector);
}

View File

@@ -1,19 +1,16 @@
import type { ComboboxOption, SystemStyleObject } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import type { SingleValue } from 'chakra-react-select';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { bboxAspectRatioIdChanged } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { ASPECT_RATIO_OPTIONS } from 'features/parameters/components/DocumentSize/constants';
import { isAspectRatioID } from 'features/parameters/components/DocumentSize/types';
import { selectAspectRatioID } from 'features/controlLayers/store/selectors';
import { ASPECT_RATIO_OPTIONS } from 'features/parameters/components/Bbox/constants';
import { isAspectRatioID } from 'features/parameters/components/Bbox/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
const selectAspectRatioID = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.aspectRatio.id);
export const AspectRatioSelect = memo(() => {
export const BboxAspectRatioSelect = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const id = useAppSelector(selectAspectRatioID);
@@ -40,6 +37,6 @@ export const AspectRatioSelect = memo(() => {
);
});
AspectRatioSelect.displayName = 'AspectRatioSelect';
BboxAspectRatioSelect.displayName = 'BboxAspectRatioSelect';
const selectStyles: SystemStyleObject = { minW: 24 };

View File

@@ -1,17 +1,13 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { bboxHeightChanged } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice, selectOptimalDimension } from 'features/controlLayers/store/selectors';
import { selectConfigSlice } from 'features/system/store/configSlice';
import { selectHeight, selectOptimalDimension } from 'features/controlLayers/store/selectors';
import { selectHeightConfig } from 'features/system/store/configSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
const selectHeight = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.height);
const selectHeightConfig = createSelector(selectConfigSlice, (config) => config.sd.height);
export const ParamHeight = memo(() => {
export const BboxHeight = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const optimalDimension = useAppSelector(selectOptimalDimension);
@@ -58,4 +54,4 @@ export const ParamHeight = memo(() => {
);
});
ParamHeight.displayName = 'ParamHeight';
BboxHeight.displayName = 'BboxHeight';

View File

@@ -9,7 +9,7 @@ import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi';
const selectAspectRatioIsLocked = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.aspectRatio.isLocked);
export const LockAspectRatioButton = memo(() => {
export const BboxLockAspectRatioButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isLocked = useAppSelector(selectAspectRatioIsLocked);
@@ -29,4 +29,4 @@ export const LockAspectRatioButton = memo(() => {
);
});
LockAspectRatioButton.displayName = 'LockAspectRatioButton';
BboxLockAspectRatioButton.displayName = 'BboxLockAspectRatioButton';

View File

@@ -0,0 +1,71 @@
import { Flex, Grid, GridItem } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { selectAspectRatioValue, selectHeight, selectWidth } from 'features/controlLayers/store/selectors';
import { memo, useMemo } from 'react';
import { useMeasure } from 'react-use';
export const BboxPreview = memo(() => {
const bboxWidth = useAppSelector(selectWidth);
const bboxHeight = useAppSelector(selectHeight);
const aspectRatioValue = useAppSelector(selectAspectRatioValue);
const [ref, dims] = useMeasure<HTMLDivElement>();
const previewBoxSize = useMemo(() => {
if (!dims) {
return { width: 0, height: 0 };
}
let width = bboxWidth;
let height = bboxHeight;
if (bboxWidth > bboxHeight) {
width = dims.width;
height = width / aspectRatioValue;
} else {
height = dims.height;
width = height * aspectRatioValue;
}
return { width, height };
}, [dims, bboxWidth, bboxHeight, aspectRatioValue]);
return (
<Flex w="full" h="full" alignItems="center" justifyContent="center" ref={ref}>
<Flex
position="relative"
borderRadius="base"
borderColor="base.600"
borderWidth="3px"
width={`${previewBoxSize.width}px`}
height={`${previewBoxSize.height}px`}
alignItems="center"
justifyContent="center"
>
<Grid
borderRadius="base"
position="absolute"
top={0}
right={0}
bottom={0}
left={0}
gridTemplateColumns="1fr 1fr 1fr"
gridTemplateRows="1fr 1fr 1fr"
gap="1px"
bg="base.700"
>
<GridItem bg="base.800" />
<GridItem bg="base.800" />
<GridItem bg="base.800" />
<GridItem bg="base.800" />
<GridItem bg="base.800" />
<GridItem bg="base.800" />
<GridItem bg="base.800" />
<GridItem bg="base.800" />
<GridItem bg="base.800" />
</Grid>
</Flex>
</Flex>
);
});
BboxPreview.displayName = 'BboxPreview';

View File

@@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
const selectScaleMethod = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.scaleMethod);
const ParamScaleBeforeProcessing = () => {
const BboxScaleMethod = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const scaleMethod = useAppSelector(selectScaleMethod);
@@ -47,4 +47,4 @@ const ParamScaleBeforeProcessing = () => {
);
};
export default memo(ParamScaleBeforeProcessing);
export default memo(BboxScaleMethod);

View File

@@ -14,7 +14,7 @@ const selectScaledBoundingBoxHeightConfig = createSelector(
(config) => config.sd.scaledBoundingBoxHeight
);
const ParamScaledHeight = () => {
const BboxScaledHeight = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const optimalDimension = useAppSelector(selectOptimalDimension);
@@ -55,4 +55,4 @@ const ParamScaledHeight = () => {
);
};
export default memo(ParamScaledHeight);
export default memo(BboxScaledHeight);

View File

@@ -14,7 +14,7 @@ const selectScaledBoundingBoxWidthConfig = createSelector(
(config) => config.sd.scaledBoundingBoxWidth
);
const ParamScaledWidth = () => {
const BboxScaledWidth = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const optimalDimension = useAppSelector(selectOptimalDimension);
@@ -54,4 +54,4 @@ const ParamScaledWidth = () => {
);
};
export default memo(ParamScaledWidth);
export default memo(BboxScaledWidth);

View File

@@ -11,7 +11,7 @@ import { RiSparklingFill } from 'react-icons/ri';
const selectWidth = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.width);
const selectHeight = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.height);
export const SetOptimalSizeButton = memo(() => {
export const BboxSetOptimalSizeButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const width = useAppSelector(selectWidth);
@@ -51,4 +51,4 @@ export const SetOptimalSizeButton = memo(() => {
);
});
SetOptimalSizeButton.displayName = 'SetOptimalSizeButton';
BboxSetOptimalSizeButton.displayName = 'BboxSetOptimalSizeButton';

View File

@@ -0,0 +1,38 @@
import type { FormLabelProps } from '@invoke-ai/ui-library';
import { Flex, FormControlGroup } from '@invoke-ai/ui-library';
import { BboxAspectRatioSelect } from 'features/parameters/components/Bbox/BboxAspectRatioSelect';
import { BboxHeight } from 'features/parameters/components/Bbox/BboxHeight';
import { BboxLockAspectRatioButton } from 'features/parameters/components/Bbox/BboxLockAspectRatioButton';
import { BboxPreview } from 'features/parameters/components/Bbox/BboxPreview';
import { BboxSetOptimalSizeButton } from 'features/parameters/components/Bbox/BboxSetOptimalSizeButton';
import { BboxSwapDimensionsButton } from 'features/parameters/components/Bbox/BboxSwapDimensionsButton';
import { BboxWidth } from 'features/parameters/components/Bbox/BboxWidth';
import { memo } from 'react';
export const BboxSettings = memo(() => {
return (
<Flex gap={4} alignItems="center">
<Flex gap={4} flexDirection="column" width="full">
<FormControlGroup formLabelProps={formLabelProps}>
<Flex gap={4}>
<BboxAspectRatioSelect />
<BboxSwapDimensionsButton />
<BboxLockAspectRatioButton />
<BboxSetOptimalSizeButton />
</Flex>
<BboxWidth />
<BboxHeight />
</FormControlGroup>
</Flex>
<Flex w="108px" h="108px" flexShrink={0} flexGrow={0} alignItems="center" justifyContent="center">
<BboxPreview />
</Flex>
</Flex>
);
});
BboxSettings.displayName = 'BboxSettings';
const formLabelProps: FormLabelProps = {
minW: 14,
};

View File

@@ -5,7 +5,7 @@ import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowsDownUpBold } from 'react-icons/pi';
export const SwapDimensionsButton = memo(() => {
export const BboxSwapDimensionsButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
@@ -23,4 +23,4 @@ export const SwapDimensionsButton = memo(() => {
);
});
SwapDimensionsButton.displayName = 'SwapDimensionsButton';
BboxSwapDimensionsButton.displayName = 'BboxSwapDimensionsButton';

View File

@@ -1,17 +1,13 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { bboxWidthChanged } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice, selectOptimalDimension } from 'features/controlLayers/store/selectors';
import { selectConfigSlice } from 'features/system/store/configSlice';
import { selectOptimalDimension, selectWidth } from 'features/controlLayers/store/selectors';
import { selectWidthConfig } from 'features/system/store/configSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
const selectWidth = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.width);
const selectWidthConfig = createSelector(selectConfigSlice, (config) => config.sd.width);
export const ParamWidth = memo(() => {
export const BboxWidth = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const width = useAppSelector(selectWidth);
@@ -58,4 +54,4 @@ export const ParamWidth = memo(() => {
);
});
ParamWidth.displayName = 'ParamWidth';
BboxWidth.displayName = 'BboxWidth';

View File

@@ -1,20 +0,0 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectImg2imgStrength, setImg2imgStrength } from 'features/controlLayers/store/paramsSlice';
import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength';
import { memo, useCallback } from 'react';
const ParamImageToImageStrength = () => {
const img2imgStrength = useAppSelector(selectImg2imgStrength);
const dispatch = useAppDispatch();
const onChange = useCallback(
(v: number) => {
dispatch(setImg2imgStrength(v));
},
[dispatch]
);
return <ImageToImageStrength value={img2imgStrength} onChange={onChange} />;
};
export default memo(ParamImageToImageStrength);

View File

@@ -1,38 +0,0 @@
import type { FormLabelProps } from '@invoke-ai/ui-library';
import { Flex, FormControlGroup } from '@invoke-ai/ui-library';
import { CanvasResizer } from 'features/controlLayers/components/CanvasResizer';
import { ParamHeight } from 'features/parameters/components/Core/ParamHeight';
import { ParamWidth } from 'features/parameters/components/Core/ParamWidth';
import { AspectRatioSelect } from 'features/parameters/components/DocumentSize/AspectRatioSelect';
import { LockAspectRatioButton } from 'features/parameters/components/DocumentSize/LockAspectRatioButton';
import { SetOptimalSizeButton } from 'features/parameters/components/DocumentSize/SetOptimalSizeButton';
import { SwapDimensionsButton } from 'features/parameters/components/DocumentSize/SwapDimensionsButton';
import { memo } from 'react';
export const DocumentSize = memo(() => {
return (
<Flex gap={4} alignItems="center">
<Flex gap={4} flexDirection="column" width="full">
<FormControlGroup formLabelProps={formLabelProps}>
<Flex gap={4}>
<AspectRatioSelect />
<SwapDimensionsButton />
<LockAspectRatioButton />
<SetOptimalSizeButton />
</Flex>
<ParamWidth />
<ParamHeight />
</FormControlGroup>
</Flex>
<Flex w="108px" h="108px" flexShrink={0} flexGrow={0} bg="base.850" alignItems="center" justifyContent="center">
<CanvasResizer />
</Flex>
</Flex>
);
});
DocumentSize.displayName = 'DocumentSize';
const formLabelProps: FormLabelProps = {
minW: 14,
};

View File

@@ -1,47 +0,0 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { selectImg2imgStrengthConfig } from 'features/system/store/configSlice';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const marks = [0, 0.5, 1];
type Props = {
value: number;
onChange: (v: number) => void;
};
const ImageToImageStrength = ({ value, onChange }: Props) => {
const config = useAppSelector(selectImg2imgStrengthConfig);
const { t } = useTranslation();
return (
<FormControl>
<InformationalPopover feature="paramDenoisingStrength">
<FormLabel>{`${t('parameters.denoisingStrength')}`}</FormLabel>
</InformationalPopover>
<CompositeSlider
step={config.coarseStep}
fineStep={config.fineStep}
min={config.sliderMin}
max={config.sliderMax}
defaultValue={config.initial}
onChange={onChange}
value={value}
marks={marks}
/>
<CompositeNumberInput
step={config.coarseStep}
fineStep={config.fineStep}
min={config.numberInputMin}
max={config.numberInputMax}
defaultValue={config.initial}
onChange={onChange}
value={value}
/>
</FormControl>
);
};
export default memo(ImageToImageStrength);

View File

@@ -1,16 +1,14 @@
import type { FormLabelProps } from '@invoke-ai/ui-library';
import { Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-library';
import { EMPTY_ARRAY } from 'app/store/constants';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { HrfSettings } from 'features/hrf/components/HrfSettings';
import { selectHrfSlice } from 'features/hrf/store/hrfSlice';
import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing';
import ParamScaledHeight from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight';
import ParamScaledWidth from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth';
import ParamImageToImageStrength from 'features/parameters/components/Canvas/ParamImageToImageStrength';
import { DocumentSize } from 'features/parameters/components/DocumentSize/DocumentSize';
import BboxScaledHeight from 'features/parameters/components/Bbox/BboxScaledHeight';
import BboxScaledWidth from 'features/parameters/components/Bbox/BboxScaledWidth';
import BboxScaleMethod from 'features/parameters/components/Bbox/BboxScaleMethod';
import { BboxSettings } from 'features/parameters/components/Bbox/BboxSettings';
import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput';
import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize';
import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle';
@@ -19,34 +17,30 @@ import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selector = createMemoizedSelector(
[selectHrfSlice, selectCanvasSlice, selectParamsSlice],
(hrf, canvas, params) => {
const { shouldRandomizeSeed, model } = params;
const { hrfEnabled } = hrf;
const badges: string[] = [];
const isSDXL = model?.base === 'sdxl';
const selectBadges = createMemoizedSelector([selectCanvasSlice, selectParamsSlice], (canvas, params) => {
const { shouldRandomizeSeed } = params;
const badges: string[] = [];
const { aspectRatio } = canvas.bbox;
const { width, height } = canvas.bbox.rect;
const { aspectRatio } = canvas.bbox;
const { width, height } = canvas.bbox.rect;
badges.push(`${width}×${height}`);
badges.push(aspectRatio.id);
badges.push(`${width}×${height}`);
badges.push(aspectRatio.id);
if (aspectRatio.isLocked) {
badges.push('locked');
}
if (!shouldRandomizeSeed) {
badges.push('Manual Seed');
}
if (hrfEnabled && !isSDXL) {
badges.push('HiRes Fix');
}
return { badges, isSDXL };
if (aspectRatio.isLocked) {
badges.push('locked');
}
);
if (!shouldRandomizeSeed) {
badges.push('Manual Seed');
}
if (badges.length === 0) {
return EMPTY_ARRAY;
}
badges;
});
const scalingLabelProps: FormLabelProps = {
minW: '4.5rem',
@@ -54,7 +48,7 @@ const scalingLabelProps: FormLabelProps = {
export const ImageSettingsAccordion = memo(() => {
const { t } = useTranslation();
const { badges, isSDXL } = useAppSelector(selector);
const badges = useAppSelector(selectBadges);
const { isOpen: isOpenAccordion, onToggle: onToggleAccordion } = useStandaloneAccordionToggle({
id: 'image-settings',
defaultIsOpen: true,
@@ -72,22 +66,18 @@ export const ImageSettingsAccordion = memo(() => {
onToggle={onToggleAccordion}
>
<Flex px={4} pt={4} w="full" h="full" flexDir="column" data-testid="image-settings-accordion">
<Flex flexDir="column" gap={4}>
<DocumentSize />
<ParamImageToImageStrength />
<BboxSettings />
<Flex pt={4} gap={4} alignItems="center">
<ParamSeedNumberInput />
<ParamSeedShuffle />
<ParamSeedRandomize />
</Flex>
<Expander label={t('accordions.advanced.options')} isOpen={isOpenExpander} onToggle={onToggleExpander}>
<Flex gap={4} pb={4} flexDir="column">
<Flex gap={4} alignItems="center">
<ParamSeedNumberInput />
<ParamSeedShuffle />
<ParamSeedRandomize />
</Flex>
{!isSDXL && <HrfSettings />}
<ParamScaleBeforeProcessing />
<BboxScaleMethod />
<FormControlGroup formLabelProps={scalingLabelProps}>
<ParamScaledWidth />
<ParamScaledHeight />
<BboxScaledWidth />
<BboxScaledHeight />
</FormControlGroup>
</Flex>
</Expander>

View File

@@ -16,10 +16,10 @@ type UseGetAndLoadEmbeddedWorkflowReturn = {
};
type UseGetAndLoadEmbeddedWorkflow = (
options: UseGetAndLoadEmbeddedWorkflowOptions
options?: UseGetAndLoadEmbeddedWorkflowOptions
) => UseGetAndLoadEmbeddedWorkflowReturn;
export const useGetAndLoadEmbeddedWorkflow: UseGetAndLoadEmbeddedWorkflow = ({ onSuccess, onError }) => {
export const useGetAndLoadEmbeddedWorkflow: UseGetAndLoadEmbeddedWorkflow = (options) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const [_getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult] = useLazyGetImageWorkflowQuery();
@@ -30,7 +30,7 @@ export const useGetAndLoadEmbeddedWorkflow: UseGetAndLoadEmbeddedWorkflow = ({ o
if (data) {
dispatch(workflowLoadRequested({ data, asCopy: true }));
// No toast - the listener for this action does that after the workflow is loaded
onSuccess && onSuccess();
options?.onSuccess && options?.onSuccess();
} else {
toast({
id: 'PROBLEM_RETRIEVING_WORKFLOW',
@@ -44,10 +44,10 @@ export const useGetAndLoadEmbeddedWorkflow: UseGetAndLoadEmbeddedWorkflow = ({ o
title: t('toast.problemRetrievingWorkflow'),
status: 'error',
});
onError && onError();
options?.onError && options?.onError();
}
},
[_getAndLoadEmbeddedWorkflow, dispatch, onSuccess, t, onError]
[_getAndLoadEmbeddedWorkflow, dispatch, options, t]
);
return { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult };

View File

@@ -276,12 +276,12 @@ export const queueApi = api.injectEndpoints({
},
invalidatesTags: ['SessionQueueStatus', 'BatchStatus'],
}),
cancelByBatchOrigin: build.mutation<
paths['/api/v1/queue/{queue_id}/cancel_by_origin']['put']['responses']['200']['content']['application/json'],
paths['/api/v1/queue/{queue_id}/cancel_by_origin']['put']['parameters']['query']
cancelByBatchDestination: build.mutation<
paths['/api/v1/queue/{queue_id}/cancel_by_destination']['put']['responses']['200']['content']['application/json'],
paths['/api/v1/queue/{queue_id}/cancel_by_destination']['put']['parameters']['query']
>({
query: (params) => ({
url: buildQueueUrl('cancel_by_origin'),
url: buildQueueUrl('cancel_by_destination'),
method: 'PUT',
params,
}),

View File

@@ -1104,7 +1104,7 @@ export type paths = {
patch?: never;
trace?: never;
};
"/api/v1/queue/{queue_id}/cancel_by_origin": {
"/api/v1/queue/{queue_id}/cancel_by_destination": {
parameters: {
query?: never;
header?: never;
@@ -1113,10 +1113,10 @@ export type paths = {
};
get?: never;
/**
* Cancel By Origin
* Cancel By Destination
* @description Immediately cancels all queue items with the given origin
*/
put: operations["cancel_by_origin"];
put: operations["cancel_by_destination"];
post?: never;
delete?: never;
options?: never;
@@ -3052,10 +3052,10 @@ export type components = {
canceled: number;
};
/**
* CancelByOriginResult
* @description Result of canceling by list of batch ids
* CancelByDestinationResult
* @description Result of canceling by a destination
*/
CancelByOriginResult: {
CancelByDestinationResult: {
/**
* Canceled
* @description Number of queue items canceled
@@ -18561,11 +18561,11 @@ export interface operations {
};
};
};
cancel_by_origin: {
cancel_by_destination: {
parameters: {
query: {
/** @description The origin to cancel all queue items for */
origin: string;
/** @description The destination to cancel all queue items for */
destination: string;
};
header?: never;
path: {

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