Compare commits

...

465 Commits

Author SHA1 Message Date
psychedelicious
a2ad5f1a9a feat(ui): tweak layout of staging area toolbar 2024-08-23 19:55:02 +10:00
psychedelicious
ff74a5356f chore(ui): typegen 2024-08-23 19:52:37 +10:00
psychedelicious
92dc30dace tidy(app): clean up app changes for canvas v2 2024-08-23 19:52:04 +10:00
psychedelicious
3af577b210 feat(ui): use singleton for clear q confirm dialog 2024-08-23 19:47:51 +10:00
psychedelicious
d0464330f7 fix(ui): rip out broken recall logic, NO TS ERRORS 2024-08-23 19:47:51 +10:00
psychedelicious
dd3ef4a80f chore(ui): lint 2024-08-23 19:47:51 +10:00
psychedelicious
0ced891944 fix(ui): staging area interaction scopes 2024-08-23 19:47:51 +10:00
psychedelicious
10a5452df9 fix(ui): staging area actions 2024-08-23 19:47:51 +10:00
psychedelicious
cb97969bbc tidy(ui): more cleanup 2024-08-23 19:47:51 +10:00
psychedelicious
71e742e238 fix(ui): upscale tab graph 2024-08-23 19:47:51 +10:00
psychedelicious
fadd20fb8e fix(ui): sdxl graph builder 2024-08-23 19:47:51 +10:00
psychedelicious
01b9ca78e4 fix(ui): select next entity in the list when deleting 2024-08-23 19:47:51 +10:00
psychedelicious
2baf825f34 feat(ui): fix delete layer hotkey 2024-08-23 19:47:51 +10:00
psychedelicious
1fa8048509 tidy(ui): "eye dropper" -> "color picker" 2024-08-23 19:47:51 +10:00
psychedelicious
a000ad75f6 tidy(ui): regional guidance buttons 2024-08-23 19:47:51 +10:00
psychedelicious
aefb2339bb feat(ui): update entity list menu 2024-08-23 19:47:51 +10:00
psychedelicious
a4f8671f86 feat(ui): add log debug button 2024-08-23 19:47:51 +10:00
psychedelicious
73530ba54f chore(ui): lint 2024-08-23 19:47:51 +10:00
psychedelicious
685eb9927d chore(ui): prettier 2024-08-23 19:47:51 +10:00
psychedelicious
ee57302fc3 chore(ui): eslint 2024-08-23 19:47:51 +10:00
psychedelicious
c1fb9cdb93 tidy(ui): remove unused stuff 4 2024-08-23 19:47:36 +10:00
psychedelicious
aa6d441552 tidy(ui): remove unused stuff 3 2024-08-23 19:47:01 +10:00
psychedelicious
25d8d4c2e9 tidy(ui): remove unused pkg @chakra-ui/react-use-size 2024-08-23 19:47:01 +10:00
psychedelicious
427ea6da5c feat(ui): revise graph building for control layers, fix issues w/ invocation complete events 2024-08-23 19:47:01 +10:00
psychedelicious
d9f4266630 feat(ui): use unique id for metadata in Graph class 2024-08-23 19:47:01 +10:00
psychedelicious
96f6e9e683 tidy(ui): remove unused stuff 2 2024-08-23 19:47:01 +10:00
psychedelicious
f10248e3f5 tidy(ui): remove unused stuff 2024-08-23 19:47:01 +10:00
psychedelicious
6a21f5fde1 tidy(ui): reduce use of parseify util 2024-08-23 19:47:01 +10:00
psychedelicious
ff20dd509a feat(ui): refine canvas entity list items & menus 2024-08-23 19:47:01 +10:00
psychedelicious
78a59b5b78 feat(ui): canvas layer preview, revised reactivity for adapters 2024-08-23 19:47:01 +10:00
psychedelicious
46bfbbbc87 feat(ui): add SyncableMap
Can be used with useSyncExternal store to make a `Map` reactive.
2024-08-23 19:47:01 +10:00
psychedelicious
a6d73d0773 tidy(ui): removed unused transform methods from canvasmanager 2024-08-23 19:47:01 +10:00
psychedelicious
6578e8bef8 feat(ui): transform tool ux 2024-08-23 19:47:01 +10:00
psychedelicious
0596d25e07 feat(ui): rough out canvas mode 2024-08-23 19:47:01 +10:00
psychedelicious
86e8ce9139 feat(ui): add canvas autosave checkbox 2024-08-23 19:47:01 +10:00
psychedelicious
5aa2957da4 fix(ui): memory leak when getting image DTO
must unsubscribe!
2024-08-23 19:47:01 +10:00
psychedelicious
82f0cb2c8c feat(ui): rework settings menu 2024-08-23 19:47:01 +10:00
psychedelicious
fa48145cbc feat(ui): no entities fallback buttons 2024-08-23 19:47:01 +10:00
psychedelicious
6d1edc330d perf(ui): optimize gallery image delete button rendering 2024-08-23 19:47:01 +10:00
psychedelicious
97c0d3f6be feat(ui): remove "solid" background option 2024-08-23 19:47:01 +10:00
psychedelicious
a79a25ad63 tidy(ui): organise files and classes 2024-08-23 19:47:01 +10:00
psychedelicious
6a8ceef404 tidy(ui): abstract compositing logic to module 2024-08-23 19:47:01 +10:00
psychedelicious
3539670d93 fix(ui): fix canvas cache property access 2024-08-23 19:47:01 +10:00
psychedelicious
c54bc32ef6 tidy(ui): clean up CanvasFilter class 2024-08-23 19:47:01 +10:00
psychedelicious
fee293e289 tidy(ui): clean up a few bits and bobs 2024-08-23 19:47:01 +10:00
psychedelicious
747eef9ccc tidy(ui): abstract canvas rendering logic to module 2024-08-23 19:47:01 +10:00
psychedelicious
7d2df399ed tidy(ui): abstract caching logic to module 2024-08-23 19:47:01 +10:00
psychedelicious
68fad5cdcc tidy(ui): abstract worker logic to module 2024-08-23 19:47:01 +10:00
psychedelicious
b4d656c203 tidy(ui): abstract stage logic into module 2024-08-23 19:47:01 +10:00
psychedelicious
3136d89d52 feat(ui): add entity group hiding 2024-08-23 19:47:01 +10:00
psychedelicious
27e829b955 feat(ui): move all caching out of redux
While we lose the benefit of the caches persisting across reloads, this is a much simpler way to handle things. If we need a persistent cache, we can explore it in the future.
2024-08-23 19:47:01 +10:00
psychedelicious
e03e870d5b feat(ui): revised rasterization caching
- use `stable-hash` to generate stable, non-crypto hashes for cache entries, instead of using deep object comparisons
- use an object to store image name caches
2024-08-23 19:47:01 +10:00
psychedelicious
9465ff450b feat(ui): revise filter implementation 2024-08-23 19:47:01 +10:00
psychedelicious
92906a9575 fix(ui): add button to delete inpaint mask 2024-08-23 19:47:01 +10:00
psychedelicious
77f206abe4 feat(ui): add contexts/hooks to access entity adapters directly 2024-08-23 19:47:01 +10:00
psychedelicious
44a3f61580 feat(ui): add CanvasManagerProviderGate
This context waits to render its children its until the canvas manager is available. Then its children have access to the manager directly via hook.
2024-08-23 19:47:01 +10:00
psychedelicious
0a2afed08b feat(ui) do not set $canvasManager until ready 2024-08-23 19:47:01 +10:00
psychedelicious
9b3b961105 fix(ui): inpaint mask naming 2024-08-23 19:47:01 +10:00
psychedelicious
9b1828e1aa feat(ui): efficient canvas compositing
Also solves issue of exporting layers at different opacities than what is visible
2024-08-23 19:47:01 +10:00
psychedelicious
5101873f49 feat(ui): allow multiple inpaint masks
This is easier than making it a nullable singleton
2024-08-23 19:47:01 +10:00
psychedelicious
c612f18114 fix(ui): missing rasterization cache invalidations 2024-08-23 19:47:01 +10:00
psychedelicious
7e400d876f feat(ui): iterate on filter UI, flow 2024-08-23 19:47:01 +10:00
psychedelicious
677dddcfc9 fix(ui): rehydration data loss 2024-08-23 19:47:01 +10:00
psychedelicious
0792b9175e feat(ui): sort log namespaces 2024-08-23 19:47:01 +10:00
psychedelicious
e4829f80af fix(ui): do not merge arrays by index during rehydration 2024-08-23 19:47:01 +10:00
psychedelicious
bb760f3eb4 fix(ui): clone parsed data during state rehydration
Without this, the objects and arrays in `parsed` could be mutated, and the log statment would show the mutated data.
2024-08-23 19:47:01 +10:00
psychedelicious
388c65287b fix(ui): fix logger filter
was accidetnally replacing the filter instead of appending to it.
2024-08-23 19:47:01 +10:00
psychedelicious
12cd41e05c fix(ui): race condition queue status
Sequence of events causing the race condition:
- Enqueue batch
- Invalidate `SessionQueueStatus` tag
- Request updated queue status via HTTP - batch still processing at this point
- Batch completes
- Event emitted saying so
- Optimistically update the queue status cache, it is correct
- HTTP request makes it back and overwrites the optimistic update, indicating the batch is still in progress

FIxed by not invalidating the cache.
2024-08-23 19:47:01 +10:00
psychedelicious
7765c03949 fix(ui): handle opacity for masks 2024-08-23 19:47:01 +10:00
psychedelicious
3daa80c57f feat(ui): default background to checkerboard 2024-08-23 19:47:01 +10:00
psychedelicious
5dbbef4ebd feat(ui): clean up logging namespaces, allow skipping namespaces 2024-08-23 19:47:01 +10:00
psychedelicious
db33b3f7b5 chore(ui): bump ui library 2024-08-23 19:47:01 +10:00
psychedelicious
8ffcf2a6be fix(ui): do not allow drawing if layer disabled 2024-08-23 19:47:01 +10:00
psychedelicious
2e7ae6a07e fix(ui): stale state causing race conditions & extraneous renders 2024-08-23 19:47:01 +10:00
psychedelicious
fea1711f0c fix(ui): do not clear buffer when rendering "real" objects 2024-08-23 19:47:01 +10:00
psychedelicious
2a3546db97 tidy(ui): remove "filter" from CanvasImageState 2024-08-23 19:47:01 +10:00
psychedelicious
285c266612 feat(ui): better editable title 2024-08-23 19:47:01 +10:00
psychedelicious
426ad54c53 fix(ui): stroke eraserline 2024-08-23 19:47:01 +10:00
psychedelicious
fc75f7919f feat(ui): restore transparency effect for control layers 2024-08-23 19:47:01 +10:00
psychedelicious
6c6b1aaff6 feat(ui): use text cursor for entity title 2024-08-23 19:47:01 +10:00
psychedelicious
c319d653ac tidy(ui): remove extraneous logging in CanvasStateApi 2024-08-23 19:47:01 +10:00
psychedelicious
d887e474e7 feat(ui): better buffer commit logic 2024-08-23 19:47:01 +10:00
psychedelicious
da7b52d6ba feat(ui): render buffer separately from "real" objects 2024-08-23 19:47:01 +10:00
psychedelicious
b5aa308593 fix(ui): pixelRect should always be integer 2024-08-23 19:47:01 +10:00
psychedelicious
0b7ceb3bb6 fix(ui): only update stage attrs when stage itself is dragged 2024-08-23 19:47:01 +10:00
psychedelicious
3a70cefda2 feat(ui): add line simplification
This fixes some awkward issues where line segments stack up.
2024-08-23 19:47:01 +10:00
psychedelicious
4b609251e1 fix(ui): various things listening when they need not listen 2024-08-23 19:47:01 +10:00
psychedelicious
0839eac0f7 feat(ui): layer opacity via caching 2024-08-23 19:47:01 +10:00
psychedelicious
5f2a7feeee feat(ui): reset view fits all visible objects 2024-08-23 19:47:01 +10:00
psychedelicious
982535eb92 fix(ui): rerenders when changing canvas scale 2024-08-23 19:47:01 +10:00
psychedelicious
0c2b8edc8d fix(ui): do not render rasterized layer unless renderObjects=true 2024-08-23 19:47:01 +10:00
psychedelicious
f78f4ca25f feat(ui): revise app layout strategy, add interaction scopes for hotkeys 2024-08-23 19:47:01 +10:00
psychedelicious
d6b3e6c07d feat(ui): tweak mask patterns 2024-08-23 19:47:01 +10:00
psychedelicious
071ff8e74a fix(ui): dynamic prompts recalcs when presets are loaded 2024-08-23 19:47:01 +10:00
psychedelicious
1ea8aafca1 fix(ui): use style preset prompts correctly 2024-08-23 19:46:05 +10:00
psychedelicious
533dd221f8 fix(ui): discard selected staging image not all other images 2024-08-23 19:46:05 +10:00
psychedelicious
2b325c6683 fix(ui): respect image size in staging preview 2024-08-23 19:46:05 +10:00
psychedelicious
3845b1b3e6 tidy(ui): cleanup after events change 2024-08-23 19:46:05 +10:00
psychedelicious
cea7890a67 feat(ui): move socket event handling out of redux
Download events and invocation status events (including progress images) are very frequent. There's no real need for these to pass through redux. Handling them outside redux is a significant performance win - far fewer store subscription calls, far fewer trips through middleware.

All event handling is moved outside middleware. Cleanup of unused actions and listeners to follow.
2024-08-23 19:46:05 +10:00
psychedelicious
c38fe8025d fix(ui): rebase conflicts 2024-08-23 19:46:05 +10:00
psychedelicious
f1de95349c fix(ui): update compositing rect when fill changes 2024-08-23 19:46:05 +10:00
psychedelicious
2950775fa7 feat(ui): add canvas background style 2024-08-23 19:46:05 +10:00
psychedelicious
cb293fd7ac feat(ui): mask layers choose own opacity 2024-08-23 19:46:05 +10:00
psychedelicious
43b3fab6be feat(ui): mask fill patterns 2024-08-23 19:46:05 +10:00
psychedelicious
d4b0dbce49 build(ui): add vite types to tsconfig 2024-08-23 19:46:05 +10:00
psychedelicious
137b810669 fix(ui): do not smooth pixel data when using eyeDropper 2024-08-23 19:46:05 +10:00
psychedelicious
c172657324 tidy(ui): tool components & translations 2024-08-23 19:46:05 +10:00
psychedelicious
97c966b04f feat(ui): rough out eyedropper tool
It's a bit slow bc we are converting the stage to canvas on every mouse move. Also need to improve the visual but it works.
2024-08-23 19:46:05 +10:00
psychedelicious
7178fc6253 fix(ui): ip adapters work 2024-08-23 19:46:05 +10:00
psychedelicious
4adb2eabf5 feat(ui): rename layers 2024-08-23 19:46:05 +10:00
psychedelicious
9f2c815e13 feat(ui): revise entity menus 2024-08-23 19:46:05 +10:00
psychedelicious
1435557d1d feat(ui): split control layers from raster layers for UI and internal state, same rendering as raster layers 2024-08-23 19:46:05 +10:00
psychedelicious
96abf687f6 feat(ui): implement cache for image rasterization, rip out some old controladapters code 2024-08-23 19:46:05 +10:00
psychedelicious
636d9a7209 feat(ui, app): use layer as control (wip) 2024-08-23 19:46:05 +10:00
psychedelicious
3b36eb0223 feat(ui): add contextmenu for canvas entities 2024-08-23 19:46:05 +10:00
psychedelicious
388c97bff0 feat(ui): more better logging & naming 2024-08-23 19:46:05 +10:00
psychedelicious
b1cb018695 feat(ui): better logging w/ path 2024-08-23 19:46:05 +10:00
psychedelicious
df78dd7953 feat(ui): always show marks on canvas scale slider 2024-08-23 19:46:05 +10:00
psychedelicious
0dc344a22e fix(ui): do not import button from chakra 2024-08-23 19:46:05 +10:00
psychedelicious
350d7f6f14 fix(ui): scaled bbox preview 2024-08-23 19:46:05 +10:00
psychedelicious
11059ee2d4 feat(ui): tidy up atoms 2024-08-23 19:46:05 +10:00
psychedelicious
c90d3f3bb9 feat(ui): convert all my pubsubs to atoms
its the same but better
2024-08-23 19:46:05 +10:00
psychedelicious
7f6d439fd1 feat(ui): add trnalsation 2024-08-23 19:46:05 +10:00
psychedelicious
783a78f069 fix(ui): give up on thumbnail loading, causes flash during transformer 2024-08-23 19:46:05 +10:00
psychedelicious
0ff031950d fix(ui): depth anything v2 2024-08-23 19:46:05 +10:00
psychedelicious
d7e8f3d756 tidy(ui): remove unused code, comments 2024-08-23 19:46:05 +10:00
psychedelicious
4668ea449b fix(ui): staging area works 2024-08-23 19:46:05 +10:00
psychedelicious
30d318d021 feat(nodes): temp disable canvas output crop 2024-08-23 19:46:05 +10:00
psychedelicious
de96f97e5f fix(ui): max scale 1 when reset view 2024-08-23 19:46:05 +10:00
psychedelicious
57c0a2dfb1 feat(ui): better scale changer component, reset view functionality 2024-08-23 19:46:05 +10:00
psychedelicious
cd4e464bde fix(ui): img2img 2024-08-23 19:46:05 +10:00
psychedelicious
49e48c3eb7 feat(ui): add manual scale controls 2024-08-23 19:46:05 +10:00
psychedelicious
edd3b3bce9 fix(ui): do not await clearBuffer 2024-08-23 19:46:04 +10:00
psychedelicious
f8bfb66108 feat(ui): dnd image into layer 2024-08-23 19:46:04 +10:00
psychedelicious
3b6a76cbf3 fix(ui): do not await commitBuffer 2024-08-23 19:46:04 +10:00
psychedelicious
e0b60e4320 fix(ui): properly destroy entities in manager cleanup 2024-08-23 19:46:04 +10:00
psychedelicious
2159319035 tidy(ui): clearer component names for regional guidance 2024-08-23 19:46:04 +10:00
psychedelicious
b170fc232e tidy(ui): clearer component names for ip adapter 2024-08-23 19:46:04 +10:00
psychedelicious
594da60f2f tidy(ui): clearer component names for inpaint mask 2024-08-23 19:46:04 +10:00
psychedelicious
6a432f6518 tidy(ui): clearer component names for control adapters 2024-08-23 19:46:04 +10:00
psychedelicious
eb8eacfec6 feat(ui): simplify canvas list item headers 2024-08-23 19:46:04 +10:00
psychedelicious
c8d04d42e2 fix(ui): ip adapter list item 2024-08-23 19:46:04 +10:00
psychedelicious
d39c9de81e tidy(ui): clean up unused logic 2024-08-23 19:46:04 +10:00
psychedelicious
a27d39b9ff feat(ui): clean up state, add mutex for image loading, add thumbnail loading 2024-08-23 19:46:04 +10:00
psychedelicious
6b385614f0 chore(ui): add async-mutex dep 2024-08-23 19:46:04 +10:00
psychedelicious
3ae7250ef7 feat(ui): txt2img, img2img, inpaint & outpaint working 2024-08-23 19:46:04 +10:00
psychedelicious
a42d0ce1d2 feat(ui): no padding on transformer outlines 2024-08-23 19:46:04 +10:00
psychedelicious
d9131f7563 feat(ui): restore object count to layer titles 2024-08-23 19:46:04 +10:00
psychedelicious
bdce958f29 tidy(ui): "useIsEntitySelected" -> "useEntityIsSelected" 2024-08-23 19:46:04 +10:00
psychedelicious
3c86f1e979 tidy(ui): move transformer statics into class 2024-08-23 19:46:04 +10:00
psychedelicious
894b8a29b9 tidy(ui): massive cleanup
- create a context for entity identifiers, massively simplifying UI for each entity int he list
- consolidate common redux actions
- remove now-unused code
2024-08-23 19:46:04 +10:00
psychedelicious
8436a44973 perf(ui): do not add duplicate points to lines 2024-08-23 19:46:04 +10:00
psychedelicious
f9f9ec3688 feat(ui): up line tension to 0.3 2024-08-23 19:46:04 +10:00
psychedelicious
5a98d7a1f6 perf(ui): disable stroke, perfect draw on compositing rect 2024-08-23 19:46:04 +10:00
psychedelicious
f9bc96e497 tidy(ui): remove unused code, initial image 2024-08-23 19:46:04 +10:00
psychedelicious
56350ff5dc tidy(ui): remove unused state & actions 2024-08-23 19:46:04 +10:00
psychedelicious
6c1139340c feat(ui): region mask rendering 2024-08-23 19:46:04 +10:00
psychedelicious
641b1a7e6f feat(ui): esc cancels drawing buffer
maybe this is not wanted? we'll see
2024-08-23 19:46:04 +10:00
psychedelicious
674a3f462f fix(ui): render transformer over objects, fix issue w/ inpaint rect color 2024-08-23 19:46:04 +10:00
psychedelicious
2283186d3a fix(ui): brush preview fill for inpaint/region 2024-08-23 19:46:04 +10:00
psychedelicious
340af1fe50 fix(ui): no objects rendered until vis toggled 2024-08-23 19:46:04 +10:00
psychedelicious
9378656d78 feat(ui): inpaint mask transform 2024-08-23 19:46:04 +10:00
psychedelicious
5f0413e222 fix(ui): layer accidental early set isFirstRender=false 2024-08-23 19:46:04 +10:00
psychedelicious
c3e47515b1 fix(ui): inpaint mask rendering 2024-08-23 19:46:04 +10:00
psychedelicious
5dcef6fa0d feat(ui): wip inpaint mask uses new API 2024-08-23 19:46:04 +10:00
psychedelicious
31ac02cd93 feat(ui): move updatePosition to transformer 2024-08-23 19:46:04 +10:00
psychedelicious
ab16976084 feat(ui): move resetScale to transformer 2024-08-23 19:46:04 +10:00
psychedelicious
8e2b7845e1 tidy(ui): more imperative naming 2024-08-23 19:46:04 +10:00
psychedelicious
3973bce342 tidy(ui): use imperative names for setters in stateapi 2024-08-23 19:46:04 +10:00
psychedelicious
f63847a504 fix(ui): commit drawing buffer on tool change, fixing bbox not calculating 2024-08-23 19:46:04 +10:00
psychedelicious
07e3529948 fix(ui): sync transformer when requesting bbox calc 2024-08-23 19:46:04 +10:00
psychedelicious
03e1c60694 tidy(ui): rename union CanvasEntity -> CanvasEntityState 2024-08-23 19:46:04 +10:00
psychedelicious
d766ed71fc fix(ui): request rect calc immediately on transform, hiding rect 2024-08-23 19:46:04 +10:00
psychedelicious
ae68ef142a feat(ui): move bbox calculation to transformer 2024-08-23 19:46:04 +10:00
psychedelicious
20f55768c4 feat(ui): use set for transformer subscriptions 2024-08-23 19:46:04 +10:00
psychedelicious
c4fad4456e tidy(ui): clean up worker tasks when complete 2024-08-23 19:46:04 +10:00
psychedelicious
78f5ec44ad tidy(ui): remove unused code in CanvasTool 2024-08-23 19:46:04 +10:00
psychedelicious
e14ba86942 feat(ui): use pubsub for isTransforming on manager 2024-08-23 19:46:04 +10:00
psychedelicious
d4e7720f6b docs(ui): update transformer docstrings 2024-08-23 19:46:04 +10:00
psychedelicious
a3f0e7e1cb feat(ui): revised event pubsub, transformer logic split out 2024-08-23 19:46:04 +10:00
psychedelicious
30a696c476 feat(ui): add simple pubsub 2024-08-23 19:46:04 +10:00
psychedelicious
66d6c64e16 feat(ui): document & clean up object renderer 2024-08-23 19:46:04 +10:00
psychedelicious
d15be9b57c feat(ui): split out object renderer 2024-08-23 19:46:04 +10:00
psychedelicious
e5da902fd0 fix(ui): unable to hold shit while transforming to retain ratio 2024-08-23 19:46:04 +10:00
psychedelicious
fc558094c2 tidy(ui): rename canvas stuff 2024-08-23 19:46:04 +10:00
psychedelicious
ad9312e989 tidy(ui): consolidate getLoggingContext builders 2024-08-23 19:46:04 +10:00
psychedelicious
8e1a70b008 fix(ui): align all tools to 1px grid
- Offset brush tool by 0.5px when width is odd, ensuring each stroke edge is exactly on a pixel boundary
- Round the rect tool also
2024-08-23 19:46:04 +10:00
psychedelicious
17f88cd5ad feat(ui): disable image smoothing on layers 2024-08-23 19:46:04 +10:00
psychedelicious
298f1919fa fix(ui): round position when rasterizing layer 2024-08-23 19:46:04 +10:00
psychedelicious
4d20cc11d4 feat(ui): continue modularizing transform 2024-08-23 19:46:04 +10:00
psychedelicious
14f249a2f0 feat(ui): fix a few things that didn't unsubscribe correctly, add helper to manage subscriptions 2024-08-23 19:46:04 +10:00
psychedelicious
b9746a6c2c feat(ui): merge bbox outline into transformer 2024-08-23 19:46:04 +10:00
psychedelicious
94f298a6f4 fix(ui): update parent's pos not transformers 2024-08-23 19:46:04 +10:00
psychedelicious
8d3a8178da feat(ui): merge interaction rect into transformer class 2024-08-23 19:46:04 +10:00
psychedelicious
cad4212fe8 feat(ui): prepare staging area 2024-08-23 19:46:04 +10:00
psychedelicious
cff28dfaa9 feat(ui): typing for logging context 2024-08-23 19:46:04 +10:00
psychedelicious
70d7509fcc feat(ui): remove inheritance of CanvasObject
JS is terrible
2024-08-23 19:46:04 +10:00
psychedelicious
cf83af7a27 feat(ui): split & document transformer logic, iterate on class structures 2024-08-23 19:46:04 +10:00
psychedelicious
5c5a405c0f feat(ui): rotation snap to nearest 45deg when holding shift 2024-08-23 19:46:04 +10:00
psychedelicious
0208e4b232 feat(ui): expose subscribe method for nanostores 2024-08-23 19:46:04 +10:00
psychedelicious
e940754795 tidy(ui): remove layer scaling reducers 2024-08-23 19:46:04 +10:00
psychedelicious
dc9fa1a735 fix(ui): pixel-perfect transforms 2024-08-23 19:46:04 +10:00
psychedelicious
08591fbf6d fix(ui): layer visibility toggle 2024-08-23 19:46:04 +10:00
psychedelicious
74db71bb5d fix(nodes): fix canvas mask erode
it wasn't eroding enough and caused incorrect transparency in result images
2024-08-23 19:46:04 +10:00
psychedelicious
60dbe798a5 fix(ui): do not reset layer on first render 2024-08-23 19:46:04 +10:00
psychedelicious
0e676605fe feat(ui): revised logging and naming setup, fix staging area 2024-08-23 19:46:04 +10:00
psychedelicious
3f781016f6 feat(ui): add repr methods to layer and object classes 2024-08-23 19:46:04 +10:00
psychedelicious
17cd2f6b02 feat(ui): use nanoid(10) instead of uuidv4 for canvas
Shorter ids makes it much more readable
2024-08-23 19:46:04 +10:00
psychedelicious
99102a1b34 build(ui): add nanoid as explicit dep 2024-08-23 19:46:04 +10:00
psychedelicious
8d72e7d9e8 fix(ui): move CanvasImage's konva image to correct object 2024-08-23 19:46:04 +10:00
psychedelicious
0b6b6f97ad fix(ui): prevent flash when applying transform 2024-08-23 19:46:04 +10:00
psychedelicious
fb2f6382b1 build(ui): add eslint rules for async stuff 2024-08-23 19:46:04 +10:00
psychedelicious
1ddea87c35 feat(ui): trying to fix flicker after transform 2024-08-23 19:46:04 +10:00
psychedelicious
ea02323095 feat(ui): transform cleanup 2024-08-23 19:46:04 +10:00
psychedelicious
49733091c7 feat(ui): fix transform when rotated 2024-08-23 19:46:04 +10:00
psychedelicious
cf833fd6e2 fix(ui): use pixel bbox when image is in layer 2024-08-23 19:46:04 +10:00
psychedelicious
ba5cf07ab8 fix(ui): transforming when axes flipped 2024-08-23 19:46:04 +10:00
psychedelicious
d15321a373 feat(ui): hallelujah (???) 2024-08-23 19:46:04 +10:00
psychedelicious
de597a5eb4 feat(ui): add debug button 2024-08-23 19:46:04 +10:00
psychedelicious
e5f5cbdf5c fix(ui): transformer padding 2024-08-23 19:46:04 +10:00
psychedelicious
7d4342bbff feat(ui): wip transform mode 2 2024-08-23 19:46:04 +10:00
psychedelicious
7f8a1d8d20 feat(ui): wip transform mode 2024-08-23 19:46:04 +10:00
psychedelicious
65353ac1e1 feat(ui): wip transform mode 2024-08-23 19:46:04 +10:00
psychedelicious
7f9a31ca4a fix(ui): dnd to canvas broke 2024-08-23 19:46:04 +10:00
psychedelicious
592eb2886c fix(ui): conflicts after rebasing 2024-08-23 19:46:04 +10:00
psychedelicious
c220dd8987 fix(ui): imageDropped listener 2024-08-23 19:46:04 +10:00
psychedelicious
a263beb0d5 wip 2024-08-23 19:46:04 +10:00
psychedelicious
46b7c510eb fix(ui): transform tool seems to be working 2024-08-23 19:46:04 +10:00
psychedelicious
f405e472ea fix(ui): move tool fixes, add transform tool 2024-08-23 19:46:04 +10:00
psychedelicious
7bdfd3ef5f feat(ui): move tool now only moves 2024-08-23 19:46:04 +10:00
psychedelicious
778ee2c679 feat(ui): layer bbox calc in worker 2024-08-23 19:46:04 +10:00
psychedelicious
e70339ff3e feat(ui): tweaked entity & group selection styles 2024-08-23 19:46:04 +10:00
psychedelicious
88c57a9750 feat(ui): canvas entity list headers 2024-08-23 19:46:04 +10:00
psychedelicious
137252128b tidy(ui): CanvasRegion 2024-08-23 19:46:04 +10:00
psychedelicious
d4297b1345 tidy(ui): CanvasRect 2024-08-23 19:46:04 +10:00
psychedelicious
6059bc7b47 tidy(ui): CanvasLayer 2024-08-23 19:46:04 +10:00
psychedelicious
c3ff3eb51f tidy(ui): CanvasInpaintMask 2024-08-23 19:46:04 +10:00
psychedelicious
0b7751c413 tidy(ui): CanvasInitialImage 2024-08-23 19:46:04 +10:00
psychedelicious
d7f1c30624 tidy(ui): CanvasImage 2024-08-23 19:46:04 +10:00
psychedelicious
3f4d7dbeea tidy(ui): CanvasEraserLine 2024-08-23 19:46:04 +10:00
psychedelicious
19b6ae2907 tidy(ui): CanvasControlAdapter 2024-08-23 19:46:04 +10:00
psychedelicious
769f96ff9f tidy(ui): CanvasBrushLine 2024-08-23 19:46:04 +10:00
psychedelicious
fdaf75faa4 tidy(ui): CanvasBbox 2024-08-23 19:46:04 +10:00
psychedelicious
1380bb7ae6 tidy(ui): CanvasBackground 2024-08-23 19:46:04 +10:00
psychedelicious
9483c8cc29 tidy(ui): update canvas classes, organise location of konva nodes 2024-08-23 19:46:04 +10:00
psychedelicious
2ef8a8cf5a feat(ui): add names to all konva objects
Makes troubleshooting much simpler
2024-08-23 19:46:04 +10:00
psychedelicious
d296ec1932 fix(ui): do not await creating new canvas image
If you await this, it causes a race condition where multiple images are created.
2024-08-23 19:46:04 +10:00
psychedelicious
444ad3dae1 feat(ui): use position and dimensions instead of separate x,y,width,height attrs 2024-08-23 19:46:04 +10:00
psychedelicious
8cdcc71378 fix(ui): remove weird rtkq hook wrapper
I do not understand why I did that initially but it doesn't work with TS.
2024-08-23 19:46:04 +10:00
psychedelicious
e8bc06cfd3 feat(ui): rename types size and position to dimensions and coordinate 2024-08-23 19:46:04 +10:00
psychedelicious
67a0a024e9 tidy(ui): hide layer settings by default 2024-08-23 19:46:04 +10:00
psychedelicious
bd2c46c267 fix(ui): layer rendering when starting as disabled 2024-08-23 19:46:04 +10:00
psychedelicious
5acb27f350 feat(invocation): reduce canvas v2 mask & crop mask dilation 2024-08-23 19:46:04 +10:00
psychedelicious
7271b12d0f feat(ui): de-jank staging area and progress images 2024-08-23 19:46:04 +10:00
psychedelicious
4a79467a33 feat(ui): update staging handling to work w/ cropped mask 2024-08-23 19:46:04 +10:00
psychedelicious
5501bb87a3 chore(ui): typegen 2024-08-23 19:46:04 +10:00
psychedelicious
561610e296 feat(app): update CanvasV2MaskAndCropInvocation 2024-08-23 19:46:04 +10:00
psychedelicious
b76609ef18 feat(ui): use new canvas output node 2024-08-23 19:46:04 +10:00
psychedelicious
070b78501b chore(ui): typegen 2024-08-23 19:46:04 +10:00
psychedelicious
50df4f4ab6 feat(app): add CanvasV2MaskAndCropInvocation & CanvasV2MaskAndCropOutput
This handles some masking and cropping that the canvas needs.
2024-08-23 19:46:04 +10:00
psychedelicious
9bbf430125 fix(ui): restore nodes output tracking 2024-08-23 19:46:04 +10:00
psychedelicious
384a90958a feat(ui): rip out document size
barely knew ye
2024-08-23 19:46:04 +10:00
psychedelicious
0e4a25b029 feat(ui): convert initial image to layer when starting canvas session 2024-08-23 19:46:04 +10:00
psychedelicious
4a44e171fd fix(ui): fix layer transparency calculation 2024-08-23 19:46:04 +10:00
psychedelicious
9bc57a6f59 fix(ui): reset initial image when resetting canvas 2024-08-23 19:46:03 +10:00
psychedelicious
4341ed7ab4 fix(ui): reset node executions states when loading workflow 2024-08-23 19:46:03 +10:00
psychedelicious
97ce72c542 fix(ui): entity display list 2024-08-23 19:46:03 +10:00
psychedelicious
a2c78a57a7 feat(ui): img2img working 2024-08-23 19:46:03 +10:00
psychedelicious
044a713dc9 feat(ui): rough out img2img on canvas 2024-08-23 19:46:03 +10:00
psychedelicious
b8479c5fe2 UNDO ME WIP 2024-08-23 19:46:03 +10:00
psychedelicious
4e5d056824 feat(ui): log invocation source id on socket event 2024-08-23 19:46:03 +10:00
psychedelicious
118278b372 feat(ui): restore document size overlay renderer 2024-08-23 19:46:03 +10:00
psychedelicious
8e8c255f3f feat(ui): make documnet size a rect 2024-08-23 19:46:03 +10:00
psychedelicious
1575bee401 refactor(ui): remove modular imagesize components
This is no longer necessary with canvas v2 and added a ton of extraneous redux actions when changing the image size. Also renamed to document size
2024-08-23 19:46:03 +10:00
psychedelicious
249bbfc883 feat(ui): initialState is for generation mode 2024-08-23 19:46:03 +10:00
psychedelicious
3993ae410f feat(ui): split out canvas entity list component 2024-08-23 19:46:03 +10:00
psychedelicious
edf040e3d2 feat(ui): hide bbox button when no canvas session active 2024-08-23 19:46:03 +10:00
psychedelicious
66fd077ee7 tidy(ui): remove unused naming objects/utils
The canvas manager means we don't need to worry about konva node names as we never directly select konva nodes.
2024-08-23 19:46:03 +10:00
psychedelicious
b93462ebb6 feat(ui): split up tool chooser buttons
Prep for distinct toolbars for generation vs canvas modes
2024-08-23 19:46:03 +10:00
psychedelicious
aae60d0cdc feat(ui): add useAssertSingleton util hook
This simple hook asserts that it is only ever called once. Particularly useful for things like hotkeys hooks.
2024-08-23 19:46:03 +10:00
psychedelicious
d4da00e607 feat(ui): "stagingArea" -> "session" 2024-08-23 19:46:03 +10:00
psychedelicious
0c539ff00b feat(ui): add reset button to canvas 2024-08-23 19:46:03 +10:00
psychedelicious
5983cbf26c feat(ui): add snapToRect util 2024-08-23 19:46:03 +10:00
psychedelicious
c513d6e3af fix(ui): fiddle with control adapter filters
some jank still
2024-08-23 19:46:03 +10:00
psychedelicious
9d57c0e631 feat(ui): temp disable doc size overlay 2024-08-23 19:46:03 +10:00
psychedelicious
a1923a8966 feat(ui): no animation on layer selection
Felt sluggish
2024-08-23 19:46:03 +10:00
psychedelicious
d988e18731 feat(ui): use canvas as source for control images (wip) 2024-08-23 19:46:03 +10:00
psychedelicious
51008da2dd fix(ui): control adapter translate & scale 2024-08-23 19:46:03 +10:00
psychedelicious
6ccc1f5672 tidy(ui): removed unused state related to non-buffered drawing 2024-08-23 19:46:03 +10:00
psychedelicious
4a556f84e0 feat(ui): control adapter image rendering 2024-08-23 19:46:03 +10:00
psychedelicious
2f21a2220d fix(ui): do not floor bbox calc, it cuts off the last pixels 2024-08-23 19:46:03 +10:00
psychedelicious
91a420b13e feat(ui): fix issue where creating line needs 2 points 2024-08-23 19:46:03 +10:00
psychedelicious
c27da3581b fix(ui): edge cases when holding shift and drawing lines 2024-08-23 19:46:03 +10:00
psychedelicious
961dfbce93 fix(ui): set buffered rect color to full alpha 2024-08-23 19:46:03 +10:00
psychedelicious
df39c825ae fix(ui): handle mouseup correctly 2024-08-23 19:46:03 +10:00
psychedelicious
3f6ee1b7a4 feat(ui): buffered rect drawing 2024-08-23 19:46:03 +10:00
psychedelicious
908e504a6f fix(ui): buffered drawing edge cases 2024-08-23 19:46:03 +10:00
psychedelicious
f2fa41afc5 perf(ui): do not use stage.find 2024-08-23 19:46:03 +10:00
psychedelicious
440ff40ad5 perf(ui): object groups do not listen 2024-08-23 19:46:03 +10:00
psychedelicious
5c15458e15 perf(ui): buffered drawing (wip) 2024-08-23 19:46:03 +10:00
psychedelicious
be5b474f1e tidy(ui): organise files 2024-08-23 19:46:03 +10:00
psychedelicious
cee178c2b6 tidy(ui): organise files 2024-08-23 19:46:03 +10:00
psychedelicious
27657f8b7a tidy(ui): organise files 2024-08-23 19:46:03 +10:00
psychedelicious
e0cde3815a fix(ui): background rendering 2024-08-23 19:46:03 +10:00
psychedelicious
09d0421de4 pkg(ui): remove unused deps react-konva & use-image 2024-08-23 19:46:03 +10:00
psychedelicious
47b94d563c feat(ui): organize konva state and files 2024-08-23 19:46:03 +10:00
psychedelicious
0b5d20c9f0 fix(ui): merge conflicts in image deletion listener 2024-08-23 19:46:03 +10:00
psychedelicious
80e7e1293a fix(ui): region rendering 2024-08-23 19:46:03 +10:00
psychedelicious
3a82b0cbc1 fix(ui): inpaint mask rendering 2024-08-23 19:46:03 +10:00
psychedelicious
a27cbc13b6 fix(ui): staging area rendering 2024-08-23 19:46:03 +10:00
psychedelicious
a8f962eb3f fix(ui): stale selected entity 2024-08-23 19:46:03 +10:00
psychedelicious
7f40d23f19 fix(ui): staging area image offset 2024-08-23 19:46:03 +10:00
psychedelicious
918354cd9d feat(ui): tweak layer ui component 2024-08-23 19:46:03 +10:00
psychedelicious
eef9278ee6 fix(ui): resetting layer resets position 2024-08-23 19:46:03 +10:00
psychedelicious
2c32e2e5c1 feat(ui): updated layer list component styling 2024-08-23 19:46:03 +10:00
psychedelicious
6f05654db5 feat(ui): transformable layers 2024-08-23 19:46:03 +10:00
psychedelicious
1d31b6902f feat(ui): move tool icon is pointer like in other apps 2024-08-23 19:46:03 +10:00
psychedelicious
5a7d615e64 feat(ui): do not floor cursor position 2024-08-23 19:46:03 +10:00
psychedelicious
1dbf9e4ed4 feat(ui): disable gallery hotkeys while staging 2024-08-23 19:46:03 +10:00
psychedelicious
5dcc6ee203 feat(ui): revised canvas progress & staging image handling 2024-08-23 19:46:03 +10:00
psychedelicious
84a4e6ae3f feat(ui): show queue item origin in queue list 2024-08-23 19:46:03 +10:00
psychedelicious
f283bfd68f chore(ui): typegen 2024-08-23 19:46:03 +10:00
psychedelicious
6e5ff7b79c feat(app): add origin to session queue
The origin is an optional field indicating the queue item's origin. For example, "canvas" when the queue item originated from the canvas or "workflows" when the queue item originated from the workflows tab. If omitted, we assume the queue item originated from the API directly.

- Add migration to add the nullable column to the `session_queue` table.
- Update relevant event payloads with the new field.
- Add `cancel_by_origin` method to `session_queue` service and corresponding route. This is required for the canvas to bail out early when staging images.
- Add `origin` to both `SessionQueueItem` and `Batch` - it needs to be provided initially via the batch and then passed onto the queue item.
-
2024-08-23 19:46:03 +10:00
psychedelicious
7c3800d03f fix(ui): denoise start on outpainting 2024-08-23 19:46:03 +10:00
psychedelicious
941db90518 feat(ui): add redux events for queue cleared & batch enqueued socket events 2024-08-23 19:46:03 +10:00
psychedelicious
0d9ecf0f90 feat(ui): canvas staging area works 2024-08-23 19:46:03 +10:00
psychedelicious
9c77023a11 feat(ui): switch to view tool when staging 2024-08-23 19:46:03 +10:00
psychedelicious
b55378c63c tidy(ui): disable preview images on every enqueue 2024-08-23 19:46:03 +10:00
psychedelicious
946c2a49ab feat(ui): rough out save staging image 2024-08-23 19:46:03 +10:00
psychedelicious
b823c31ec6 feat(ui): staging area image visibility toggle 2024-08-23 19:46:03 +10:00
psychedelicious
ec6361e5cb fix(ui): batch building after removing canvas files 2024-08-23 19:46:03 +10:00
psychedelicious
0c26d28278 feat(ui): make Graph class's getMetadataNode public 2024-08-23 19:46:03 +10:00
psychedelicious
c5172d4c5a tidy(ui): remove old canvas graphs 2024-08-23 19:46:03 +10:00
psychedelicious
89de04775e fix(ui): do not select already-selected entity 2024-08-23 19:46:03 +10:00
psychedelicious
b4c3c940b5 tidy(ui): naming things 2024-08-23 19:46:03 +10:00
psychedelicious
aee2aad959 tidy(ui): file organisation 2024-08-23 19:46:03 +10:00
psychedelicious
5ca48a8a5f fix(ui): reset cursor pos when fitting document 2024-08-23 19:46:03 +10:00
psychedelicious
1806aa187b feat(ui): staging area works more better 2024-08-23 19:46:03 +10:00
psychedelicious
7824cb7a1a feat(ui): staging area barely works 2024-08-23 19:46:03 +10:00
psychedelicious
9807a896f4 feat(ui): consolidate konva API 2024-08-23 19:46:03 +10:00
psychedelicious
19866f057d feat(ui): consolidate konva API 2024-08-23 19:46:03 +10:00
psychedelicious
ec4eae3c9c feat(ui): staging area (rendering wip) 2024-08-23 19:46:03 +10:00
psychedelicious
bea0cba038 tidy(ui): type "Dimensions" -> "Size" 2024-08-23 19:46:03 +10:00
psychedelicious
48ee75af9c feat(ui): add updateNode to Graph 2024-08-23 19:46:03 +10:00
psychedelicious
929c593d2f feat(ui): sdxl graphs 2024-08-23 19:46:03 +10:00
psychedelicious
221f32eca7 feat(ui): sd1 outpaint graph 2024-08-23 19:46:03 +10:00
psychedelicious
c3acc15e8b tests(ui): add missing tests for Graph class 2024-08-23 19:46:03 +10:00
psychedelicious
1b653278fc feat(ui): add Graph.getid() util 2024-08-23 19:46:03 +10:00
psychedelicious
cc9062ee46 feat(ui): outpaint graph, organize builder a bit 2024-08-23 19:46:03 +10:00
psychedelicious
91c0feb0ad feat(ui): inpaint sd1 graph 2024-08-23 19:46:03 +10:00
psychedelicious
ae60292ac8 feat(ui): temp disable image caching while testing 2024-08-23 19:46:03 +10:00
psychedelicious
a6ca17b19d feat(ui): txt2img & img2img graphs 2024-08-23 19:46:03 +10:00
psychedelicious
6a4a5ece74 feat(ui): minor change to canvas bbox state type 2024-08-23 19:46:03 +10:00
psychedelicious
9b81860307 feat(ui): simplified konva node to blob/imagedata utils 2024-08-23 19:46:03 +10:00
psychedelicious
5f4a3928d2 feat(ui): node manager getter/setter 2024-08-23 19:46:03 +10:00
psychedelicious
b703884763 feat(ui): generation mode calculation, fudged graphs 2024-08-23 19:46:03 +10:00
psychedelicious
32da98ab8f feat(ui): add utils for getting images from canvas 2024-08-23 19:46:03 +10:00
psychedelicious
bd5a85bf70 feat(ui): even more simplified API - lean on the konva node manager to abstract imperative state API & rendering 2024-08-23 19:46:03 +10:00
psychedelicious
d045f24014 feat(ui): revised docstrings for renderers & simplified api 2024-08-23 19:46:03 +10:00
psychedelicious
2aad3f89c3 feat(ui): inpaint mask UI components 2024-08-23 19:46:03 +10:00
psychedelicious
dd54d19f00 feat(ui): inpaint mask rendering (wip) 2024-08-23 19:46:03 +10:00
psychedelicious
0ed6591d8c fix(ui): models loaded handler 2024-08-23 19:46:03 +10:00
psychedelicious
712e090134 feat(ui): internal state for inpaint mask 2024-08-23 19:46:03 +10:00
psychedelicious
8fc2a1d1cf refactor(ui): divvy up canvas state a bit 2024-08-23 19:46:03 +10:00
psychedelicious
cc15c1593e feat(ui): get region and base layer canvas to blob logic working 2024-08-23 19:46:03 +10:00
psychedelicious
9997d3abda refactor(ui): node manager handles more tedious annoying stuff 2024-08-23 19:46:03 +10:00
psychedelicious
031471e785 feat(ui): use node manager for addRegions 2024-08-23 19:46:03 +10:00
psychedelicious
2e860c6791 feat(ui): persist bbox 2024-08-23 19:46:03 +10:00
psychedelicious
d071a9e17d fix(ui): fix generation graphs 2024-08-23 19:46:03 +10:00
psychedelicious
ed53d33321 feat(ui): add toggle for clipToBbox 2024-08-23 19:46:03 +10:00
psychedelicious
382bc6d978 feat(ui): rename konva node manager 2024-08-23 19:46:03 +10:00
psychedelicious
dab42e258c refactor(ui): create classes to abstract mgmt of konva nodes 2024-08-23 19:46:03 +10:00
psychedelicious
81556410bb tidy(ui): organise renderers 2024-08-23 19:46:03 +10:00
psychedelicious
1f2dfd473c refactor(ui): create entity to konva node map abstraction (wip)
Instead of chaining konva `find` and `findOne` methods, all konva nodes are added to a mapping object. Finding and manipulating them is much simpler.

Done for regions and layers, wip for control adapters.
2024-08-23 19:46:03 +10:00
psychedelicious
8f0f51be2c perf(ui): fix lag w/ region rendering
Needed to memoize these selectors
2024-08-23 19:46:03 +10:00
psychedelicious
7179e250ed feat(ui): move canvas fill color picker to right 2024-08-23 19:46:03 +10:00
psychedelicious
5bec091fd6 refactor(ui): remove unused ellipse & polygon objects 2024-08-23 19:46:03 +10:00
psychedelicious
2c5896cb0c fix(ui): incorrect rect/brush/eraser positions 2024-08-23 19:46:03 +10:00
psychedelicious
93ff252dc0 refactor(ui): enable global debugging flag 2024-08-23 19:46:03 +10:00
psychedelicious
ac52224455 refactor(ui): disable the preview renderer for now 2024-08-23 19:46:03 +10:00
psychedelicious
4087cad23d tweak(ui): canvas editor layout 2024-08-23 19:46:03 +10:00
psychedelicious
e936b1ff8f perf(ui): memoize layeractionsmenu valid actions 2024-08-23 19:46:03 +10:00
psychedelicious
b7f9c5e221 refactor(ui): decouple konva renderer from react
Subscribe to redux store directly, skipping all the react overhead.

With react in dev mode, a typical frame while using the brush tool on almost-empty canvas is reduced from ~7.5ms to ~3.5ms. All things considered, this still feels slow, but it's a massive improvement.
2024-08-23 19:46:03 +10:00
psychedelicious
fc5467150e feat(ui): clip lines to bbox 2024-08-23 19:46:03 +10:00
psychedelicious
4dcab357a0 fix(ui): document fit positioning 2024-08-23 19:46:03 +10:00
psychedelicious
695e464255 feat(ui): document bounds overlay 2024-08-23 19:46:03 +10:00
psychedelicious
9999b60c3b tidy(ui): background layer 2024-08-23 19:46:03 +10:00
psychedelicious
e7df53e260 refactor(ui): use "entity" instead of "data" for canvas 2024-08-23 19:46:03 +10:00
psychedelicious
844590a571 feat(ui): brush size border radius = 1 2024-08-23 19:46:03 +10:00
psychedelicious
9622beaa0d fix(ui): canvas HUD doesn't interrupt tool 2024-08-23 19:46:03 +10:00
psychedelicious
007e2553a8 refactor(ui): split up canvas entity renderers, temp disable preview 2024-08-23 19:46:03 +10:00
psychedelicious
15ad4e3f5e fix(ui): delete all layers button 2024-08-23 19:46:03 +10:00
psychedelicious
be5094fcb4 fix(ui): ignore keyboard shortcuts in input/textarea elements 2024-08-23 19:46:03 +10:00
psychedelicious
a20a861680 fix(ui): canvas entity ids getting clobbered 2024-08-23 19:46:03 +10:00
psychedelicious
396d0a4bc0 fix(ui): move lora followup fixes 2024-08-23 19:46:03 +10:00
psychedelicious
ca9314e077 chore(ui): lint 2024-08-23 19:46:03 +10:00
psychedelicious
4b848798e7 refactor(ui): move loras to canvas slice 2024-08-23 19:46:03 +10:00
psychedelicious
083bcbc77d fix(ui): layer is selected when added 2024-08-23 19:46:03 +10:00
psychedelicious
e8cdc9ae62 feat(ui): r to center & fit stage on document 2024-08-23 19:46:03 +10:00
psychedelicious
8abfa759a4 feat(ui): better HUD 2024-08-23 19:46:03 +10:00
psychedelicious
f6a324b633 fix(ui): always use current brush width when making straight lines 2024-08-23 19:46:03 +10:00
psychedelicious
f083be9391 feat(ui): hold shift w/ brush to draw straight line 2024-08-23 19:46:03 +10:00
psychedelicious
091e2fb751 fix(ui): update bg on canvas resize 2024-08-23 19:46:03 +10:00
psychedelicious
d8539daf1f refactor(ui): better hud 2024-08-23 19:46:03 +10:00
psychedelicious
7ec059f5fa refactor(ui): scaled tool preview border 2024-08-23 19:46:03 +10:00
psychedelicious
4f2ecdefd2 refactor(ui): port remaining canvasV1 rendering logic to V2, remove old code 2024-08-23 19:46:03 +10:00
psychedelicious
e8891a1988 refactor(ui): fix more types 2024-08-23 19:46:03 +10:00
psychedelicious
37d2607f34 refactor(ui): metadata recall (wip)
just enough let the app run
2024-08-23 19:46:03 +10:00
psychedelicious
0e7b10d3d9 refactor(ui): undo/redo button temp fix 2024-08-23 19:46:03 +10:00
psychedelicious
1f85888638 refactor(ui): fix renderer stuff 2024-08-23 19:46:03 +10:00
psychedelicious
c1f9a129fa refactor(ui): fix misc types 2024-08-23 19:46:03 +10:00
psychedelicious
7ccc5ba398 refactor(ui): fix gallery stuff 2024-08-23 19:46:03 +10:00
psychedelicious
5e1a6ae334 refactor(ui): fix delete image stuff 2024-08-23 19:46:03 +10:00
psychedelicious
3f6cf638f9 refactor(ui): fix useIsReadyToEnqueue for new adapterType field 2024-08-23 19:46:03 +10:00
psychedelicious
46e062a828 refactor(ui): update generation tab graphs 2024-08-23 19:46:02 +10:00
psychedelicious
cc3a0b5d6c refactor(ui): add adapterType to ControlAdapterData 2024-08-23 19:46:02 +10:00
psychedelicious
775479ab7b refactor(ui): update components & logic to use new unified slice (again) 2024-08-23 19:46:02 +10:00
psychedelicious
6b9e0e6d63 refactor(ui): update components & logic to use new unified slice 2024-08-23 19:46:02 +10:00
psychedelicious
83a5c87f5e refactor(ui): merge compositing, params into canvasV2 slice 2024-08-23 19:46:02 +10:00
psychedelicious
84fde74331 refactor(ui): add scaled bbox state 2024-08-23 19:46:02 +10:00
psychedelicious
a517e29b39 refactor(ui): update dnd/image upload 2024-08-23 19:46:02 +10:00
psychedelicious
ccceba7565 refactor(ui): update size/prompts state 2024-08-23 19:46:02 +10:00
psychedelicious
5fc7a03669 refactor(ui): rip out old control adapter implementation 2024-08-23 19:46:02 +10:00
psychedelicious
8864ad1b50 refactor(ui): canvas v2 (wip)
fix entity count select
2024-08-23 19:46:02 +10:00
psychedelicious
f2989885fb refactor(ui): canvas v2 (wip)
delete unused file
2024-08-23 19:46:02 +10:00
psychedelicious
19c66e5c76 refactor(ui): canvas v2 (wip)
merge all canvas state reducers into one big slice (but with the logic split across files so it's not hell)
2024-08-23 19:46:02 +10:00
psychedelicious
8a6690a57c refactor(ui): canvas v2 (wip)
Fix a few more components
2024-08-23 19:46:02 +10:00
psychedelicious
2cc60f253a refactor(ui): canvas v2 (wip)
missed a spot
2024-08-23 19:46:02 +10:00
psychedelicious
cb69872dd3 refactor(ui): canvas v2 (wip)
Redo all UI components for different canvas entity types
2024-08-23 19:46:02 +10:00
psychedelicious
ba66d7c9a6 refactor(ui): canvas v2 (wip) 2024-08-23 19:46:02 +10:00
psychedelicious
9fe727c9f8 refactor(ui): canvas v2 (wip) 2024-08-23 19:46:02 +10:00
psychedelicious
58c656224f refactor(ui): canvas v2 (wip) 2024-08-23 19:46:02 +10:00
psychedelicious
c51253f5f6 refactor(ui): canvas v2 (wip) 2024-08-23 19:46:02 +10:00
psychedelicious
6c1d1588fc feat(ui): bbox tool 2024-08-23 19:46:02 +10:00
psychedelicious
95d6183a6c fix(ui): rect tool preview 2024-08-23 19:46:02 +10:00
psychedelicious
f4c9facdaf fix(ui): multiple stages 2024-08-23 19:46:02 +10:00
psychedelicious
a274e6f165 feat(ui): decouple konva logic from nanostores 2024-08-23 19:46:02 +10:00
psychedelicious
154e3e6f64 feat(ui): store all stage attrs in nanostores 2024-08-23 19:46:02 +10:00
psychedelicious
2c3ac972e5 feat(ui): round stage scale 2024-08-23 19:46:02 +10:00
psychedelicious
2e2e072b0b chore(ui): bump konva 2024-08-23 19:46:02 +10:00
psychedelicious
9d8dd2bf66 feat(ui): generation bbox transformation working
whew
2024-08-23 19:46:02 +10:00
psychedelicious
9047f6db30 feat(ui): wip generation bbox 2024-08-23 19:46:02 +10:00
psychedelicious
5ab345ee63 feat(ui): wip generation bbox 2024-08-23 19:46:02 +10:00
psychedelicious
d8a83acd3a feat(ui): CL zoom and pan, some rendering optimizations 2024-08-23 19:46:02 +10:00
psychedelicious
1f58e5756b Revert "feat(ui): add x,y,scaleX,scaleY,rotation to objects"
This reverts commit 53318b396c967c72326a7e4dea09667b2ab20bdd.
2024-08-23 19:46:02 +10:00
psychedelicious
744acb8f07 feat(ui): layers manage their own bbox 2024-08-23 19:46:02 +10:00
psychedelicious
ae7228d821 docs(ui): konva image object docstrings 2024-08-23 19:46:02 +10:00
psychedelicious
99d8b3a7bf feat(ui): add x,y,scaleX,scaleY,rotation to objects 2024-08-23 19:46:02 +10:00
psychedelicious
3fbe65bbcf fix(ui): show color picker when using rect tool 2024-08-23 19:46:02 +10:00
psychedelicious
f5d879d8e7 feat(ui): image loading fallback for raster layers 2024-08-23 19:46:02 +10:00
psychedelicious
cbc5a4f8e6 feat(ui): bbox calc for raster layers 2024-08-23 19:46:02 +10:00
psychedelicious
37ac7d8ed5 feat(ui): do not fill brush preview when drawing 2024-08-23 19:46:02 +10:00
psychedelicious
bee3fa339d fix(ui): brush spacing handling 2024-08-23 19:46:02 +10:00
psychedelicious
c171fe2b96 fix(ui): jank when starting a shape when not already focused on stage 2024-08-23 19:46:02 +10:00
psychedelicious
1fa8032fdb feat(ui): wip raster layers
I meant to split this up into smaller commits and undo some of it, but I committed afterwards and it's tedious to undo.
2024-08-23 19:46:02 +10:00
psychedelicious
5c0676bcc2 feat(ui): support image objects on raster layers
Just the UI and internal state, not rendering yet.
2024-08-23 19:46:02 +10:00
psychedelicious
cefd9a027c tidy(ui): clean up event handlers
Separate logic for each tool in preparation for ellipse and polygon tools.
2024-08-23 19:46:02 +10:00
psychedelicious
1bce156de1 feat(ui): raster layer reset, object group util 2024-08-23 19:46:02 +10:00
psychedelicious
cd4f63f2fd feat(ui): rect shape preview now has fill 2024-08-23 19:46:02 +10:00
psychedelicious
3c7140cbf3 feat(ui): cancel shape drawing on esc 2024-08-23 19:46:02 +10:00
psychedelicious
b71ba63b5a feat(ui): temp disable history on CL 2024-08-23 19:46:02 +10:00
psychedelicious
d540e2c0d3 feat(ui): raster layer logic
- Deduplicate shared logic
- Split up giant renderers file into separate cohesive files
- Tons of cleanup
- Progress on raster layer functionality
2024-08-23 19:46:02 +10:00
psychedelicious
d79fafc5f5 feat(ui): add raster layer rendering and interaction (WIP) 2024-08-23 19:46:02 +10:00
psychedelicious
9e93fa2092 feat(ui): scaffold out raster layers
Raster layers may have images, lines and shapes. These will replace initial image layers and provide sketching functionality like we have on canvas.
2024-08-23 19:46:02 +10:00
psychedelicious
392e9b4882 refactor(ui): revise types for line and rect objects
- Create separate object types for brush and eraser lines, instead of a single type that has a `tool` field.
- Create new object type for rect shapes.
- Add logic to schemas to migrate old object types to new.
- Update renderers & reducers.
2024-08-23 19:46:02 +10:00
678 changed files with 18977 additions and 28307 deletions

View File

@@ -11,6 +11,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
Batch,
BatchStatus,
CancelByBatchIDsResult,
CancelByOriginResult,
ClearResult,
EnqueueBatchResult,
PruneResult,
@@ -105,6 +106,19 @@ async def cancel_by_batch_ids(
return ApiDependencies.invoker.services.session_queue.cancel_by_batch_ids(queue_id=queue_id, batch_ids=batch_ids)
@session_queue_router.put(
"/{queue_id}/cancel_by_origin",
operation_id="cancel_by_origin",
responses={200: {"model": CancelByBatchIDsResult}},
)
async def cancel_by_origin(
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:
"""Immediately cancels all queue items with the given origin"""
return ApiDependencies.invoker.services.session_queue.cancel_by_origin(queue_id=queue_id, origin=origin)
@session_queue_router.put(
"/{queue_id}/clear",
operation_id="clear",

View File

@@ -6,13 +6,19 @@ import cv2
import numpy
from PIL import Image, ImageChops, ImageFilter, ImageOps
from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
from invokeai.app.invocations.baseinvocation import (
BaseInvocation,
Classification,
invocation,
invocation_output,
)
from invokeai.app.invocations.constants import IMAGE_MODES
from invokeai.app.invocations.fields import (
ColorField,
FieldDescriptions,
ImageField,
InputField,
OutputField,
WithBoard,
WithMetadata,
)
@@ -1007,3 +1013,66 @@ class MaskFromIDInvocation(BaseInvocation, WithMetadata, WithBoard):
image_dto = context.images.save(image=mask, image_category=ImageCategory.MASK)
return ImageOutput.build(image_dto)
@invocation_output("canvas_v2_mask_and_crop_output")
class CanvasV2MaskAndCropOutput(ImageOutput):
offset_x: int = OutputField(description="The x offset of the image, after cropping")
offset_y: int = OutputField(description="The y offset of the image, after cropping")
@invocation(
"canvas_v2_mask_and_crop",
title="Canvas V2 Mask and Crop",
tags=["image", "mask", "id"],
category="image",
version="1.0.0",
classification=Classification.Prototype,
)
class CanvasV2MaskAndCropInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Handles Canvas V2 image output masking and cropping"""
image: ImageField = InputField(description="The image to apply the mask to")
mask: ImageField = InputField(description="The mask to apply")
mask_blur: int = InputField(default=0, ge=0, description="The amount to blur the mask by")
def _prepare_mask(self, mask: Image.Image) -> Image.Image:
mask_array = numpy.array(mask)
kernel = numpy.ones((self.mask_blur, self.mask_blur), numpy.uint8)
dilated_mask_array = cv2.erode(mask_array, kernel, iterations=3)
dilated_mask = Image.fromarray(dilated_mask_array)
if self.mask_blur > 0:
mask = dilated_mask.filter(ImageFilter.GaussianBlur(self.mask_blur))
return ImageOps.invert(mask.convert("L"))
def invoke(self, context: InvocationContext) -> CanvasV2MaskAndCropOutput:
image = context.images.get_pil(self.image.image_name)
mask = self._prepare_mask(context.images.get_pil(self.mask.image_name))
image.putalpha(mask)
# bbox = image.getbbox()
# image = image.crop(bbox)
image_dto = context.images.save(image=image)
return CanvasV2MaskAndCropOutput(
image=ImageField(image_name=image_dto.image_name),
offset_x=0,
offset_y=0,
width=image.width,
height=image.height,
)
# def invoke(self, context: InvocationContext) -> CanvasV2MaskAndCropOutput:
# image = context.images.get_pil(self.image.image_name)
# mask = self._prepare_mask(context.images.get_pil(self.mask.image_name))
# image.putalpha(mask)
# bbox = image.getbbox()
# image = image.crop(bbox)
# image_dto = context.images.save(image=image)
# return CanvasV2MaskAndCropOutput(
# image=ImageField(image_name=image_dto.image_name),
# offset_x=bbox[0],
# offset_y=bbox[1],
# width=image.width,
# height=image.height,
# )

View File

@@ -88,6 +88,7 @@ class QueueItemEventBase(QueueEventBase):
item_id: int = Field(description="The ID of the queue item")
batch_id: str = Field(description="The ID of the queue batch")
origin: str | None = Field(default=None, description="The origin of the batch")
class InvocationEventBase(QueueItemEventBase):
@@ -95,8 +96,6 @@ class InvocationEventBase(QueueItemEventBase):
session_id: str = Field(description="The ID of the session (aka graph execution state)")
queue_id: str = Field(description="The ID of the queue")
item_id: int = Field(description="The ID of the queue item")
batch_id: str = Field(description="The ID of the queue batch")
session_id: str = Field(description="The ID of the session (aka graph execution state)")
invocation: AnyInvocation = Field(description="The ID of the invocation")
invocation_source_id: str = Field(description="The ID of the prepared invocation's source node")
@@ -114,6 +113,7 @@ class InvocationStartedEvent(InvocationEventBase):
queue_id=queue_item.queue_id,
item_id=queue_item.item_id,
batch_id=queue_item.batch_id,
origin=queue_item.origin,
session_id=queue_item.session_id,
invocation=invocation,
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
@@ -147,6 +147,7 @@ class InvocationDenoiseProgressEvent(InvocationEventBase):
queue_id=queue_item.queue_id,
item_id=queue_item.item_id,
batch_id=queue_item.batch_id,
origin=queue_item.origin,
session_id=queue_item.session_id,
invocation=invocation,
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
@@ -184,6 +185,7 @@ class InvocationCompleteEvent(InvocationEventBase):
queue_id=queue_item.queue_id,
item_id=queue_item.item_id,
batch_id=queue_item.batch_id,
origin=queue_item.origin,
session_id=queue_item.session_id,
invocation=invocation,
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
@@ -216,6 +218,7 @@ class InvocationErrorEvent(InvocationEventBase):
queue_id=queue_item.queue_id,
item_id=queue_item.item_id,
batch_id=queue_item.batch_id,
origin=queue_item.origin,
session_id=queue_item.session_id,
invocation=invocation,
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
@@ -253,6 +256,7 @@ class QueueItemStatusChangedEvent(QueueItemEventBase):
queue_id=queue_item.queue_id,
item_id=queue_item.item_id,
batch_id=queue_item.batch_id,
origin=queue_item.origin,
session_id=queue_item.session_id,
status=queue_item.status,
error_type=queue_item.error_type,
@@ -279,12 +283,14 @@ class BatchEnqueuedEvent(QueueEventBase):
description="The number of invocations initially requested to be enqueued (may be less than enqueued if queue was full)"
)
priority: int = Field(description="The priority of the batch")
origin: str | None = Field(default=None, description="The origin of the batch")
@classmethod
def build(cls, enqueue_result: EnqueueBatchResult) -> "BatchEnqueuedEvent":
return cls(
queue_id=enqueue_result.queue_id,
batch_id=enqueue_result.batch.batch_id,
origin=enqueue_result.batch.origin,
enqueued=enqueue_result.enqueued,
requested=enqueue_result.requested,
priority=enqueue_result.priority,

View File

@@ -6,6 +6,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
Batch,
BatchStatus,
CancelByBatchIDsResult,
CancelByOriginResult,
CancelByQueueIDResult,
ClearResult,
EnqueueBatchResult,
@@ -95,6 +96,11 @@ class SessionQueueBase(ABC):
"""Cancels all queue items with matching batch IDs"""
pass
@abstractmethod
def cancel_by_origin(self, queue_id: str, origin: str) -> CancelByOriginResult:
"""Cancels all queue items with the given batch origin"""
pass
@abstractmethod
def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
"""Cancels all queue items with matching queue ID"""

View File

@@ -77,6 +77,7 @@ BatchDataCollection: TypeAlias = list[list[BatchDatum]]
class Batch(BaseModel):
batch_id: str = Field(default_factory=uuid_string, description="The ID of the batch")
origin: str | None = Field(default=None, description="The origin of this batch.")
data: Optional[BatchDataCollection] = Field(default=None, description="The batch data collection.")
graph: Graph = Field(description="The graph to initialize the session with")
workflow: Optional[WorkflowWithoutID] = Field(
@@ -195,6 +196,7 @@ class SessionQueueItemWithoutGraph(BaseModel):
status: QUEUE_ITEM_STATUS = Field(default="pending", description="The status of this queue item")
priority: int = Field(default=0, description="The priority of this queue item")
batch_id: str = Field(description="The ID of the batch associated with this queue item")
origin: str | None = Field(default=None, description="The origin of this queue item. ")
session_id: str = Field(
description="The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed."
)
@@ -294,6 +296,7 @@ class SessionQueueStatus(BaseModel):
class BatchStatus(BaseModel):
queue_id: str = Field(..., description="The ID of the queue")
batch_id: str = Field(..., description="The ID of the batch")
origin: str | None = Field(..., description="The origin of the batch")
pending: int = Field(..., description="Number of queue items with status 'pending'")
in_progress: int = Field(..., description="Number of queue items with status 'in_progress'")
completed: int = Field(..., description="Number of queue items with status 'complete'")
@@ -328,6 +331,12 @@ class CancelByBatchIDsResult(BaseModel):
canceled: int = Field(..., description="Number of queue items canceled")
class CancelByOriginResult(BaseModel):
"""Result of canceling by list of batch ids"""
canceled: int = Field(..., description="Number of queue items canceled")
class CancelByQueueIDResult(CancelByBatchIDsResult):
"""Result of canceling by queue id"""
@@ -433,6 +442,7 @@ class SessionQueueValueToInsert(NamedTuple):
field_values: Optional[str] # field_values json
priority: int # priority
workflow: Optional[str] # workflow json
origin: str | None
ValuesToInsert: TypeAlias = list[SessionQueueValueToInsert]
@@ -453,6 +463,7 @@ def prepare_values_to_insert(queue_id: str, batch: Batch, priority: int, max_new
json.dumps(field_values, default=to_jsonable_python) if field_values else None, # field_values (json)
priority, # priority
json.dumps(workflow, default=to_jsonable_python) if workflow else None, # workflow (json)
batch.origin, # origin
)
)
return values_to_insert

View File

@@ -10,6 +10,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
Batch,
BatchStatus,
CancelByBatchIDsResult,
CancelByOriginResult,
CancelByQueueIDResult,
ClearResult,
EnqueueBatchResult,
@@ -127,8 +128,8 @@ class SqliteSessionQueue(SessionQueueBase):
self.__cursor.executemany(
"""--sql
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow)
VALUES (?, ?, ?, ?, ?, ?, ?)
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
values_to_insert,
)
@@ -417,11 +418,7 @@ class SqliteSessionQueue(SessionQueueBase):
)
self.__conn.commit()
if current_queue_item is not None and current_queue_item.batch_id in batch_ids:
batch_status = self.get_batch_status(queue_id=queue_id, batch_id=current_queue_item.batch_id)
queue_status = self.get_queue_status(queue_id=queue_id)
self.__invoker.services.events.emit_queue_item_status_changed(
current_queue_item, batch_status, queue_status
)
self._set_queue_item_status(current_queue_item.item_id, "canceled")
except Exception:
self.__conn.rollback()
raise
@@ -429,6 +426,46 @@ class SqliteSessionQueue(SessionQueueBase):
self.__lock.release()
return CancelByBatchIDsResult(canceled=count)
def cancel_by_origin(self, queue_id: str, origin: str) -> CancelByOriginResult:
try:
current_queue_item = self.get_current(queue_id)
self.__lock.acquire()
where = """--sql
WHERE
queue_id == ?
AND origin == ?
AND status != 'canceled'
AND status != 'completed'
AND status != 'failed'
"""
params = (queue_id, origin)
self.__cursor.execute(
f"""--sql
SELECT COUNT(*)
FROM session_queue
{where};
""",
params,
)
count = self.__cursor.fetchone()[0]
self.__cursor.execute(
f"""--sql
UPDATE session_queue
SET status = 'canceled'
{where};
""",
params,
)
self.__conn.commit()
if current_queue_item is not None and current_queue_item.origin == origin:
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)
def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
try:
current_queue_item = self.get_current(queue_id)
@@ -541,7 +578,8 @@ class SqliteSessionQueue(SessionQueueBase):
started_at,
session_id,
batch_id,
queue_id
queue_id,
origin
FROM session_queue
WHERE queue_id = ?
"""
@@ -621,7 +659,7 @@ class SqliteSessionQueue(SessionQueueBase):
self.__lock.acquire()
self.__cursor.execute(
"""--sql
SELECT status, count(*)
SELECT status, count(*), origin
FROM session_queue
WHERE
queue_id = ?
@@ -633,6 +671,7 @@ class SqliteSessionQueue(SessionQueueBase):
result = cast(list[sqlite3.Row], self.__cursor.fetchall())
total = sum(row[1] for row in result)
counts: dict[str, int] = {row[0]: row[1] for row in result}
origin = result[0]["origin"] if result else None
except Exception:
self.__conn.rollback()
raise
@@ -641,6 +680,7 @@ class SqliteSessionQueue(SessionQueueBase):
return BatchStatus(
batch_id=batch_id,
origin=origin,
queue_id=queue_id,
pending=counts.get("pending", 0),
in_progress=counts.get("in_progress", 0),

View File

@@ -17,6 +17,7 @@ from invokeai.app.services.shared.sqlite_migrator.migrations.migration_11 import
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_12 import build_migration_12
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_13 import build_migration_13
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_14 import build_migration_14
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_15 import build_migration_15
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator
@@ -51,6 +52,7 @@ def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileSto
migrator.register_migration(build_migration_12(app_config=config))
migrator.register_migration(build_migration_13())
migrator.register_migration(build_migration_14())
migrator.register_migration(build_migration_15())
migrator.run_migrations()
return db

View File

@@ -0,0 +1,31 @@
import sqlite3
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
class Migration15Callback:
def __call__(self, cursor: sqlite3.Cursor) -> None:
self._add_origin_col(cursor)
def _add_origin_col(self, cursor: sqlite3.Cursor) -> None:
"""
- Adds `origin` column to the session queue table.
"""
cursor.execute("ALTER TABLE session_queue ADD COLUMN origin TEXT;")
def build_migration_15() -> Migration:
"""
Build the migration from database version 14 to 15.
This migration does the following:
- Adds `origin` column to the session queue table.
"""
migration_15 = Migration(
from_version=14,
to_version=15,
callback=Migration15Callback(),
)
return migration_15

View File

@@ -12,6 +12,10 @@ module.exports = {
'i18next/no-literal-string': 'error',
// https://eslint.org/docs/latest/rules/no-console
'no-console': 'error',
// https://eslint.org/docs/latest/rules/no-promise-executor-return
'no-promise-executor-return': 'error',
// https://eslint.org/docs/latest/rules/require-await
'require-await': 'error',
},
overrides: [
/**

View File

@@ -1,5 +1,5 @@
import { PropsWithChildren, memo, useEffect } from 'react';
import { modelChanged } from '../src/features/parameters/store/generationSlice';
import { modelChanged } from '../src/features/controlLayers/store/canvasV2Slice';
import { useAppDispatch } from '../src/app/store/storeHooks';
import { useGlobalModifiersInit } from '@invoke-ai/ui-library';
/**
@@ -10,7 +10,9 @@ export const ReduxInit = memo((props: PropsWithChildren) => {
const dispatch = useAppDispatch();
useGlobalModifiersInit();
useEffect(() => {
dispatch(modelChanged({ key: 'test_model', hash: 'some_hash', name: 'some name', base: 'sd-1', type: 'main' }));
dispatch(
modelChanged({ model: { key: 'test_model', hash: 'some_hash', name: 'some name', base: 'sd-1', type: 'main' } })
);
}, []);
return props.children;

View File

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

View File

@@ -52,17 +52,17 @@
}
},
"dependencies": {
"@chakra-ui/react-use-size": "^2.1.0",
"@dagrejs/dagre": "^1.1.3",
"@dagrejs/graphlib": "^2.2.3",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fontsource-variable/inter": "^5.0.20",
"@invoke-ai/ui-library": "^0.0.29",
"@invoke-ai/ui-library": "^0.0.31",
"@nanostores/react": "^0.7.3",
"@reduxjs/toolkit": "2.2.3",
"@roarr/browser-log-writer": "^1.3.0",
"async-mutex": "^0.5.0",
"chakra-react-select": "^4.9.1",
"compare-versions": "^6.1.1",
"dateformat": "^5.0.3",
@@ -74,6 +74,8 @@
"jsondiffpatch": "^0.6.0",
"konva": "^9.3.14",
"lodash-es": "^4.17.21",
"lru-cache": "^11.0.0",
"nanoid": "^5.0.7",
"nanostores": "^0.11.2",
"new-github-issue-url": "^1.0.0",
"overlayscrollbars": "^2.10.0",
@@ -88,7 +90,6 @@
"react-hotkeys-hook": "4.5.0",
"react-i18next": "^14.1.3",
"react-icons": "^5.2.1",
"react-konva": "^18.2.10",
"react-redux": "9.1.2",
"react-resizable-panels": "^2.0.23",
"react-select": "5.8.0",
@@ -102,9 +103,9 @@
"roarr": "^7.21.1",
"serialize-error": "^11.0.3",
"socket.io-client": "^4.7.5",
"stable-hash": "^0.0.4",
"use-debounce": "^10.0.2",
"use-device-pixel-ratio": "^1.1.2",
"use-image": "^1.1.1",
"uuid": "^10.0.0",
"zod": "^3.23.8",
"zod-validation-error": "^3.3.1"

View File

@@ -5,9 +5,6 @@ settings:
excludeLinksFromLockfile: false
dependencies:
'@chakra-ui/react-use-size':
specifier: ^2.1.0
version: 2.1.0(react@18.3.1)
'@dagrejs/dagre':
specifier: ^1.1.3
version: 1.1.3
@@ -27,8 +24,8 @@ dependencies:
specifier: ^5.0.20
version: 5.0.20
'@invoke-ai/ui-library':
specifier: ^0.0.29
version: 0.0.29(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1)
specifier: ^0.0.31
version: 0.0.31(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1)
'@nanostores/react':
specifier: ^0.7.3
version: 0.7.3(nanostores@0.11.2)(react@18.3.1)
@@ -38,6 +35,9 @@ dependencies:
'@roarr/browser-log-writer':
specifier: ^1.3.0
version: 1.3.0
async-mutex:
specifier: ^0.5.0
version: 0.5.0
chakra-react-select:
specifier: ^4.9.1
version: 4.9.1(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
@@ -71,6 +71,12 @@ dependencies:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
lru-cache:
specifier: ^11.0.0
version: 11.0.0
nanoid:
specifier: ^5.0.7
version: 5.0.7
nanostores:
specifier: ^0.11.2
version: 0.11.2
@@ -113,9 +119,6 @@ dependencies:
react-icons:
specifier: ^5.2.1
version: 5.2.1(react@18.3.1)
react-konva:
specifier: ^18.2.10
version: 18.2.10(konva@9.3.14)(react-dom@18.3.1)(react@18.3.1)
react-redux:
specifier: 9.1.2
version: 9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1)
@@ -155,15 +158,15 @@ dependencies:
socket.io-client:
specifier: ^4.7.5
version: 4.7.5
stable-hash:
specifier: ^0.0.4
version: 0.0.4
use-debounce:
specifier: ^10.0.2
version: 10.0.2(react@18.3.1)
use-device-pixel-ratio:
specifier: ^1.1.2
version: 1.1.2(react@18.3.1)
use-image:
specifier: ^1.1.1
version: 1.1.1(react-dom@18.3.1)(react@18.3.1)
uuid:
specifier: ^10.0.0
version: 10.0.0
@@ -3561,8 +3564,8 @@ packages:
prettier: 3.3.3
dev: true
/@invoke-ai/ui-library@0.0.29(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-7SYOaiEEKk9iHk0hg2R2yVxiuV3I1x6bDEv0R3Y2tCH/Aq5XDG2tR+d7SQAPqf5+za3S+qfNFjbjl7GvEMwqmA==}
/@invoke-ai/ui-library@0.0.31(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-7LtOUN/bcGHc8jCRd2m22DvP2eeogqwM/shdXQpLH5RY2FzWJNXlWdVT4hIPGDu7znnk3xvXlZvo6tiGSjbnCQ==}
peerDependencies:
'@fontsource-variable/inter': ^5.0.16
react: ^18.2.0
@@ -5202,12 +5205,6 @@ packages:
'@types/react': 18.3.3
dev: true
/@types/react-reconciler@0.28.8:
resolution: {integrity: sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==}
dependencies:
'@types/react': 18.3.3
dev: false
/@types/react-transition-group@4.4.10:
resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==}
dependencies:
@@ -5903,6 +5900,12 @@ packages:
tslib: 2.6.3
dev: true
/async-mutex@0.5.0:
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
dependencies:
tslib: 2.6.3
dev: false
/attr-accept@2.2.2:
resolution: {integrity: sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==}
engines: {node: '>=4'}
@@ -8384,15 +8387,6 @@ packages:
set-function-name: 2.0.2
dev: true
/its-fine@1.2.5(react@18.3.1):
resolution: {integrity: sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==}
peerDependencies:
react: '>=18.0'
dependencies:
'@types/react-reconciler': 0.28.8
react: 18.3.1
dev: false
/jackspeak@2.3.6:
resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
engines: {node: '>=14'}
@@ -8710,6 +8704,11 @@ packages:
engines: {node: 14 || >=16.14}
dev: true
/lru-cache@11.0.0:
resolution: {integrity: sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==}
engines: {node: 20 || >=22}
dev: false
/lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
dependencies:
@@ -8997,6 +8996,12 @@ packages:
hasBin: true
dev: true
/nanoid@5.0.7:
resolution: {integrity: sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==}
engines: {node: ^18 || >=20}
hasBin: true
dev: false
/nanostores@0.11.2:
resolution: {integrity: sha512-6bucNxMJA5rNV554WQl+MWGng0QVMzlRgpKTHHfIbVLrhQ+yRXBychV9ECGVuuUfCMQPjfIG9bj8oJFZ9hYP/Q==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -9801,33 +9806,6 @@ packages:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
dev: true
/react-konva@18.2.10(konva@9.3.14)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==}
peerDependencies:
konva: ^8.0.1 || ^7.2.5 || ^9.0.0
react: '>=18.0.0'
react-dom: '>=18.0.0'
dependencies:
'@types/react-reconciler': 0.28.8
its-fine: 1.2.5(react@18.3.1)
konva: 9.3.14
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-reconciler: 0.29.2(react@18.3.1)
scheduler: 0.23.2
dev: false
/react-reconciler@0.29.2(react@18.3.1):
resolution: {integrity: sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==}
engines: {node: '>=0.10.0'}
peerDependencies:
react: ^18.3.1
dependencies:
loose-envify: 1.4.0
react: 18.3.1
scheduler: 0.23.2
dev: false
/react-redux@9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1):
resolution: {integrity: sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==}
peerDependencies:
@@ -10625,6 +10603,10 @@ packages:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
dev: true
/stable-hash@0.0.4:
resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==}
dev: false
/stack-generator@2.0.10:
resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==}
dependencies:
@@ -11330,16 +11312,6 @@ packages:
react: 18.3.1
dev: false
/use-image@1.1.1(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-n4YO2k8AJG/BcDtxmBx8Aa+47kxY5m335dJiCQA5tTeVU4XdhrhqR6wT0WISRXwdMEOv5CSjqekDZkEMiiWaYQ==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/use-isomorphic-layout-effect@1.1.2(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
peerDependencies:

View File

@@ -80,6 +80,7 @@
"aboutDesc": "Using Invoke for work? Check out:",
"aboutHeading": "Own Your Creative Power",
"accept": "Accept",
"apply": "Apply",
"add": "Add",
"advanced": "Advanced",
"ai": "ai",
@@ -115,6 +116,7 @@
"githubLabel": "Github",
"goTo": "Go to",
"hotkeysLabel": "Hotkeys",
"loadingImage": "Loading Image",
"imageFailedToLoad": "Unable to Load Image",
"img2img": "Image To Image",
"inpaint": "inpaint",
@@ -325,6 +327,10 @@
"canceled": "Canceled",
"completedIn": "Completed in",
"batch": "Batch",
"origin": "Origin",
"originCanvas": "Canvas",
"originWorkflows": "Workflows",
"originOther": "Other",
"batchFieldValues": "Batch Field Values",
"item": "Item",
"session": "Session",
@@ -1095,7 +1101,6 @@
"confirmOnDelete": "Confirm On Delete",
"developer": "Developer",
"displayInProgress": "Display Progress Images",
"enableImageDebugging": "Enable Image Debugging",
"enableInformationalPopovers": "Enable Informational Popovers",
"informationalPopoversDisabled": "Informational Popovers Disabled",
"informationalPopoversDisabledDesc": "Informational popovers have been disabled. Enable them in Settings.",
@@ -1562,7 +1567,7 @@
"copyToClipboard": "Copy to Clipboard",
"cursorPosition": "Cursor Position",
"darkenOutsideSelection": "Darken Outside Selection",
"discardAll": "Discard All",
"discardAll": "Discard All & Cancel Pending Generations",
"discardCurrent": "Discard Current",
"downloadAsImage": "Download As Image",
"enableMask": "Enable Mask",
@@ -1640,16 +1645,31 @@
"storeNotInitialized": "Store is not initialized"
},
"controlLayers": {
"generateMode": "Generate",
"generateModeDesc": "Create individual images. Generated images are added directly to the gallery.",
"composeMode": "Compose",
"composeModeDesc": "Compose your work iterative. Generated images are added back to the canvas.",
"autoSave": "Auto-save to Gallery",
"resetCanvas": "Reset Canvas",
"resetAll": "Reset All",
"deleteAll": "Delete All",
"clearCaches": "Clear Caches",
"recalculateRects": "Recalculate Rects",
"clipToBbox": "Clip Strokes to Bbox",
"addLayer": "Add Layer",
"moveToFront": "Move to Front",
"moveToBack": "Move to Back",
"moveForward": "Move Forward",
"moveBackward": "Move Backward",
"brushSize": "Brush Size",
"width": "Width",
"zoom": "Zoom",
"resetView": "Reset View",
"controlLayers": "Control Layers",
"globalMaskOpacity": "Global Mask Opacity",
"autoNegative": "Auto Negative",
"enableAutoNegative": "Enable Auto Negative",
"disableAutoNegative": "Disable Auto Negative",
"deletePrompt": "Delete Prompt",
"resetRegion": "Reset Region",
"debugLayers": "Debug Layers",
@@ -1658,21 +1678,83 @@
"addPositivePrompt": "Add $t(common.positivePrompt)",
"addNegativePrompt": "Add $t(common.negativePrompt)",
"addIPAdapter": "Add $t(common.ipAdapter)",
"regionalGuidance": "Regional Guidance",
"regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)",
"raster": "Raster",
"rasterLayer_one": "Raster Layer",
"controlLayer_one": "Control Layer",
"inpaintMask_one": "Inpaint Mask",
"regionalGuidance_one": "Regional Guidance",
"ipAdapter_one": "IP Adapter",
"rasterLayer_other": "Raster Layers",
"controlLayer_other": "Control Layers",
"inpaintMask_other": "Inpaint Masks",
"regionalGuidance_other": "Regional Guidance",
"ipAdapter_other": "IP Adapters",
"opacity": "Opacity",
"regionalGuidance_withCount_hidden": "Regional Guidance ({{count}} hidden)",
"controlAdapters_withCount_hidden": "Control Adapters ({{count}} hidden)",
"controlLayers_withCount_hidden": "Control Layers ({{count}} hidden)",
"rasterLayers_withCount_hidden": "Raster Layers ({{count}} hidden)",
"ipAdapters_withCount_hidden": "IP Adapters ({{count}} hidden)",
"inpaintMasks_withCount_hidden": "Inpaint Masks ({{count}} hidden)",
"regionalGuidance_withCount_visible": "Regional Guidance ({{count}})",
"controlAdapters_withCount_visible": "Control Adapters ({{count}})",
"controlLayers_withCount_visible": "Control Layers ({{count}})",
"rasterLayers_withCount_visible": "Raster Layers ({{count}})",
"ipAdapters_withCount_visible": "IP Adapters ({{count}})",
"inpaintMasks_withCount_visible": "Inpaint Masks ({{count}})",
"globalControlAdapter": "Global $t(controlnet.controlAdapter_one)",
"globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)",
"globalIPAdapter": "Global $t(common.ipAdapter)",
"globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)",
"globalInitialImage": "Global Initial Image",
"globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)",
"layer": "Layer",
"opacityFilter": "Opacity Filter",
"clearProcessor": "Clear Processor",
"resetProcessor": "Reset Processor to Defaults",
"noLayersAdded": "No Layers Added",
"layers_one": "Layer",
"layers_other": "Layers"
"layers_other": "Layers",
"objects_zero": "empty",
"objects_one": "{{count}} object",
"objects_other": "{{count}} objects",
"convertToControlLayer": "Convert to Control Layer",
"convertToRasterLayer": "Convert to Raster Layer",
"transparency": "Transparency",
"enableTransparencyEffect": "Enable Transparency Effect",
"disableTransparencyEffect": "Disable Transparency Effect",
"hidingType": "Hiding {{type}}",
"showingType": "Showing {{type}}",
"dynamicGrid": "Dynamic Grid",
"logDebugInfo": "Log Debug Info",
"fill": {
"fillStyle": "Fill Style",
"solid": "Solid",
"grid": "Grid",
"crosshatch": "Crosshatch",
"vertical": "Vertical",
"horizontal": "Horizontal",
"diagonal": "Diagonal"
},
"tool": {
"brush": "Brush",
"eraser": "Eraser",
"rectangle": "Rectangle",
"bbox": "Bbox",
"move": "Move",
"view": "View",
"transform": "Transform",
"colorPicker": "Color Picker"
},
"filter": {
"filter": "Filter",
"filters": "Filters",
"filterType": "Filter Type",
"preview": "Preview",
"apply": "Apply",
"cancel": "Cancel"
}
},
"upscaling": {
"upscale": "Upscale",
@@ -1760,5 +1842,30 @@
"upscaling": "Upscaling",
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
}
},
"system": {
"enableLogging": "Enable Logging",
"logLevel": {
"logLevel": "Log Level",
"trace": "Trace",
"debug": "Debug",
"info": "Info",
"warn": "Warn",
"error": "Error",
"fatal": "Fatal"
},
"logNamespaces": {
"logNamespaces": "Log Namespaces",
"gallery": "Gallery",
"models": "Models",
"config": "Config",
"canvas": "Canvas",
"generation": "Generation",
"workflows": "Workflows",
"system": "System",
"events": "Events",
"queue": "Queue",
"metadata": "Metadata"
}
}
}

View File

@@ -38,7 +38,7 @@ async function generateTypes(schema) {
process.stdout.write(`\nOK!\r\n`);
}
async function main() {
function main() {
const encoding = 'utf-8';
if (process.stdin.isTTY) {

View File

@@ -6,6 +6,7 @@ import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/ap
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import type { PartialAppConfig } from 'app/types/invokeai';
import ImageUploadOverlay from 'common/components/ImageUploadOverlay';
import { useScopeFocusWatcher } from 'common/hooks/interactionScopes';
import { useClearStorage } from 'common/hooks/useClearStorage';
import { useFullscreenDropzone } from 'common/hooks/useFullscreenDropzone';
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
@@ -13,12 +14,13 @@ import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardMo
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
import { configChanged } from 'features/system/store/configSlice';
import { languageSelector } from 'features/system/store/systemSelectors';
import InvokeTabs from 'features/ui/components/InvokeTabs';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import { AppContent } from 'features/ui/components/AppContent';
import { setActiveTab } from 'features/ui/store/uiSlice';
import type { TabName } from 'features/ui/store/uiTypes';
import { useGetAndLoadLibraryWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow';
import { AnimatePresence } from 'framer-motion';
import i18n from 'i18n';
@@ -39,7 +41,7 @@ interface Props {
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
};
selectedWorkflowId?: string;
destination?: InvokeTabName | undefined;
destination?: TabName | undefined;
}
const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, destination }: Props) => {
@@ -93,6 +95,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, desti
useStarterModelsToast();
useSyncQueueStatus();
useScopeFocusWatcher();
return (
<ErrorBoundary onReset={handleReset} FallbackComponent={AppErrorBoundaryFallback}>
@@ -105,7 +108,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, desti
{...dropzone.getRootProps()}
>
<input {...dropzone.getInputProps()} />
<InvokeTabs />
<AppContent />
<AnimatePresence>
{dropzone.isDragActive && isHandlingUpload && (
<ImageUploadOverlay dropzone={dropzone} setIsHandlingUpload={setIsHandlingUpload} />
@@ -116,6 +119,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, desti
<ChangeBoardModal />
<DynamicPromptsModal />
<StylePresetModal />
<ClearQueueConfirmationsAlertDialog />
<PreselectedImage selectedImage={selectedImage} />
</ErrorBoundary>
);

View File

@@ -19,7 +19,7 @@ import type { PartialAppConfig } from 'app/types/invokeai';
import Loading from 'common/components/Loading/Loading';
import AppDndContext from 'features/dnd/components/AppDndContext';
import type { WorkflowCategory } from 'features/nodes/types/workflow';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import type { TabName } from 'features/ui/store/uiTypes';
import type { PropsWithChildren, ReactNode } from 'react';
import React, { lazy, memo, useEffect, useMemo } from 'react';
import { Provider } from 'react-redux';
@@ -45,7 +45,7 @@ interface Props extends PropsWithChildren {
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
};
selectedWorkflowId?: string;
destination?: InvokeTabName;
destination?: TabName;
customStarUi?: CustomStarUi;
socketOptions?: Partial<ManagerOptions & SocketOptions>;
isDebugging?: boolean;

View File

@@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
import { $authToken } from 'app/store/nanostores/authToken';
import { $baseUrl } from 'app/store/nanostores/baseUrl';
import { $isDebugging } from 'app/store/nanostores/isDebugging';
import { useAppDispatch } from 'app/store/storeHooks';
import { useAppStore } from 'app/store/nanostores/store';
import type { MapStore } from 'nanostores';
import { atom, map } from 'nanostores';
import { useEffect, useMemo } from 'react';
@@ -18,14 +18,19 @@ declare global {
}
}
export type AppSocket = Socket<ServerToClientEvents, ClientToServerEvents>;
export const $socket = atom<AppSocket | null>(null);
export const $socketOptions = map<Partial<ManagerOptions & SocketOptions>>({});
const $isSocketInitialized = atom<boolean>(false);
export const $isConnected = atom<boolean>(false);
/**
* Initializes the socket.io connection and sets up event listeners.
*/
export const useSocketIO = () => {
const dispatch = useAppDispatch();
const { dispatch, getState } = useAppStore();
const baseUrl = useStore($baseUrl);
const authToken = useStore($authToken);
const addlSocketOptions = useStore($socketOptions);
@@ -61,8 +66,9 @@ export const useSocketIO = () => {
return;
}
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io(socketUrl, socketOptions);
setEventListeners({ dispatch, socket });
const socket: AppSocket = io(socketUrl, socketOptions);
$socket.set(socket);
setEventListeners({ socket, dispatch, getState, setIsConnected: $isConnected.set });
socket.connect();
if ($isDebugging.get() || import.meta.env.MODE === 'development') {
@@ -84,5 +90,5 @@ export const useSocketIO = () => {
socket.disconnect();
$isSocketInitialized.set(false);
};
}, [dispatch, socketOptions, socketUrl]);
}, [dispatch, getState, socketOptions, socketUrl]);
};

View File

@@ -15,21 +15,21 @@ export const BASE_CONTEXT = {};
export const $logger = atom<Logger>(Roarr.child(BASE_CONTEXT));
export type LoggerNamespace =
| 'images'
| 'models'
| 'config'
| 'canvas'
| 'generation'
| 'nodes'
| 'system'
| 'socketio'
| 'session'
| 'queue'
| 'dnd'
| 'controlLayers';
export const zLogNamespace = z.enum([
'canvas',
'config',
'events',
'gallery',
'generation',
'metadata',
'models',
'system',
'queue',
'workflows',
]);
export type LogNamespace = z.infer<typeof zLogNamespace>;
export const logger = (namespace: LoggerNamespace) => $logger.get().child({ namespace });
export const logger = (namespace: LogNamespace) => $logger.get().child({ namespace });
export const zLogLevel = z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']);
export type LogLevel = z.infer<typeof zLogLevel>;

View File

@@ -3,27 +3,34 @@ import { useAppSelector } from 'app/store/storeHooks';
import { useEffect, useMemo } from 'react';
import { ROARR, Roarr } from 'roarr';
import type { LoggerNamespace } from './logger';
import type { LogNamespace } from './logger';
import { $logger, BASE_CONTEXT, LOG_LEVEL_MAP, logger } from './logger';
export const useLogger = (namespace: LoggerNamespace) => {
const consoleLogLevel = useAppSelector((s) => s.system.consoleLogLevel);
const shouldLogToConsole = useAppSelector((s) => s.system.shouldLogToConsole);
export const useLogger = (namespace: LogNamespace) => {
const logLevel = useAppSelector((s) => s.system.logLevel);
const logNamespaces = useAppSelector((s) => s.system.logNamespaces);
const logIsEnabled = useAppSelector((s) => s.system.logIsEnabled);
// The provided Roarr browser log writer uses localStorage to config logging to console
useEffect(() => {
if (shouldLogToConsole) {
if (logIsEnabled) {
// Enable console log output
localStorage.setItem('ROARR_LOG', 'true');
// Use a filter to show only logs of the given level
localStorage.setItem('ROARR_FILTER', `context.logLevel:>=${LOG_LEVEL_MAP[consoleLogLevel]}`);
let filter = `context.logLevel:>=${LOG_LEVEL_MAP[logLevel]}`;
if (logNamespaces.length > 0) {
filter += ` AND (${logNamespaces.map((ns) => `context.namespace:${ns}`).join(' OR ')})`;
} else {
filter += ' AND context.namespace:undefined';
}
localStorage.setItem('ROARR_FILTER', filter);
} else {
// Disable console log output
localStorage.setItem('ROARR_LOG', 'false');
}
ROARR.write = createLogWriter();
}, [consoleLogLevel, shouldLogToConsole]);
}, [logLevel, logIsEnabled, logNamespaces]);
// Update the module-scoped logger context as needed
useEffect(() => {

View File

@@ -1,7 +1,7 @@
import { createAction } from '@reduxjs/toolkit';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import type { TabName } from 'features/ui/store/uiTypes';
export const enqueueRequested = createAction<{
tabName: InvokeTabName;
tabName: TabName;
prepend: boolean;
}>('app/enqueueRequested');

View File

@@ -1,5 +1,6 @@
import { createDraftSafeSelectorCreator, createSelectorCreator, lruMemoize } from '@reduxjs/toolkit';
import type { GetSelectorsOptions } from '@reduxjs/toolkit/dist/entities/state_selectors';
import type { RootState } from 'app/store/store';
import { isEqual } from 'lodash-es';
/**
@@ -19,3 +20,5 @@ export const getSelectorsOptions: GetSelectorsOptions = {
argsMemoize: lruMemoize,
}),
};
export const createMemoizedAppSelector = createMemoizedSelector.withTypes<RootState>();

View File

@@ -1,5 +1,4 @@
import { logger } from 'app/logging/logger';
import { parseify } from 'common/util/serialize';
import { PersistError, RehydrateError } from 'redux-remember';
import { serializeError } from 'serialize-error';
@@ -41,6 +40,6 @@ export const errorHandler = (err: PersistError | RehydrateError) => {
} else if (err instanceof RehydrateError) {
log.error({ error: serializeError(err) }, 'Problem rehydrating state');
} else {
log.error({ error: parseify(err) }, 'Problem in persistence layer');
log.error({ error: serializeError(err) }, 'Problem in persistence layer');
}
};

View File

@@ -1,9 +1,7 @@
import type { UnknownAction } from '@reduxjs/toolkit';
import { deepClone } from 'common/util/deepClone';
import { isAnyGraphBuilt } from 'features/nodes/store/actions';
import { appInfoApi } from 'services/api/endpoints/appInfo';
import type { Graph } from 'services/api/types';
import { socketGeneratorProgress } from 'services/events/actions';
export const actionSanitizer = <A extends UnknownAction>(action: A): A => {
if (isAnyGraphBuilt(action)) {
@@ -24,13 +22,5 @@ export const actionSanitizer = <A extends UnknownAction>(action: A): A => {
};
}
if (socketGeneratorProgress.match(action)) {
const sanitized = deepClone(action);
if (sanitized.payload.data.progress_image) {
sanitized.payload.data.progress_image.dataURL = '<Progress image omitted>';
}
return sanitized;
}
return action;
};

View File

@@ -1,7 +1,7 @@
import type { TypedStartListening } from '@reduxjs/toolkit';
import { createListenerMiddleware } from '@reduxjs/toolkit';
import { addAdHocPostProcessingRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
import { addCommitStagingAreaImageListener } from 'app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener';
import { addStagingListeners } from 'app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener';
import { addAnyEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/anyEnqueued';
import { addAppConfigReceivedListener } from 'app/store/middleware/listenerMiddleware/listeners/appConfigReceived';
import { addAppStartedListener } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
@@ -9,17 +9,6 @@ import { addBatchEnqueuedListener } from 'app/store/middleware/listenerMiddlewar
import { addDeleteBoardAndImagesFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted';
import { addBoardIdSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/boardIdSelected';
import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload';
import { addCanvasCopiedToClipboardListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard';
import { addCanvasDownloadedAsImageListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage';
import { addCanvasImageToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet';
import { addCanvasMaskSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery';
import { addCanvasMaskToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet';
import { addCanvasMergedListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMerged';
import { addCanvasSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery';
import { addControlAdapterPreprocessor } from 'app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor';
import { addControlNetAutoProcessListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess';
import { addControlNetImageProcessedListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed';
import { addEnqueueRequestedCanvasListener } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas';
import { addEnqueueRequestedLinear } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear';
import { addEnqueueRequestedNodes } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes';
import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
@@ -37,16 +26,7 @@ import { addModelSelectedListener } from 'app/store/middleware/listenerMiddlewar
import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded';
import { addDynamicPromptsListener } from 'app/store/middleware/listenerMiddleware/listeners/promptChanged';
import { addSetDefaultSettingsListener } from 'app/store/middleware/listenerMiddleware/listeners/setDefaultSettings';
import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected';
import { addSocketDisconnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketDisconnected';
import { addGeneratorProgressEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress';
import { addInvocationCompleteEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete';
import { addInvocationErrorEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError';
import { addInvocationStartedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted';
import { addModelInstallEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall';
import { addModelLoadEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad';
import { addSocketQueueItemStatusChangedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged';
import { addStagingAreaImageSavedListener } from 'app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved';
import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketConnected';
import { addUpdateAllNodesRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested';
import { addWorkflowLoadRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested';
import type { AppDispatch, RootState } from 'app/store/store';
@@ -83,7 +63,6 @@ addGalleryImageClickedListener(startAppListening);
addGalleryOffsetChangedListener(startAppListening);
// User Invoked
addEnqueueRequestedCanvasListener(startAppListening);
addEnqueueRequestedNodes(startAppListening);
addEnqueueRequestedLinear(startAppListening);
addEnqueueRequestedUpscale(startAppListening);
@@ -91,31 +70,22 @@ addAnyEnqueuedListener(startAppListening);
addBatchEnqueuedListener(startAppListening);
// Canvas actions
addCanvasSavedToGalleryListener(startAppListening);
addCanvasMaskSavedToGalleryListener(startAppListening);
addCanvasImageToControlNetListener(startAppListening);
addCanvasMaskToControlNetListener(startAppListening);
addCanvasDownloadedAsImageListener(startAppListening);
addCanvasCopiedToClipboardListener(startAppListening);
addCanvasMergedListener(startAppListening);
addStagingAreaImageSavedListener(startAppListening);
addCommitStagingAreaImageListener(startAppListening);
// addCanvasSavedToGalleryListener(startAppListening);
// addCanvasMaskSavedToGalleryListener(startAppListening);
// addCanvasImageToControlNetListener(startAppListening);
// addCanvasMaskToControlNetListener(startAppListening);
// addCanvasDownloadedAsImageListener(startAppListening);
// addCanvasCopiedToClipboardListener(startAppListening);
// addCanvasMergedListener(startAppListening);
// addStagingAreaImageSavedListener(startAppListening);
// addCommitStagingAreaImageListener(startAppListening);
addStagingListeners(startAppListening);
// Socket.IO
addGeneratorProgressEventListener(startAppListening);
addInvocationCompleteEventListener(startAppListening);
addInvocationErrorEventListener(startAppListening);
addInvocationStartedEventListener(startAppListening);
addSocketConnectedEventListener(startAppListening);
addSocketDisconnectedEventListener(startAppListening);
addModelLoadEventListener(startAppListening);
addModelInstallEventListener(startAppListening);
addSocketQueueItemStatusChangedEventListener(startAppListening);
addBulkDownloadListeners(startAppListening);
// ControlNet
addControlNetImageProcessedListener(startAppListening);
addControlNetAutoProcessListener(startAppListening);
// Gallery bulk download
addBulkDownloadListeners(startAppListening);
// Boards
addImageAddedToBoardFulfilledListener(startAppListening);
@@ -148,4 +118,4 @@ addAdHocPostProcessingRequestedListener(startAppListening);
addDynamicPromptsListener(startAppListening);
addSetDefaultSettingsListener(startAppListening);
addControlAdapterPreprocessor(startAppListening);
// addControlAdapterPreprocessor(startAppListening);

View File

@@ -1,21 +1,21 @@
import { createAction } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { parseify } from 'common/util/serialize';
import type { SerializableObject } from 'common/types';
import { buildAdHocPostProcessingGraph } from 'features/nodes/util/graph/buildAdHocPostProcessingGraph';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { queueApi } from 'services/api/endpoints/queue';
import type { BatchConfig, ImageDTO } from 'services/api/types';
const log = logger('queue');
export const adHocPostProcessingRequested = createAction<{ imageDTO: ImageDTO }>(`upscaling/postProcessingRequested`);
export const addAdHocPostProcessingRequestedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: adHocPostProcessingRequested,
effect: async (action, { dispatch, getState }) => {
const log = logger('session');
const { imageDTO } = action.payload;
const state = getState();
@@ -39,9 +39,9 @@ export const addAdHocPostProcessingRequestedListener = (startAppListening: AppSt
const enqueueResult = await req.unwrap();
req.reset();
log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued'));
log.debug({ enqueueResult } as SerializableObject, t('queue.graphQueued'));
} catch (error) {
log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue'));
log.error({ enqueueBatchArg } as SerializableObject, t('queue.graphFailedToQueue'));
if (error instanceof Object && 'status' in error && error.status === 403) {
return;

View File

@@ -23,7 +23,7 @@ export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartLis
*/
startAppListening({
matcher: matchAnyBoardDeleted,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const state = getState();
const deletedBoardId = action.meta.arg.originalArgs;
const { autoAddBoardId, selectedBoardId } = state.gallery;
@@ -44,7 +44,7 @@ export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartLis
// If we archived a board, it may end up hidden. If it's selected or the auto-add board, we should reset those.
startAppListening({
matcher: boardsApi.endpoints.updateBoard.matchFulfilled,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const state = getState();
const { shouldShowArchivedBoards } = state.gallery;
@@ -61,7 +61,7 @@ export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartLis
// When we hide archived boards, if the selected or the auto-add board is archived, we should reset those.
startAppListening({
actionCreator: shouldShowArchivedBoardsChanged,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const shouldShowArchivedBoards = action.payload;
// We only need to take action if we have just hidden archived boards.
@@ -100,7 +100,7 @@ export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartLis
*/
startAppListening({
matcher: boardsApi.endpoints.listAllBoards.matchFulfilled,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const boards = action.payload;
const state = getState();
const { selectedBoardId, autoAddBoardId } = state.gallery;

View File

@@ -1,33 +1,36 @@
import { isAnyOf } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import {
canvasBatchIdsReset,
commitStagingAreaImage,
discardStagedImages,
resetCanvas,
setInitialCanvasImage,
} from 'features/canvas/store/canvasSlice';
rasterLayerAdded,
sessionStagingAreaImageAccepted,
sessionStagingAreaReset,
} from 'features/controlLayers/store/canvasV2Slice';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { queueApi } from 'services/api/endpoints/queue';
import { $lastCanvasProgressEvent } from 'services/events/setEventListeners';
import { assert } from 'tsafe';
const matcher = isAnyOf(commitStagingAreaImage, discardStagedImages, resetCanvas, setInitialCanvasImage);
const log = logger('canvas');
export const addCommitStagingAreaImageListener = (startAppListening: AppStartListening) => {
export const addStagingListeners = (startAppListening: AppStartListening) => {
startAppListening({
matcher,
effect: async (_, { dispatch, getState }) => {
const log = logger('canvas');
const state = getState();
const { batchIds } = state.canvas;
actionCreator: sessionStagingAreaReset,
effect: async (_, { dispatch }) => {
try {
const req = dispatch(
queueApi.endpoints.cancelByBatchIds.initiate({ batch_ids: batchIds }, { fixedCacheKey: 'cancelByBatchIds' })
queueApi.endpoints.cancelByBatchOrigin.initiate(
{ origin: 'canvas' },
{ fixedCacheKey: 'cancelByBatchOrigin' }
)
);
const { canceled } = await req.unwrap();
req.reset();
$lastCanvasProgressEvent.set(null);
if (canceled > 0) {
log.debug(`Canceled ${canceled} canvas batches`);
toast({
@@ -36,7 +39,6 @@ export const addCommitStagingAreaImageListener = (startAppListening: AppStartLis
status: 'success',
});
}
dispatch(canvasBatchIdsReset());
} catch {
log.error('Failed to cancel canvas batches');
toast({
@@ -47,4 +49,26 @@ export const addCommitStagingAreaImageListener = (startAppListening: AppStartLis
}
},
});
startAppListening({
actionCreator: sessionStagingAreaImageAccepted,
effect: (action, api) => {
const { index } = action.payload;
const state = api.getState();
const stagingAreaImage = state.canvasV2.session.stagedImages[index];
assert(stagingAreaImage, 'No staged image found to accept');
const { x, y } = state.canvasV2.bbox.rect;
const { imageDTO, offsetX, offsetY } = stagingAreaImage;
const imageObject = imageDTOToImageObject(imageDTO);
const overrides: Partial<CanvasRasterLayerState> = {
position: { x: x + offsetX, y: y + offsetY },
objects: [imageObject],
};
api.dispatch(rasterLayerAdded({ overrides, isSelected: true }));
api.dispatch(sessionStagingAreaReset());
},
});
};

View File

@@ -4,7 +4,7 @@ import { queueApi, selectQueueStatus } from 'services/api/endpoints/queue';
export const addAnyEnqueuedListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: queueApi.endpoints.enqueueBatch.matchFulfilled,
effect: async (_, { dispatch, getState }) => {
effect: (_, { dispatch, getState }) => {
const { data } = selectQueueStatus(getState());
if (!data || data.processor.is_started) {

View File

@@ -1,14 +1,14 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { setInfillMethod } from 'features/parameters/store/generationSlice';
import { setInfillMethod } from 'features/controlLayers/store/canvasV2Slice';
import { shouldUseNSFWCheckerChanged, shouldUseWatermarkerChanged } from 'features/system/store/systemSlice';
import { appInfoApi } from 'services/api/endpoints/appInfo';
export const addAppConfigReceivedListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: appInfoApi.endpoints.getAppConfig.matchFulfilled,
effect: async (action, { getState, dispatch }) => {
effect: (action, { getState, dispatch }) => {
const { infill_methods = [], nsfw_methods = [], watermarking_methods = [] } = action.payload;
const infillMethod = getState().generation.infillMethod;
const infillMethod = getState().canvasV2.compositing.infillMethod;
if (!infill_methods.includes(infillMethod)) {
// if there is no infill method, set it to the first one

View File

@@ -6,7 +6,7 @@ export const appStarted = createAction('app/appStarted');
export const addAppStartedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: appStarted,
effect: async (action, { unsubscribe, cancelActiveListeners }) => {
effect: (action, { unsubscribe, cancelActiveListeners }) => {
// this should only run once
cancelActiveListeners();
unsubscribe();

View File

@@ -1,27 +1,30 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { parseify } from 'common/util/serialize';
import type { SerializableObject } from 'common/types';
import { zPydanticValidationError } from 'features/system/store/zodSchemas';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { truncate, upperFirst } from 'lodash-es';
import { serializeError } from 'serialize-error';
import { queueApi } from 'services/api/endpoints/queue';
const log = logger('queue');
export const addBatchEnqueuedListener = (startAppListening: AppStartListening) => {
// success
startAppListening({
matcher: queueApi.endpoints.enqueueBatch.matchFulfilled,
effect: async (action) => {
const response = action.payload;
effect: (action) => {
const enqueueResult = action.payload;
const arg = action.meta.arg.originalArgs;
logger('queue').debug({ enqueueResult: parseify(response) }, 'Batch enqueued');
log.debug({ enqueueResult } as SerializableObject, 'Batch enqueued');
toast({
id: 'QUEUE_BATCH_SUCCEEDED',
title: t('queue.batchQueued'),
status: 'success',
description: t('queue.batchQueuedDesc', {
count: response.enqueued,
count: enqueueResult.enqueued,
direction: arg.prepend ? t('queue.front') : t('queue.back'),
}),
});
@@ -31,9 +34,9 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) =
// error
startAppListening({
matcher: queueApi.endpoints.enqueueBatch.matchRejected,
effect: async (action) => {
effect: (action) => {
const response = action.payload;
const arg = action.meta.arg.originalArgs;
const batchConfig = action.meta.arg.originalArgs;
if (!response) {
toast({
@@ -42,7 +45,7 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) =
status: 'error',
description: t('common.unknownError'),
});
logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue'));
log.error({ batchConfig } as SerializableObject, t('queue.batchFailedToQueue'));
return;
}
@@ -68,7 +71,7 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) =
description: t('common.unknownError'),
});
}
logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue'));
log.error({ batchConfig, error: serializeError(response) } as SerializableObject, t('queue.batchFailedToQueue'));
},
});
};

View File

@@ -1,7 +1,4 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice';
import { allLayersDeleted } from 'features/controlLayers/store/controlLayersSlice';
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { imagesApi } from 'services/api/endpoints/images';
@@ -9,39 +6,22 @@ import { imagesApi } from 'services/api/endpoints/images';
export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: imagesApi.endpoints.deleteBoardAndImages.matchFulfilled,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const { deleted_images } = action.payload;
// Remove all deleted images from the UI
let wasCanvasReset = false;
let wasNodeEditorReset = false;
let wereControlAdaptersReset = false;
let wereControlLayersReset = false;
const { canvas, nodes, controlAdapters, controlLayers } = getState();
const { nodes, canvasV2 } = getState();
deleted_images.forEach((image_name) => {
const imageUsage = getImageUsage(canvas, nodes.present, controlAdapters, controlLayers.present, image_name);
if (imageUsage.isCanvasImage && !wasCanvasReset) {
dispatch(resetCanvas());
wasCanvasReset = true;
}
const imageUsage = getImageUsage(nodes.present, canvasV2, image_name);
if (imageUsage.isNodesImage && !wasNodeEditorReset) {
dispatch(nodeEditorReset());
wasNodeEditorReset = true;
}
if (imageUsage.isControlImage && !wereControlAdaptersReset) {
dispatch(controlAdaptersReset());
wereControlAdaptersReset = true;
}
if (imageUsage.isControlLayerImage && !wereControlLayersReset) {
dispatch(allLayersDeleted());
wereControlLayersReset = true;
}
});
},
});

View File

@@ -1,21 +1,15 @@
import { ExternalLink } from '@invoke-ai/ui-library';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
import {
socketBulkDownloadComplete,
socketBulkDownloadError,
socketBulkDownloadStarted,
} from 'services/events/actions';
const log = logger('images');
const log = logger('gallery');
export const addBulkDownloadListeners = (startAppListening: AppStartListening) => {
startAppListening({
matcher: imagesApi.endpoints.bulkDownloadImages.matchFulfilled,
effect: async (action) => {
effect: (action) => {
log.debug(action.payload, 'Bulk download requested');
// If we have an item name, we are processing the bulk download locally and should use it as the toast id to
@@ -33,7 +27,7 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) =
startAppListening({
matcher: imagesApi.endpoints.bulkDownloadImages.matchRejected,
effect: async () => {
effect: () => {
log.debug('Bulk download request failed');
// There isn't any toast to update if we get this event.
@@ -44,55 +38,4 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) =
});
},
});
startAppListening({
actionCreator: socketBulkDownloadStarted,
effect: async (action) => {
// This should always happen immediately after the bulk download request, so we don't need to show a toast here.
log.debug(action.payload.data, 'Bulk download preparation started');
},
});
startAppListening({
actionCreator: socketBulkDownloadComplete,
effect: async (action) => {
log.debug(action.payload.data, 'Bulk download preparation completed');
const { bulk_download_item_name } = action.payload.data;
// TODO(psyche): This URL may break in in some environments (e.g. Nvidia workbench) but we need to test it first
const url = `/api/v1/images/download/${bulk_download_item_name}`;
toast({
id: bulk_download_item_name,
title: t('gallery.bulkDownloadReady', 'Download ready'),
status: 'success',
description: (
<ExternalLink
label={t('gallery.clickToDownload', 'Click here to download')}
href={url}
download={bulk_download_item_name}
/>
),
duration: null,
});
},
});
startAppListening({
actionCreator: socketBulkDownloadError,
effect: async (action) => {
log.debug(action.payload.data, 'Bulk download preparation failed');
const { bulk_download_item_name } = action.payload.data;
toast({
id: bulk_download_item_name,
title: t('gallery.bulkDownloadFailed'),
status: 'error',
description: action.payload.data.error,
duration: null,
});
},
});
};

View File

@@ -1,38 +0,0 @@
import { $logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { canvasCopiedToClipboard } from 'features/canvas/store/actions';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
export const addCanvasCopiedToClipboardListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: canvasCopiedToClipboard,
effect: async (action, { getState }) => {
const moduleLog = $logger.get().child({ namespace: 'canvasCopiedToClipboardListener' });
const state = getState();
try {
const blob = getBaseLayerBlob(state);
copyBlobToClipboard(blob);
} catch (err) {
moduleLog.error(String(err));
toast({
id: 'CANVAS_COPY_FAILED',
title: t('toast.problemCopyingCanvas'),
description: t('toast.problemCopyingCanvasDesc'),
status: 'error',
});
return;
}
toast({
id: 'CANVAS_COPY_SUCCEEDED',
title: t('toast.canvasCopiedClipboard'),
status: 'success',
});
},
});
};

View File

@@ -1,34 +0,0 @@
import { $logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { canvasDownloadedAsImage } from 'features/canvas/store/actions';
import { downloadBlob } from 'features/canvas/util/downloadBlob';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
export const addCanvasDownloadedAsImageListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: canvasDownloadedAsImage,
effect: async (action, { getState }) => {
const moduleLog = $logger.get().child({ namespace: 'canvasSavedToGalleryListener' });
const state = getState();
let blob;
try {
blob = await getBaseLayerBlob(state);
} catch (err) {
moduleLog.error(String(err));
toast({
id: 'CANVAS_DOWNLOAD_FAILED',
title: t('toast.problemDownloadingCanvas'),
description: t('toast.problemDownloadingCanvasDesc'),
status: 'error',
});
return;
}
downloadBlob(blob, 'canvas.png');
toast({ id: 'CANVAS_DOWNLOAD_SUCCEEDED', title: t('toast.canvasDownloaded'), status: 'success' });
},
});
};

View File

@@ -1,60 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { canvasImageToControlAdapter } from 'features/canvas/store/actions';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { controlAdapterImageChanged } from 'features/controlAdapters/store/controlAdaptersSlice';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
export const addCanvasImageToControlNetListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: canvasImageToControlAdapter,
effect: async (action, { dispatch, getState }) => {
const log = logger('canvas');
const state = getState();
const { id } = action.payload;
let blob: Blob;
try {
blob = await getBaseLayerBlob(state, true);
} catch (err) {
log.error(String(err));
toast({
id: 'PROBLEM_SAVING_CANVAS',
title: t('toast.problemSavingCanvas'),
description: t('toast.problemSavingCanvasDesc'),
status: 'error',
});
return;
}
const { autoAddBoardId } = state.gallery;
const imageDTO = await dispatch(
imagesApi.endpoints.uploadImage.initiate({
file: new File([blob], 'savedCanvas.png', {
type: 'image/png',
}),
image_category: 'control',
is_intermediate: true,
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
crop_visible: false,
postUploadAction: {
type: 'TOAST',
title: t('toast.canvasSentControlnetAssets'),
},
})
).unwrap();
const { image_name } = imageDTO;
dispatch(
controlAdapterImageChanged({
id,
controlImage: image_name,
})
);
},
});
};

View File

@@ -1,60 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { canvasMaskSavedToGallery } from 'features/canvas/store/actions';
import { getCanvasData } from 'features/canvas/util/getCanvasData';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
export const addCanvasMaskSavedToGalleryListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: canvasMaskSavedToGallery,
effect: async (action, { dispatch, getState }) => {
const log = logger('canvas');
const state = getState();
const canvasBlobsAndImageData = await getCanvasData(
state.canvas.layerState,
state.canvas.boundingBoxCoordinates,
state.canvas.boundingBoxDimensions,
state.canvas.isMaskEnabled,
state.canvas.shouldPreserveMaskedArea
);
if (!canvasBlobsAndImageData) {
return;
}
const { maskBlob } = canvasBlobsAndImageData;
if (!maskBlob) {
log.error('Problem getting mask layer blob');
toast({
id: 'PROBLEM_SAVING_MASK',
title: t('toast.problemSavingMask'),
description: t('toast.problemSavingMaskDesc'),
status: 'error',
});
return;
}
const { autoAddBoardId } = state.gallery;
dispatch(
imagesApi.endpoints.uploadImage.initiate({
file: new File([maskBlob], 'canvasMaskImage.png', {
type: 'image/png',
}),
image_category: 'mask',
is_intermediate: false,
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
crop_visible: true,
postUploadAction: {
type: 'TOAST',
title: t('toast.maskSavedAssets'),
},
})
);
},
});
};

View File

@@ -1,70 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { canvasMaskToControlAdapter } from 'features/canvas/store/actions';
import { getCanvasData } from 'features/canvas/util/getCanvasData';
import { controlAdapterImageChanged } from 'features/controlAdapters/store/controlAdaptersSlice';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
export const addCanvasMaskToControlNetListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: canvasMaskToControlAdapter,
effect: async (action, { dispatch, getState }) => {
const log = logger('canvas');
const state = getState();
const { id } = action.payload;
const canvasBlobsAndImageData = await getCanvasData(
state.canvas.layerState,
state.canvas.boundingBoxCoordinates,
state.canvas.boundingBoxDimensions,
state.canvas.isMaskEnabled,
state.canvas.shouldPreserveMaskedArea
);
if (!canvasBlobsAndImageData) {
return;
}
const { maskBlob } = canvasBlobsAndImageData;
if (!maskBlob) {
log.error('Problem getting mask layer blob');
toast({
id: 'PROBLEM_IMPORTING_MASK',
title: t('toast.problemImportingMask'),
description: t('toast.problemImportingMaskDesc'),
status: 'error',
});
return;
}
const { autoAddBoardId } = state.gallery;
const imageDTO = await dispatch(
imagesApi.endpoints.uploadImage.initiate({
file: new File([maskBlob], 'canvasMaskImage.png', {
type: 'image/png',
}),
image_category: 'mask',
is_intermediate: true,
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
crop_visible: false,
postUploadAction: {
type: 'TOAST',
title: t('toast.maskSentControlnetAssets'),
},
})
).unwrap();
const { image_name } = imageDTO;
dispatch(
controlAdapterImageChanged({
id,
controlImage: image_name,
})
);
},
});
};

View File

@@ -1,73 +0,0 @@
import { $logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { canvasMerged } from 'features/canvas/store/actions';
import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore';
import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
export const addCanvasMergedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: canvasMerged,
effect: async (action, { dispatch }) => {
const moduleLog = $logger.get().child({ namespace: 'canvasCopiedToClipboardListener' });
const blob = await getFullBaseLayerBlob();
if (!blob) {
moduleLog.error('Problem getting base layer blob');
toast({
id: 'PROBLEM_MERGING_CANVAS',
title: t('toast.problemMergingCanvas'),
description: t('toast.problemMergingCanvasDesc'),
status: 'error',
});
return;
}
const canvasBaseLayer = $canvasBaseLayer.get();
if (!canvasBaseLayer) {
moduleLog.error('Problem getting canvas base layer');
toast({
id: 'PROBLEM_MERGING_CANVAS',
title: t('toast.problemMergingCanvas'),
description: t('toast.problemMergingCanvasDesc'),
status: 'error',
});
return;
}
const baseLayerRect = canvasBaseLayer.getClientRect({
relativeTo: canvasBaseLayer.getParent() ?? undefined,
});
const imageDTO = await dispatch(
imagesApi.endpoints.uploadImage.initiate({
file: new File([blob], 'mergedCanvas.png', {
type: 'image/png',
}),
image_category: 'general',
is_intermediate: true,
postUploadAction: {
type: 'TOAST',
title: t('toast.canvasMerged'),
},
})
).unwrap();
// TODO: I can't figure out how to do the type narrowing in the `take()` so just brute forcing it here
const { image_name } = imageDTO;
dispatch(
setMergedCanvas({
kind: 'image',
layer: 'base',
imageName: image_name,
...baseLayerRect,
})
);
},
});
};

View File

@@ -1,53 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { parseify } from 'common/util/serialize';
import { canvasSavedToGallery } from 'features/canvas/store/actions';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
export const addCanvasSavedToGalleryListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: canvasSavedToGallery,
effect: async (action, { dispatch, getState }) => {
const log = logger('canvas');
const state = getState();
let blob;
try {
blob = await getBaseLayerBlob(state);
} catch (err) {
log.error(String(err));
toast({
id: 'CANVAS_SAVE_FAILED',
title: t('toast.problemSavingCanvas'),
description: t('toast.problemSavingCanvasDesc'),
status: 'error',
});
return;
}
const { autoAddBoardId } = state.gallery;
dispatch(
imagesApi.endpoints.uploadImage.initiate({
file: new File([blob], 'savedCanvas.png', {
type: 'image/png',
}),
image_category: 'general',
is_intermediate: false,
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
crop_visible: true,
postUploadAction: {
type: 'TOAST',
title: t('toast.canvasSavedGallery'),
},
metadata: {
_canvas_objects: parseify(state.canvas.layerState.objects),
},
})
);
},
});
};

View File

@@ -1,194 +0,0 @@
import { isAnyOf } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { AppDispatch } from 'app/store/store';
import { parseify } from 'common/util/serialize';
import {
caLayerImageChanged,
caLayerModelChanged,
caLayerProcessedImageChanged,
caLayerProcessorConfigChanged,
caLayerProcessorPendingBatchIdChanged,
caLayerRecalled,
isControlAdapterLayer,
} from 'features/controlLayers/store/controlLayersSlice';
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { isEqual } from 'lodash-es';
import { getImageDTO } from 'services/api/endpoints/images';
import { queueApi } from 'services/api/endpoints/queue';
import type { BatchConfig } from 'services/api/types';
import { socketInvocationComplete } from 'services/events/actions';
import { assert } from 'tsafe';
const matcher = isAnyOf(
caLayerImageChanged,
caLayerProcessedImageChanged,
caLayerProcessorConfigChanged,
caLayerModelChanged,
caLayerRecalled
);
const DEBOUNCE_MS = 300;
const log = logger('session');
/**
* Simple helper to cancel a batch and reset the pending batch ID
*/
const cancelProcessorBatch = async (dispatch: AppDispatch, layerId: string, batchId: string) => {
const req = dispatch(queueApi.endpoints.cancelByBatchIds.initiate({ batch_ids: [batchId] }));
log.trace({ batchId }, 'Cancelling existing preprocessor batch');
try {
await req.unwrap();
} catch {
// no-op
} finally {
req.reset();
// Always reset the pending batch ID - the cancel req could fail if the batch doesn't exist
dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null }));
}
};
export const addControlAdapterPreprocessor = (startAppListening: AppStartListening) => {
startAppListening({
matcher,
effect: async (action, { dispatch, getState, getOriginalState, cancelActiveListeners, delay, take, signal }) => {
const layerId = caLayerRecalled.match(action) ? action.payload.id : action.payload.layerId;
const state = getState();
const originalState = getOriginalState();
// Cancel any in-progress instances of this listener
cancelActiveListeners();
log.trace('Control Layer CA auto-process triggered');
// Delay before starting actual work
await delay(DEBOUNCE_MS);
const layer = state.controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId);
if (!layer) {
return;
}
// We should only process if the processor settings or image have changed
const originalLayer = originalState.controlLayers.present.layers
.filter(isControlAdapterLayer)
.find((l) => l.id === layerId);
const originalImage = originalLayer?.controlAdapter.image;
const originalConfig = originalLayer?.controlAdapter.processorConfig;
const image = layer.controlAdapter.image;
const processedImage = layer.controlAdapter.processedImage;
const config = layer.controlAdapter.processorConfig;
if (isEqual(config, originalConfig) && isEqual(image, originalImage) && processedImage) {
// Neither config nor image have changed, we can bail
return;
}
if (!image || !config) {
// - If we have no image, we have nothing to process
// - If we have no processor config, we have nothing to process
// Clear the processed image and bail
dispatch(caLayerProcessedImageChanged({ layerId, imageDTO: null }));
return;
}
// At this point, the user has stopped fiddling with the processor settings and there is a processor selected.
// If there is a pending processor batch, cancel it.
if (layer.controlAdapter.processorPendingBatchId) {
cancelProcessorBatch(dispatch, layerId, layer.controlAdapter.processorPendingBatchId);
}
// TODO(psyche): I can't get TS to be happy, it thinkgs `config` is `never` but it should be inferred from the generic... I'll just cast it for now
const processorNode = CA_PROCESSOR_DATA[config.type].buildNode(image, config as never);
const enqueueBatchArg: BatchConfig = {
prepend: true,
batch: {
graph: {
nodes: {
[processorNode.id]: {
...processorNode,
// Control images are always intermediate - do not save to gallery
is_intermediate: true,
},
},
edges: [],
},
runs: 1,
},
};
// Kick off the processor batch
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, {
fixedCacheKey: 'enqueueBatch',
})
);
try {
const enqueueResult = await req.unwrap();
// TODO(psyche): Update the pydantic models, pretty sure we will _always_ have a batch_id here, but the model says it's optional
assert(enqueueResult.batch.batch_id, 'Batch ID not returned from queue');
dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: enqueueResult.batch.batch_id }));
log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued'));
// Wait for the processor node to complete
const [invocationCompleteAction] = await take(
(action): action is ReturnType<typeof socketInvocationComplete> =>
socketInvocationComplete.match(action) &&
action.payload.data.batch_id === enqueueResult.batch.batch_id &&
action.payload.data.invocation_source_id === processorNode.id
);
// We still have to check the output type
assert(
invocationCompleteAction.payload.data.result.type === 'image_output',
`Processor did not return an image output, got: ${invocationCompleteAction.payload.data.result}`
);
const { image_name } = invocationCompleteAction.payload.data.result.image;
const imageDTO = await getImageDTO(image_name);
assert(imageDTO, "Failed to fetch processor output's image DTO");
// Whew! We made it. Update the layer with the processed image
log.debug({ layerId, imageDTO }, 'ControlNet image processed');
dispatch(caLayerProcessedImageChanged({ layerId, imageDTO }));
dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null }));
} catch (error) {
if (signal.aborted) {
// The listener was canceled - we need to cancel the pending processor batch, if there is one (could have changed by now).
const pendingBatchId = getState()
.controlLayers.present.layers.filter(isControlAdapterLayer)
.find((l) => l.id === layerId)?.controlAdapter.processorPendingBatchId;
if (pendingBatchId) {
cancelProcessorBatch(dispatch, layerId, pendingBatchId);
}
log.trace('Control Adapter preprocessor cancelled');
} else {
// Some other error condition...
log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue'));
if (error instanceof Object) {
if ('data' in error && 'status' in error) {
if (error.status === 403) {
dispatch(caLayerImageChanged({ layerId, imageDTO: null }));
return;
}
}
}
toast({
id: 'GRAPH_QUEUE_FAILED',
title: t('queue.graphFailedToQueue'),
status: 'error',
});
}
} finally {
req.reset();
}
},
});
};

View File

@@ -1,85 +0,0 @@
import type { AnyListenerPredicate } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { RootState } from 'app/store/store';
import { controlAdapterImageProcessed } from 'features/controlAdapters/store/actions';
import {
controlAdapterAutoConfigToggled,
controlAdapterImageChanged,
controlAdapterModelChanged,
controlAdapterProcessorParamsChanged,
controlAdapterProcessortTypeChanged,
selectControlAdapterById,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
type AnyControlAdapterParamChangeAction =
| ReturnType<typeof controlAdapterProcessorParamsChanged>
| ReturnType<typeof controlAdapterModelChanged>
| ReturnType<typeof controlAdapterImageChanged>
| ReturnType<typeof controlAdapterProcessortTypeChanged>
| ReturnType<typeof controlAdapterAutoConfigToggled>;
const predicate: AnyListenerPredicate<RootState> = (action, state, prevState) => {
const isActionMatched =
controlAdapterProcessorParamsChanged.match(action) ||
controlAdapterModelChanged.match(action) ||
controlAdapterImageChanged.match(action) ||
controlAdapterProcessortTypeChanged.match(action) ||
controlAdapterAutoConfigToggled.match(action);
if (!isActionMatched) {
return false;
}
const { id } = action.payload;
const prevCA = selectControlAdapterById(prevState.controlAdapters, id);
const ca = selectControlAdapterById(state.controlAdapters, id);
if (!prevCA || !isControlNetOrT2IAdapter(prevCA) || !ca || !isControlNetOrT2IAdapter(ca)) {
return false;
}
if (controlAdapterAutoConfigToggled.match(action)) {
// do not process if the user just disabled auto-config
if (prevCA.shouldAutoConfig === true) {
return false;
}
}
const { controlImage, processorType, shouldAutoConfig } = ca;
if (controlAdapterModelChanged.match(action) && !shouldAutoConfig) {
// do not process if the action is a model change but the processor settings are dirty
return false;
}
const isProcessorSelected = processorType !== 'none';
const hasControlImage = Boolean(controlImage);
return isProcessorSelected && hasControlImage;
};
const DEBOUNCE_MS = 300;
/**
* Listener that automatically processes a ControlNet image when its processor parameters are changed.
*
* The network request is debounced.
*/
export const addControlNetAutoProcessListener = (startAppListening: AppStartListening) => {
startAppListening({
predicate,
effect: async (action, { dispatch, cancelActiveListeners, delay }) => {
const log = logger('session');
const { id } = (action as AnyControlAdapterParamChangeAction).payload;
// Cancel any in-progress instances of this listener
cancelActiveListeners();
log.trace('ControlNet auto-process triggered');
// Delay before starting actual work
await delay(DEBOUNCE_MS);
dispatch(controlAdapterImageProcessed({ id }));
},
});
};

View File

@@ -1,118 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { parseify } from 'common/util/serialize';
import { controlAdapterImageProcessed } from 'features/controlAdapters/store/actions';
import {
controlAdapterImageChanged,
controlAdapterProcessedImageChanged,
pendingControlImagesCleared,
selectControlAdapterById,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
import { queueApi } from 'services/api/endpoints/queue';
import type { BatchConfig, ImageDTO } from 'services/api/types';
import { socketInvocationComplete } from 'services/events/actions';
export const addControlNetImageProcessedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: controlAdapterImageProcessed,
effect: async (action, { dispatch, getState, take }) => {
const log = logger('session');
const { id } = action.payload;
const ca = selectControlAdapterById(getState().controlAdapters, id);
if (!ca?.controlImage || !isControlNetOrT2IAdapter(ca)) {
log.error('Unable to process ControlNet image');
return;
}
if (ca.processorType === 'none' || ca.processorNode.type === 'none') {
return;
}
// ControlNet one-off procressing graph is just the processor node, no edges.
// Also we need to grab the image.
const nodeId = ca.processorNode.id;
const enqueueBatchArg: BatchConfig = {
prepend: true,
batch: {
graph: {
nodes: {
[ca.processorNode.id]: {
...ca.processorNode,
is_intermediate: true,
use_cache: false,
image: { image_name: ca.controlImage },
},
},
edges: [],
},
runs: 1,
},
};
try {
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, {
fixedCacheKey: 'enqueueBatch',
})
);
const enqueueResult = await req.unwrap();
req.reset();
log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued'));
const [invocationCompleteAction] = await take(
(action): action is ReturnType<typeof socketInvocationComplete> =>
socketInvocationComplete.match(action) &&
action.payload.data.batch_id === enqueueResult.batch.batch_id &&
action.payload.data.invocation_source_id === nodeId
);
// We still have to check the output type
if (invocationCompleteAction.payload.data.result.type === 'image_output') {
const { image_name } = invocationCompleteAction.payload.data.result.image;
// Wait for the ImageDTO to be received
const [{ payload }] = await take(
(action) =>
imagesApi.endpoints.getImageDTO.matchFulfilled(action) && action.payload.image_name === image_name
);
const processedControlImage = payload as ImageDTO;
log.debug({ controlNetId: action.payload, processedControlImage }, 'ControlNet image processed');
// Update the processed image in the store
dispatch(
controlAdapterProcessedImageChanged({
id,
processedControlImage: processedControlImage.image_name,
})
);
}
} catch (error) {
log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue'));
if (error instanceof Object) {
if ('data' in error && 'status' in error) {
if (error.status === 403) {
dispatch(pendingControlImagesCleared());
dispatch(controlAdapterImageChanged({ id, controlImage: null }));
return;
}
}
}
toast({
id: 'GRAPH_QUEUE_FAILED',
title: t('queue.graphFailedToQueue'),
status: 'error',
});
}
},
});
};

View File

@@ -1,144 +0,0 @@
import { logger } from 'app/logging/logger';
import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
import { parseify } from 'common/util/serialize';
import { canvasBatchIdAdded, stagingAreaInitialized } from 'features/canvas/store/canvasSlice';
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
import { getCanvasData } from 'features/canvas/util/getCanvasData';
import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode';
import { canvasGraphBuilt } from 'features/nodes/store/actions';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildCanvasGraph } from 'features/nodes/util/graph/canvas/buildCanvasGraph';
import { imagesApi } from 'services/api/endpoints/images';
import { queueApi } from 'services/api/endpoints/queue';
import type { ImageDTO } from 'services/api/types';
/**
* This listener is responsible invoking the canvas. This involves a number of steps:
*
* 1. Generate image blobs from the canvas layers
* 2. Determine the generation mode from the layers (txt2img, img2img, inpaint)
* 3. Build the canvas graph
* 4. Create the session with the graph
* 5. Upload the init image if necessary
* 6. Upload the mask image if necessary
* 7. Update the init and mask images with the session ID
* 8. Initialize the staging area if not yet initialized
* 9. Dispatch the sessionReadyToInvoke action to invoke the session
*/
export const addEnqueueRequestedCanvasListener = (startAppListening: AppStartListening) => {
startAppListening({
predicate: (action): action is ReturnType<typeof enqueueRequested> =>
enqueueRequested.match(action) && action.payload.tabName === 'canvas',
effect: async (action, { getState, dispatch }) => {
const log = logger('queue');
const { prepend } = action.payload;
const state = getState();
const { layerState, boundingBoxCoordinates, boundingBoxDimensions, isMaskEnabled, shouldPreserveMaskedArea } =
state.canvas;
// Build canvas blobs
const canvasBlobsAndImageData = await getCanvasData(
layerState,
boundingBoxCoordinates,
boundingBoxDimensions,
isMaskEnabled,
shouldPreserveMaskedArea
);
if (!canvasBlobsAndImageData) {
log.error('Unable to create canvas data');
return;
}
const { baseBlob, baseImageData, maskBlob, maskImageData } = canvasBlobsAndImageData;
// Determine the generation mode
const generationMode = getCanvasGenerationMode(baseImageData, maskImageData);
if (state.system.enableImageDebugging) {
const baseDataURL = await blobToDataURL(baseBlob);
const maskDataURL = await blobToDataURL(maskBlob);
openBase64ImageInTab([
{ base64: maskDataURL, caption: 'mask b64' },
{ base64: baseDataURL, caption: 'image b64' },
]);
}
log.debug(`Generation mode: ${generationMode}`);
// Temp placeholders for the init and mask images
let canvasInitImage: ImageDTO | undefined;
let canvasMaskImage: ImageDTO | undefined;
// For img2img and inpaint/outpaint, we need to upload the init images
if (['img2img', 'inpaint', 'outpaint'].includes(generationMode)) {
// upload the image, saving the request id
canvasInitImage = await dispatch(
imagesApi.endpoints.uploadImage.initiate({
file: new File([baseBlob], 'canvasInitImage.png', {
type: 'image/png',
}),
image_category: 'general',
is_intermediate: true,
})
).unwrap();
}
// For inpaint/outpaint, we also need to upload the mask layer
if (['inpaint', 'outpaint'].includes(generationMode)) {
// upload the image, saving the request id
canvasMaskImage = await dispatch(
imagesApi.endpoints.uploadImage.initiate({
file: new File([maskBlob], 'canvasMaskImage.png', {
type: 'image/png',
}),
image_category: 'mask',
is_intermediate: true,
})
).unwrap();
}
const graph = await buildCanvasGraph(state, generationMode, canvasInitImage, canvasMaskImage);
log.debug({ graph: parseify(graph) }, `Canvas graph built`);
// currently this action is just listened to for logging
dispatch(canvasGraphBuilt(graph));
const batchConfig = prepareLinearUIBatch(state, graph, prepend);
try {
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
fixedCacheKey: 'enqueueBatch',
})
);
const enqueueResult = await req.unwrap();
req.reset();
const batchId = enqueueResult.batch.batch_id as string; // we know the is a string, backend provides it
// Prep the canvas staging area if it is not yet initialized
if (!state.canvas.layerState.stagingArea.boundingBox) {
dispatch(
stagingAreaInitialized({
boundingBox: {
...state.canvas.boundingBoxCoordinates,
...state.canvas.boundingBoxDimensions,
},
})
);
}
// Associate the session with the canvas session ID
dispatch(canvasBatchIdAdded(batchId));
} catch {
// no-op
}
},
});
};

View File

@@ -1,10 +1,18 @@
import { logger } from 'app/logging/logger';
import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
import { sessionStagingAreaReset, sessionStartedStaging } from 'features/controlLayers/store/canvasV2Slice';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildGenerationTabGraph } from 'features/nodes/util/graph/generation/buildGenerationTabGraph';
import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/generation/buildGenerationTabSDXLGraph';
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import { serializeError } from 'serialize-error';
import { queueApi } from 'services/api/endpoints/queue';
import type { Invocation } from 'services/api/types';
import { assert } from 'tsafe';
const log = logger('generation');
export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => {
startAppListening({
@@ -12,32 +20,54 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
enqueueRequested.match(action) && action.payload.tabName === 'generation',
effect: async (action, { getState, dispatch }) => {
const state = getState();
const { shouldShowProgressInViewer } = state.ui;
const model = state.generation.model;
const model = state.canvasV2.params.model;
const { prepend } = action.payload;
let graph;
const manager = $canvasManager.get();
assert(manager, 'No model found in state');
if (model?.base === 'sdxl') {
graph = await buildGenerationTabSDXLGraph(state);
} else {
graph = await buildGenerationTabGraph(state);
let didStartStaging = false;
if (!state.canvasV2.session.isStaging && state.canvasV2.session.mode === 'compose') {
dispatch(sessionStartedStaging());
didStartStaging = true;
}
const batchConfig = prepareLinearUIBatch(state, graph, prepend);
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
fixedCacheKey: 'enqueueBatch',
})
);
try {
await req.unwrap();
if (shouldShowProgressInViewer) {
dispatch(isImageViewerOpenChanged(true));
let g: Graph;
let noise: Invocation<'noise'>;
let posCond: Invocation<'compel' | 'sdxl_compel_prompt'>;
assert(model, 'No model found in state');
const base = model.base;
if (base === 'sdxl') {
const result = await buildSDXLGraph(state, manager);
g = result.g;
noise = result.noise;
posCond = result.posCond;
} else if (base === 'sd-1' || base === 'sd-2') {
const result = await buildSD1Graph(state, manager);
g = result.g;
noise = result.noise;
posCond = result.posCond;
} else {
assert(false, `No graph builders for base ${base}`);
}
} finally {
const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond);
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
fixedCacheKey: 'enqueueBatch',
})
);
req.reset();
await req.unwrap();
} catch (error) {
log.error({ error: serializeError(error) }, 'Failed to enqueue batch');
if (didStartStaging && getState().canvasV2.session.isStaging) {
dispatch(sessionStagingAreaReset());
}
}
},
});

View File

@@ -29,7 +29,8 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
batch: {
graph,
workflow: builtWorkflow,
runs: state.generation.iterations,
runs: state.canvasV2.params.iterations,
origin: 'workflows',
},
prepend: action.payload.prepend,
};

View File

@@ -14,9 +14,9 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening)
const { shouldShowProgressInViewer } = state.ui;
const { prepend } = action.payload;
const graph = await buildMultidiffusionUpscaleGraph(state);
const { g, noise, posCond } = await buildMultidiffusionUpscaleGraph(state);
const batchConfig = prepareLinearUIBatch(state, graph, prepend);
const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond);
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {

View File

@@ -27,7 +27,7 @@ export const galleryImageClicked = createAction<{
export const addGalleryImageClickedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: galleryImageClicked,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const { imageDTO, shiftKey, ctrlKey, metaKey, altKey } = action.payload;
const state = getState();
const queryArgs = selectListImagesQueryArgs(state);

View File

@@ -1,24 +1,27 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { SerializableObject } from 'common/types';
import { parseify } from 'common/util/serialize';
import { $templates } from 'features/nodes/store/nodesSlice';
import { parseSchema } from 'features/nodes/util/schema/parseSchema';
import { size } from 'lodash-es';
import { serializeError } from 'serialize-error';
import { appInfoApi } from 'services/api/endpoints/appInfo';
const log = logger('system');
export const addGetOpenAPISchemaListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: appInfoApi.endpoints.getOpenAPISchema.matchFulfilled,
effect: (action, { getState }) => {
const log = logger('system');
const schemaJSON = action.payload;
log.debug({ schemaJSON: parseify(schemaJSON) }, 'Received OpenAPI schema');
log.debug({ schemaJSON: parseify(schemaJSON) } as SerializableObject, 'Received OpenAPI schema');
const { nodesAllowlist, nodesDenylist } = getState().config;
const nodeTemplates = parseSchema(schemaJSON, nodesAllowlist, nodesDenylist);
log.debug({ nodeTemplates: parseify(nodeTemplates) }, `Built ${size(nodeTemplates)} node templates`);
log.debug({ nodeTemplates } as SerializableObject, `Built ${size(nodeTemplates)} node templates`);
$templates.set(nodeTemplates);
},
@@ -30,8 +33,7 @@ export const addGetOpenAPISchemaListener = (startAppListening: AppStartListening
// If action.meta.condition === true, the request was canceled/skipped because another request was in flight or
// the value was already in the cache. We don't want to log these errors.
if (!action.meta.condition) {
const log = logger('system');
log.error({ error: parseify(action.error) }, 'Problem retrieving OpenAPI Schema');
log.error({ error: serializeError(action.error) }, 'Problem retrieving OpenAPI Schema');
}
},
});

View File

@@ -2,15 +2,13 @@ import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { imagesApi } from 'services/api/endpoints/images';
const log = logger('gallery');
export const addImageAddedToBoardFulfilledListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: imagesApi.endpoints.addImageToBoard.matchFulfilled,
effect: (action) => {
const log = logger('images');
const { board_id, imageDTO } = action.meta.arg.originalArgs;
// TODO: update listImages cache for this board
log.debug({ board_id, imageDTO }, 'Image added to board');
},
});
@@ -18,9 +16,7 @@ export const addImageAddedToBoardFulfilledListener = (startAppListening: AppStar
startAppListening({
matcher: imagesApi.endpoints.addImageToBoard.matchRejected,
effect: (action) => {
const log = logger('images');
const { board_id, imageDTO } = action.meta.arg.originalArgs;
log.debug({ board_id, imageDTO }, 'Problem adding image to board');
},
});

View File

@@ -1,20 +1,7 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { AppDispatch, RootState } from 'app/store/store';
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import {
controlAdapterImageChanged,
controlAdapterProcessedImageChanged,
selectControlAdapterAll,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import {
isControlAdapterLayer,
isInitialImageLayer,
isIPAdapterLayer,
isRegionalGuidanceLayer,
layerDeleted,
} from 'features/controlLayers/store/controlLayersSlice';
import { entityDeleted, ipaImageChanged } from 'features/controlLayers/store/canvasV2Slice';
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
@@ -26,6 +13,10 @@ import { forEach, intersectionBy } from 'lodash-es';
import { imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
const log = logger('gallery');
//TODO(psyche): handle image deletion (canvas sessions?)
// Some utils to delete images from different parts of the app
const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
state.nodes.present.nodes.forEach((node) => {
@@ -47,52 +38,37 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im
});
};
const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
forEach(selectControlAdapterAll(state.controlAdapters), (ca) => {
if (
ca.controlImage === imageDTO.image_name ||
(isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name)
) {
dispatch(
controlAdapterImageChanged({
id: ca.id,
controlImage: null,
})
);
dispatch(
controlAdapterProcessedImageChanged({
id: ca.id,
processedControlImage: null,
})
);
// const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
// state.canvasV2.controlAdapters.entities.forEach(({ id, imageObject, processedImageObject }) => {
// if (
// imageObject?.image.image_name === imageDTO.image_name ||
// processedImageObject?.image.image_name === imageDTO.image_name
// ) {
// dispatch(caImageChanged({ id, imageDTO: null }));
// dispatch(caProcessedImageChanged({ id, imageDTO: null }));
// }
// });
// };
const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
state.canvasV2.ipAdapters.entities.forEach(({ id, ipAdapter }) => {
if (ipAdapter.image?.image_name === imageDTO.image_name) {
dispatch(ipaImageChanged({ id, imageDTO: null }));
}
});
};
const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
state.controlLayers.present.layers.forEach((l) => {
if (isRegionalGuidanceLayer(l)) {
if (l.ipAdapters.some((ipa) => ipa.image?.name === imageDTO.image_name)) {
dispatch(layerDeleted(l.id));
const deleteLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
state.canvasV2.rasterLayers.entities.forEach(({ id, objects }) => {
let shouldDelete = false;
for (const obj of objects) {
if (obj.type === 'image' && obj.image.image_name === imageDTO.image_name) {
shouldDelete = true;
break;
}
}
if (isControlAdapterLayer(l)) {
if (
l.controlAdapter.image?.name === imageDTO.image_name ||
l.controlAdapter.processedImage?.name === imageDTO.image_name
) {
dispatch(layerDeleted(l.id));
}
}
if (isIPAdapterLayer(l)) {
if (l.ipAdapter.image?.name === imageDTO.image_name) {
dispatch(layerDeleted(l.id));
}
}
if (isInitialImageLayer(l)) {
if (l.image?.name === imageDTO.image_name) {
dispatch(layerDeleted(l.id));
}
if (shouldDelete) {
dispatch(entityDeleted({ entityIdentifier: { id, type: 'raster_layer' } }));
}
});
};
@@ -145,14 +121,10 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening)
}
}
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
if (imageUsage.isCanvasImage) {
dispatch(resetCanvas());
}
deleteControlAdapterImages(state, dispatch, imageDTO);
deleteNodesImages(state, dispatch, imageDTO);
deleteControlLayerImages(state, dispatch, imageDTO);
// deleteControlAdapterImages(state, dispatch, imageDTO);
deleteIPAdapterImages(state, dispatch, imageDTO);
deleteLayerImages(state, dispatch, imageDTO);
} catch {
// no-op
} finally {
@@ -189,14 +161,11 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening)
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
if (imagesUsage.some((i) => i.isCanvasImage)) {
dispatch(resetCanvas());
}
imageDTOs.forEach((imageDTO) => {
deleteControlAdapterImages(state, dispatch, imageDTO);
deleteNodesImages(state, dispatch, imageDTO);
deleteControlLayerImages(state, dispatch, imageDTO);
// deleteControlAdapterImages(state, dispatch, imageDTO);
deleteIPAdapterImages(state, dispatch, imageDTO);
deleteLayerImages(state, dispatch, imageDTO);
});
} catch {
// no-op
@@ -220,7 +189,6 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening)
startAppListening({
matcher: imagesApi.endpoints.deleteImage.matchFulfilled,
effect: (action) => {
const log = logger('images');
log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Image deleted');
},
});
@@ -228,7 +196,6 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening)
startAppListening({
matcher: imagesApi.endpoints.deleteImage.matchRejected,
effect: (action) => {
const log = logger('images');
log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Unable to delete image');
},
});

View File

@@ -1,28 +1,18 @@
import { createAction } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { parseify } from 'common/util/serialize';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import {
controlAdapterImageChanged,
controlAdapterIsEnabledChanged,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import {
caLayerImageChanged,
iiLayerImageChanged,
ipaLayerImageChanged,
rgLayerIPAdapterImageChanged,
} from 'features/controlLayers/store/controlLayersSlice';
controlLayerAdded,
ipaImageChanged,
rasterLayerAdded,
rgIPAdapterImageChanged,
} from 'features/controlLayers/store/canvasV2Slice';
import type { CanvasControlLayerState, CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { isValidDrop } from 'features/dnd/util/isValidDrop';
import {
imageSelected,
imageToCompareChanged,
isImageViewerOpenChanged,
selectionChanged,
} from 'features/gallery/store/gallerySlice';
import { imageToCompareChanged, isImageViewerOpenChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { imagesApi } from 'services/api/endpoints/images';
@@ -31,11 +21,12 @@ export const dndDropped = createAction<{
activeData: TypesafeDraggableData;
}>('dnd/dndDropped');
const log = logger('system');
export const addImageDroppedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: dndDropped,
effect: async (action, { dispatch, getState }) => {
const log = logger('dnd');
effect: (action, { dispatch, getState }) => {
const { activeData, overData } = action.payload;
if (!isValidDrop(overData, activeData)) {
return;
@@ -46,81 +37,21 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
} else if (activeData.payloadType === 'GALLERY_SELECTION') {
log.debug({ activeData, overData }, `Images (${getState().gallery.selection.length}) dropped`);
} else if (activeData.payloadType === 'NODE_FIELD') {
log.debug({ activeData: parseify(activeData), overData: parseify(overData) }, 'Node field dropped');
log.debug({ activeData, overData }, 'Node field dropped');
} else {
log.debug({ activeData, overData }, `Unknown payload dropped`);
}
/**
* Image dropped on current image
*/
if (
overData.actionType === 'SET_CURRENT_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
dispatch(imageSelected(activeData.payload.imageDTO));
dispatch(isImageViewerOpenChanged(true));
return;
}
/**
* Image dropped on ControlNet
*/
if (
overData.actionType === 'SET_CONTROL_ADAPTER_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { id } = overData.context;
dispatch(
controlAdapterImageChanged({
id,
controlImage: activeData.payload.imageDTO.image_name,
})
);
dispatch(
controlAdapterIsEnabledChanged({
id,
isEnabled: true,
})
);
return;
}
/**
* Image dropped on Control Adapter Layer
*/
if (
overData.actionType === 'SET_CA_LAYER_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { layerId } = overData.context;
dispatch(
caLayerImageChanged({
layerId,
imageDTO: activeData.payload.imageDTO,
})
);
return;
}
/**
* Image dropped on IP Adapter Layer
*/
if (
overData.actionType === 'SET_IPA_LAYER_IMAGE' &&
overData.actionType === 'SET_IPA_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { layerId } = overData.context;
dispatch(
ipaLayerImageChanged({
layerId,
imageDTO: activeData.payload.imageDTO,
})
);
const { id } = overData.context;
dispatch(ipaImageChanged({ id, imageDTO: activeData.payload.imageDTO }));
return;
}
@@ -128,48 +59,48 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
* Image dropped on RG Layer IP Adapter
*/
if (
overData.actionType === 'SET_RG_LAYER_IP_ADAPTER_IMAGE' &&
overData.actionType === 'SET_RG_IP_ADAPTER_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { layerId, ipAdapterId } = overData.context;
dispatch(
rgLayerIPAdapterImageChanged({
layerId,
ipAdapterId,
imageDTO: activeData.payload.imageDTO,
})
);
const { id, ipAdapterId } = overData.context;
dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO: activeData.payload.imageDTO }));
return;
}
/**
* Image dropped on II Layer Image
* Image dropped on Raster layer
*/
if (
overData.actionType === 'SET_II_LAYER_IMAGE' &&
overData.actionType === 'ADD_RASTER_LAYER_FROM_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { layerId } = overData.context;
dispatch(
iiLayerImageChanged({
layerId,
imageDTO: activeData.payload.imageDTO,
})
);
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
const { x, y } = getState().canvasV2.bbox.rect;
const overrides: Partial<CanvasRasterLayerState> = {
objects: [imageObject],
position: { x, y },
};
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
return;
}
/**
* Image dropped on Canvas
* Image dropped on Raster layer
*/
if (
overData.actionType === 'SET_CANVAS_INITIAL_IMAGE' &&
overData.actionType === 'ADD_CONTROL_LAYER_FROM_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
dispatch(setInitialCanvasImage(activeData.payload.imageDTO, selectOptimalDimension(getState())));
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
const { x, y } = getState().canvasV2.bbox.rect;
const overrides: Partial<CanvasControlLayerState> = {
objects: [imageObject],
position: { x, y },
};
dispatch(controlLayerAdded({ overrides, isSelected: true }));
return;
}

View File

@@ -2,13 +2,13 @@ import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { imagesApi } from 'services/api/endpoints/images';
const log = logger('gallery');
export const addImageRemovedFromBoardFulfilledListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: imagesApi.endpoints.removeImageFromBoard.matchFulfilled,
effect: (action) => {
const log = logger('images');
const imageDTO = action.meta.arg.originalArgs;
log.debug({ imageDTO }, 'Image removed from board');
},
});
@@ -16,9 +16,7 @@ export const addImageRemovedFromBoardFulfilledListener = (startAppListening: App
startAppListening({
matcher: imagesApi.endpoints.removeImageFromBoard.matchRejected,
effect: (action) => {
const log = logger('images');
const imageDTO = action.meta.arg.originalArgs;
log.debug({ imageDTO }, 'Problem removing image from board');
},
});

View File

@@ -6,16 +6,17 @@ import { imagesToDeleteSelected, isModalOpenChanged } from 'features/deleteImage
export const addImageToDeleteSelectedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: imagesToDeleteSelected,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const imageDTOs = action.payload;
const state = getState();
const { shouldConfirmOnDelete } = state.system;
const imagesUsage = selectImageUsage(getState());
const isImageInUse =
imagesUsage.some((i) => i.isCanvasImage) ||
imagesUsage.some((i) => i.isControlImage) ||
imagesUsage.some((i) => i.isNodesImage);
imagesUsage.some((i) => i.isLayerImage) ||
imagesUsage.some((i) => i.isControlAdapterImage) ||
imagesUsage.some((i) => i.isIPAdapterImage) ||
imagesUsage.some((i) => i.isLayerImage);
if (shouldConfirmOnDelete || isImageInUse) {
dispatch(isModalOpenChanged(true));

View File

@@ -1,19 +1,8 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import {
controlAdapterImageChanged,
controlAdapterIsEnabledChanged,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import {
caLayerImageChanged,
iiLayerImageChanged,
ipaLayerImageChanged,
rgLayerIPAdapterImageChanged,
} from 'features/controlLayers/store/controlLayersSlice';
import { ipaImageChanged, rgIPAdapterImageChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
@@ -21,11 +10,12 @@ import { omit } from 'lodash-es';
import { boardsApi } from 'services/api/endpoints/boards';
import { imagesApi } from 'services/api/endpoints/images';
const log = logger('gallery');
export const addImageUploadedFulfilledListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: imagesApi.endpoints.uploadImage.matchFulfilled,
effect: (action, { dispatch, getState }) => {
const log = logger('images');
const imageDTO = action.payload;
const state = getState();
const { autoAddBoardId } = state.gallery;
@@ -81,15 +71,6 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
return;
}
if (postUploadAction?.type === 'SET_CANVAS_INITIAL_IMAGE') {
dispatch(setInitialCanvasImage(imageDTO, selectOptimalDimension(state)));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setAsCanvasInitialImage'),
});
return;
}
if (postUploadAction?.type === 'SET_UPSCALE_INITIAL_IMAGE') {
dispatch(upscaleInitialImageChanged(imageDTO));
toast({
@@ -99,70 +80,31 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
return;
}
if (postUploadAction?.type === 'SET_CONTROL_ADAPTER_IMAGE') {
// if (postUploadAction?.type === 'SET_CA_IMAGE') {
// const { id } = postUploadAction;
// dispatch(caImageChanged({ id, imageDTO }));
// toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
// return;
// }
if (postUploadAction?.type === 'SET_IPA_IMAGE') {
const { id } = postUploadAction;
dispatch(
controlAdapterIsEnabledChanged({
id,
isEnabled: true,
})
);
dispatch(
controlAdapterImageChanged({
id,
controlImage: imageDTO.image_name,
})
);
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),
});
dispatch(ipaImageChanged({ id, imageDTO }));
toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
return;
}
if (postUploadAction?.type === 'SET_CA_LAYER_IMAGE') {
const { layerId } = postUploadAction;
dispatch(caLayerImageChanged({ layerId, imageDTO }));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),
});
}
if (postUploadAction?.type === 'SET_IPA_LAYER_IMAGE') {
const { layerId } = postUploadAction;
dispatch(ipaLayerImageChanged({ layerId, imageDTO }));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),
});
}
if (postUploadAction?.type === 'SET_RG_LAYER_IP_ADAPTER_IMAGE') {
const { layerId, ipAdapterId } = postUploadAction;
dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO }));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),
});
}
if (postUploadAction?.type === 'SET_II_LAYER_IMAGE') {
const { layerId } = postUploadAction;
dispatch(iiLayerImageChanged({ layerId, imageDTO }));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),
});
if (postUploadAction?.type === 'SET_RG_IP_ADAPTER_IMAGE') {
const { id, ipAdapterId } = postUploadAction;
dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO }));
toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
return;
}
if (postUploadAction?.type === 'SET_NODES_IMAGE') {
const { nodeId, fieldName } = postUploadAction;
dispatch(fieldImageValueChanged({ nodeId, fieldName, value: imageDTO }));
toast({
...DEFAULT_UPLOADED_TOAST,
description: `${t('toast.setNodeField')} ${fieldName}`,
});
toast({ ...DEFAULT_UPLOADED_TOAST, description: `${t('toast.setNodeField')} ${fieldName}` });
return;
}
},
@@ -171,7 +113,6 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
startAppListening({
matcher: imagesApi.endpoints.uploadImage.matchRejected,
effect: (action) => {
const log = logger('images');
const sanitizedData = {
arg: {
...omit(action.meta.arg.originalArgs, ['file', 'postUploadAction']),

View File

@@ -6,7 +6,7 @@ import type { ImageDTO } from 'services/api/types';
export const addImagesStarredListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: imagesApi.endpoints.starImages.matchFulfilled,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const { updated_image_names: starredImages } = action.payload;
const state = getState();

View File

@@ -6,7 +6,7 @@ import type { ImageDTO } from 'services/api/types';
export const addImagesUnstarredListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: imagesApi.endpoints.unstarImages.matchFulfilled,
effect: async (action, { dispatch, getState }) => {
effect: (action, { dispatch, getState }) => {
const { updated_image_names: unstarredImages } = action.payload;
const state = getState();

View File

@@ -1,23 +1,17 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import {
controlAdapterIsEnabledChanged,
selectControlAdapterAll,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { loraRemoved } from 'features/lora/store/loraSlice';
import { loraDeleted, modelChanged, vaeSelected } from 'features/controlLayers/store/canvasV2Slice';
import { modelSelected } from 'features/parameters/store/actions';
import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice';
import { zParameterModel } from 'features/parameters/types/parameterSchemas';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { forEach } from 'lodash-es';
const log = logger('models');
export const addModelSelectedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: modelSelected,
effect: (action, { getState, dispatch }) => {
const log = logger('models');
const state = getState();
const result = zParameterModel.safeParse(action.payload);
@@ -29,34 +23,36 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
const newModel = result.data;
const newBaseModel = newModel.base;
const didBaseModelChange = state.generation.model?.base !== newBaseModel;
const didBaseModelChange = state.canvasV2.params.model?.base !== newBaseModel;
if (didBaseModelChange) {
// we may need to reset some incompatible submodels
let modelsCleared = 0;
// handle incompatible loras
forEach(state.lora.loras, (lora, id) => {
state.canvasV2.loras.forEach((lora) => {
if (lora.model.base !== newBaseModel) {
dispatch(loraRemoved(id));
dispatch(loraDeleted({ id: lora.id }));
modelsCleared += 1;
}
});
// handle incompatible vae
const { vae } = state.generation;
const { vae } = state.canvasV2.params;
if (vae && vae.base !== newBaseModel) {
dispatch(vaeSelected(null));
modelsCleared += 1;
}
// handle incompatible controlnets
selectControlAdapterAll(state.controlAdapters).forEach((ca) => {
if (ca.model?.base !== newBaseModel) {
dispatch(controlAdapterIsEnabledChanged({ id: ca.id, isEnabled: false }));
modelsCleared += 1;
}
});
// state.canvasV2.controlAdapters.entities.forEach((ca) => {
// if (ca.model?.base !== newBaseModel) {
// modelsCleared += 1;
// if (ca.isEnabled) {
// dispatch(entityIsEnabledToggled({ entityIdentifier: { id: ca.id, type: 'control_adapter' } }));
// }
// }
// });
if (modelsCleared > 0) {
toast({
@@ -70,7 +66,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
}
}
dispatch(modelChanged(newModel, state.generation.model));
dispatch(modelChanged({ model: newModel, previousModel: state.canvasV2.params.model }));
},
});
};

View File

@@ -1,36 +1,42 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { AppDispatch, RootState } from 'app/store/store';
import type { JSONObject } from 'common/types';
import type { SerializableObject } from 'common/types';
import {
controlAdapterModelCleared,
selectControlAdapterAll,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
import { loraRemoved } from 'features/lora/store/loraSlice';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice';
bboxHeightChanged,
bboxWidthChanged,
controlLayerModelChanged,
ipaModelChanged,
loraDeleted,
modelChanged,
refinerModelChanged,
rgIPAdapterModelChanged,
vaeSelected,
} from 'features/controlLayers/store/canvasV2Slice';
import { calculateNewSize } from 'features/parameters/components/DocumentSize/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';
import { refinerModelChanged } from 'features/sdxl/store/sdxlSlice';
import { forEach } from 'lodash-es';
import type { Logger } from 'roarr';
import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models';
import type { AnyModelConfig } from 'services/api/types';
import {
isControlNetOrT2IAdapterModelConfig,
isIPAdapterModelConfig,
isLoRAModelConfig,
isNonRefinerMainModelConfig,
isRefinerMainModelModelConfig,
isSpandrelImageToImageModelConfig,
isVAEModelConfig,
} from 'services/api/types';
const log = logger('models');
export const addModelsLoadedListener = (startAppListening: AppStartListening) => {
startAppListening({
predicate: modelsApi.endpoints.getModelConfigs.matchFulfilled,
effect: async (action, { getState, dispatch }) => {
effect: (action, { getState, dispatch }) => {
// models loaded, we need to ensure the selected model is available and if not, select the first one
const log = logger('models');
log.info({ models: action.payload.entities }, `Models loaded (${action.payload.ids.length})`);
const state = getState();
@@ -43,6 +49,7 @@ export const addModelsLoadedListener = (startAppListening: AppStartListening) =>
handleLoRAModels(models, state, dispatch, log);
handleControlAdapterModels(models, state, dispatch, log);
handleSpandrelImageToImageModels(models, state, dispatch, log);
handleIPAdapterModels(models, state, dispatch, log);
},
});
};
@@ -51,15 +58,15 @@ type ModelHandler = (
models: AnyModelConfig[],
state: RootState,
dispatch: AppDispatch,
log: Logger<JSONObject>
log: Logger<SerializableObject>
) => undefined;
const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
const currentModel = state.generation.model;
const currentModel = state.canvasV2.params.model;
const mainModels = models.filter(isNonRefinerMainModelConfig);
if (mainModels.length === 0) {
// No models loaded at all
dispatch(modelChanged(null));
dispatch(modelChanged({ model: null }));
return;
}
@@ -74,25 +81,19 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
if (defaultModelInList) {
const result = zParameterModel.safeParse(defaultModelInList);
if (result.success) {
dispatch(modelChanged(defaultModelInList, currentModel));
dispatch(modelChanged({ model: defaultModelInList, previousModel: currentModel }));
const optimalDimension = getOptimalDimension(defaultModelInList);
if (
getIsSizeOptimal(
state.controlLayers.present.size.width,
state.controlLayers.present.size.height,
optimalDimension
)
) {
if (getIsSizeOptimal(state.canvasV2.bbox.rect.width, state.canvasV2.bbox.rect.height, optimalDimension)) {
return;
}
const { width, height } = calculateNewSize(
state.controlLayers.present.size.aspectRatio.value,
state.canvasV2.bbox.aspectRatio.value,
optimalDimension * optimalDimension
);
dispatch(widthChanged({ width }));
dispatch(heightChanged({ height }));
dispatch(bboxWidthChanged({ width }));
dispatch(bboxHeightChanged({ height }));
return;
}
}
@@ -104,11 +105,11 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
return;
}
dispatch(modelChanged(result.data, currentModel));
dispatch(modelChanged({ model: result.data, previousModel: currentModel }));
};
const handleRefinerModels: ModelHandler = (models, state, dispatch, _log) => {
const currentRefinerModel = state.sdxl.refinerModel;
const currentRefinerModel = state.canvasV2.params.refinerModel;
const refinerModels = models.filter(isRefinerMainModelModelConfig);
if (models.length === 0) {
// No models loaded at all
@@ -127,7 +128,7 @@ const handleRefinerModels: ModelHandler = (models, state, dispatch, _log) => {
};
const handleVAEModels: ModelHandler = (models, state, dispatch, log) => {
const currentVae = state.generation.vae;
const currentVae = state.canvasV2.params.vae;
if (currentVae === null) {
// null is a valid VAE! it means "use the default with the main model"
@@ -160,28 +161,45 @@ const handleVAEModels: ModelHandler = (models, state, dispatch, log) => {
};
const handleLoRAModels: ModelHandler = (models, state, dispatch, _log) => {
const loras = state.lora.loras;
forEach(loras, (lora, id) => {
const isLoRAAvailable = models.some((m) => m.key === lora.model.key);
const loraModels = models.filter(isLoRAModelConfig);
state.canvasV2.loras.forEach((lora) => {
const isLoRAAvailable = loraModels.some((m) => m.key === lora.model.key);
if (isLoRAAvailable) {
return;
}
dispatch(loraRemoved(id));
dispatch(loraDeleted({ id: lora.id }));
});
};
const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log) => {
selectControlAdapterAll(state.controlAdapters).forEach((ca) => {
const isModelAvailable = models.some((m) => m.key === ca.model?.key);
const caModels = models.filter(isControlNetOrT2IAdapterModelConfig);
state.canvasV2.controlLayers.entities.forEach((entity) => {
const isModelAvailable = caModels.some((m) => m.key === entity.controlAdapter.model?.key);
if (isModelAvailable) {
return;
}
dispatch(controlLayerModelChanged({ id: entity.id, modelConfig: null }));
});
};
dispatch(controlAdapterModelCleared({ id: ca.id }));
const handleIPAdapterModels: ModelHandler = (models, state, dispatch, _log) => {
const ipaModels = models.filter(isIPAdapterModelConfig);
state.canvasV2.ipAdapters.entities.forEach((entity) => {
const isModelAvailable = ipaModels.some((m) => m.key === entity.ipAdapter.model?.key);
if (isModelAvailable) {
return;
}
dispatch(ipaModelChanged({ id: entity.id, modelConfig: null }));
});
state.canvasV2.regions.entities.forEach(({ id, ipAdapters }) => {
ipAdapters.forEach(({ id: ipAdapterId, model }) => {
const isModelAvailable = ipaModels.some((m) => m.key === model?.key);
if (isModelAvailable) {
return;
}
dispatch(rgIPAdapterModelChanged({ id, ipAdapterId, modelConfig: null }));
});
});
};

View File

@@ -1,6 +1,6 @@
import { isAnyOf } from '@reduxjs/toolkit';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
import { positivePromptChanged } from 'features/controlLayers/store/canvasV2Slice';
import {
combinatorialToggled,
isErrorChanged,
@@ -15,7 +15,7 @@ import { getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilder
import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
import { stylePresetsApi } from 'services/api/endpoints/stylePresets';
import { utilitiesApi } from 'services/api/endpoints/utilities';
import { socketConnected } from 'services/events/actions';
import { socketConnected } from 'services/events/setEventListeners';
const matcher = isAnyOf(
positivePromptChanged,
@@ -24,8 +24,6 @@ const matcher = isAnyOf(
maxPromptsReset,
socketConnected,
activeStylePresetIdChanged,
stylePresetsApi.endpoints.deleteStylePreset.matchFulfilled,
stylePresetsApi.endpoints.updateStylePreset.matchFulfilled,
stylePresetsApi.endpoints.listStylePresets.matchFulfilled
);

View File

@@ -1,14 +1,15 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
import { setDefaultSettings } from 'features/parameters/store/actions';
import {
bboxHeightChanged,
bboxWidthChanged,
setCfgRescaleMultiplier,
setCfgScale,
setScheduler,
setSteps,
vaePrecisionChanged,
vaeSelected,
} from 'features/parameters/store/generationSlice';
} from 'features/controlLayers/store/canvasV2Slice';
import { setDefaultSettings } from 'features/parameters/store/actions';
import {
isParameterCFGRescaleMultiplier,
isParameterCFGScale,
@@ -30,7 +31,7 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
effect: async (action, { dispatch, getState }) => {
const state = getState();
const currentModel = state.generation.model;
const currentModel = state.canvasV2.params.model;
if (!currentModel) {
return;
@@ -98,13 +99,13 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
const setSizeOptions = { updateAspectRatio: true, clamp: true };
if (width) {
if (isParameterWidth(width)) {
dispatch(widthChanged({ width, ...setSizeOptions }));
dispatch(bboxWidthChanged({ width, ...setSizeOptions }));
}
}
if (height) {
if (isParameterHeight(height)) {
dispatch(heightChanged({ height, ...setSizeOptions }));
dispatch(bboxHeightChanged({ height, ...setSizeOptions }));
}
}

View File

@@ -6,9 +6,9 @@ import { atom } from 'nanostores';
import { api } from 'services/api';
import { modelsApi } from 'services/api/endpoints/models';
import { queueApi, selectQueueStatus } from 'services/api/endpoints/queue';
import { socketConnected } from 'services/events/actions';
import { socketConnected } from 'services/events/setEventListeners';
const log = logger('socketio');
const log = logger('events');
const $isFirstConnection = atom(true);

View File

@@ -1,14 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { socketDisconnected } from 'services/events/actions';
const log = logger('socketio');
export const addSocketDisconnectedEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketDisconnected,
effect: () => {
log.debug('Disconnected');
},
});
};

View File

@@ -1,26 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { deepClone } from 'common/util/deepClone';
import { parseify } from 'common/util/serialize';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
import { zNodeStatus } from 'features/nodes/types/invocation';
import { socketGeneratorProgress } from 'services/events/actions';
const log = logger('socketio');
export const addGeneratorProgressEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketGeneratorProgress,
effect: (action) => {
log.trace(parseify(action.payload), `Generator progress`);
const { invocation_source_id, step, total_steps, progress_image } = action.payload.data;
const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
if (nes) {
nes.status = zNodeStatus.enum.IN_PROGRESS;
nes.progress = (step + 1) / total_steps;
nes.progressImage = progress_image ?? null;
upsertExecutionState(nes.nodeId, nes);
}
},
});
};

View File

@@ -1,122 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { deepClone } from 'common/util/deepClone';
import { parseify } from 'common/util/serialize';
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
import {
boardIdSelected,
galleryViewChanged,
imageSelected,
isImageViewerOpenChanged,
offsetChanged,
} from 'features/gallery/store/gallerySlice';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
import { zNodeStatus } from 'features/nodes/types/invocation';
import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants';
import { boardsApi } from 'services/api/endpoints/boards';
import { imagesApi } from 'services/api/endpoints/images';
import { getCategories, getListImagesUrl } from 'services/api/util';
import { socketInvocationComplete } from 'services/events/actions';
// These nodes output an image, but do not actually *save* an image, so we don't want to handle the gallery logic on them
const nodeTypeDenylist = ['load_image', 'image'];
const log = logger('socketio');
export const addInvocationCompleteEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketInvocationComplete,
effect: async (action, { dispatch, getState }) => {
const { data } = action.payload;
log.debug({ data: parseify(data) }, `Invocation complete (${data.invocation.type})`);
const { result, invocation_source_id } = data;
// This complete event has an associated image output
if (data.result.type === 'image_output' && !nodeTypeDenylist.includes(data.invocation.type)) {
const { image_name } = data.result.image;
const { canvas, gallery } = getState();
// This populates the `getImageDTO` cache
const imageDTORequest = dispatch(
imagesApi.endpoints.getImageDTO.initiate(image_name, {
forceRefetch: true,
})
);
const imageDTO = await imageDTORequest.unwrap();
imageDTORequest.unsubscribe();
// Add canvas images to the staging area
if (canvas.batchIds.includes(data.batch_id) && data.invocation_source_id === CANVAS_OUTPUT) {
dispatch(addImageToStagingArea(imageDTO));
}
if (!imageDTO.is_intermediate) {
// update the total images for the board
dispatch(
boardsApi.util.updateQueryData('getBoardImagesTotal', imageDTO.board_id ?? 'none', (draft) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
draft.total += 1;
})
);
dispatch(
imagesApi.util.invalidateTags([
{ type: 'Board', id: imageDTO.board_id ?? 'none' },
{
type: 'ImageList',
id: getListImagesUrl({
board_id: imageDTO.board_id ?? 'none',
categories: getCategories(imageDTO),
}),
},
])
);
const { shouldAutoSwitch } = gallery;
// If auto-switch is enabled, select the new image
if (shouldAutoSwitch) {
// if auto-add is enabled, switch the gallery view and board if needed as the image comes in
if (gallery.galleryView !== 'images') {
dispatch(galleryViewChanged('images'));
}
if (imageDTO.board_id && imageDTO.board_id !== gallery.selectedBoardId) {
dispatch(
boardIdSelected({
boardId: imageDTO.board_id,
selectedImageName: imageDTO.image_name,
})
);
}
dispatch(offsetChanged({ offset: 0 }));
if (!imageDTO.board_id && gallery.selectedBoardId !== 'none') {
dispatch(
boardIdSelected({
boardId: 'none',
selectedImageName: imageDTO.image_name,
})
);
}
dispatch(imageSelected(imageDTO));
dispatch(isImageViewerOpenChanged(true));
}
}
}
const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
if (nes) {
nes.status = zNodeStatus.enum.COMPLETED;
if (nes.progress !== null) {
nes.progress = 1;
}
nes.outputs.push(result);
upsertExecutionState(nes.nodeId, nes);
}
},
});
};

View File

@@ -1,31 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { deepClone } from 'common/util/deepClone';
import { parseify } from 'common/util/serialize';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
import { zNodeStatus } from 'features/nodes/types/invocation';
import { socketInvocationError } from 'services/events/actions';
const log = logger('socketio');
export const addInvocationErrorEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketInvocationError,
effect: (action) => {
const { invocation_source_id, invocation, error_type, error_message, error_traceback } = action.payload.data;
log.error(parseify(action.payload), `Invocation error (${invocation.type})`);
const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
if (nes) {
nes.status = zNodeStatus.enum.FAILED;
nes.progress = null;
nes.progressImage = null;
nes.error = {
error_type,
error_message,
error_traceback,
};
upsertExecutionState(nes.nodeId, nes);
}
},
});
};

View File

@@ -1,24 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { deepClone } from 'common/util/deepClone';
import { parseify } from 'common/util/serialize';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
import { zNodeStatus } from 'features/nodes/types/invocation';
import { socketInvocationStarted } from 'services/events/actions';
const log = logger('socketio');
export const addInvocationStartedEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketInvocationStarted,
effect: (action) => {
log.debug(parseify(action.payload), `Invocation started (${action.payload.data.invocation.type})`);
const { invocation_source_id } = action.payload.data;
const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
if (nes) {
nes.status = zNodeStatus.enum.IN_PROGRESS;
upsertExecutionState(nes.nodeId, nes);
}
},
});
};

View File

@@ -1,196 +0,0 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { api, LIST_TAG } from 'services/api';
import { modelsApi } from 'services/api/endpoints/models';
import {
socketModelInstallCancelled,
socketModelInstallComplete,
socketModelInstallDownloadProgress,
socketModelInstallDownloadsComplete,
socketModelInstallDownloadStarted,
socketModelInstallError,
socketModelInstallStarted,
} from 'services/events/actions';
/**
* A model install has two main stages - downloading and installing. All these events are namespaced under `model_install_`
* which is a bit misleading. For example, a `model_install_started` event is actually fired _after_ the model has fully
* downloaded and is being "physically" installed.
*
* Note: the download events are only fired for remote model installs, not local.
*
* Here's the expected flow:
* - API receives install request, model manager preps the install
* - `model_install_download_started` fired when the download starts
* - `model_install_download_progress` fired continually until the download is complete
* - `model_install_download_complete` fired when the download is complete
* - `model_install_started` fired when the "physical" installation starts
* - `model_install_complete` fired when the installation is complete
* - `model_install_cancelled` fired if the installation is cancelled
* - `model_install_error` fired if the installation has an error
*/
const selectModelInstalls = modelsApi.endpoints.listModelInstalls.select();
export const addModelInstallEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketModelInstallDownloadStarted,
effect: async (action, { dispatch, getState }) => {
const { id } = action.payload.data;
const { data } = selectModelInstalls(getState());
if (!data || !data.find((m) => m.id === id)) {
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
} else {
dispatch(
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
const modelImport = draft.find((m) => m.id === id);
if (modelImport) {
modelImport.status = 'downloading';
}
return draft;
})
);
}
},
});
startAppListening({
actionCreator: socketModelInstallStarted,
effect: async (action, { dispatch, getState }) => {
const { id } = action.payload.data;
const { data } = selectModelInstalls(getState());
if (!data || !data.find((m) => m.id === id)) {
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
} else {
dispatch(
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
const modelImport = draft.find((m) => m.id === id);
if (modelImport) {
modelImport.status = 'running';
}
return draft;
})
);
}
},
});
startAppListening({
actionCreator: socketModelInstallDownloadProgress,
effect: async (action, { dispatch, getState }) => {
const { bytes, total_bytes, id } = action.payload.data;
const { data } = selectModelInstalls(getState());
if (!data || !data.find((m) => m.id === id)) {
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
} else {
dispatch(
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
const modelImport = draft.find((m) => m.id === id);
if (modelImport) {
modelImport.bytes = bytes;
modelImport.total_bytes = total_bytes;
modelImport.status = 'downloading';
}
return draft;
})
);
}
},
});
startAppListening({
actionCreator: socketModelInstallComplete,
effect: (action, { dispatch, getState }) => {
const { id } = action.payload.data;
const { data } = selectModelInstalls(getState());
if (!data || !data.find((m) => m.id === id)) {
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
} else {
dispatch(
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
const modelImport = draft.find((m) => m.id === id);
if (modelImport) {
modelImport.status = 'completed';
}
return draft;
})
);
}
dispatch(api.util.invalidateTags([{ type: 'ModelConfig', id: LIST_TAG }]));
dispatch(api.util.invalidateTags([{ type: 'ModelScanFolderResults', id: LIST_TAG }]));
},
});
startAppListening({
actionCreator: socketModelInstallError,
effect: (action, { dispatch, getState }) => {
const { id, error, error_type } = action.payload.data;
const { data } = selectModelInstalls(getState());
if (!data || !data.find((m) => m.id === id)) {
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
} else {
dispatch(
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
const modelImport = draft.find((m) => m.id === id);
if (modelImport) {
modelImport.status = 'error';
modelImport.error_reason = error_type;
modelImport.error = error;
}
return draft;
})
);
}
},
});
startAppListening({
actionCreator: socketModelInstallCancelled,
effect: (action, { dispatch, getState }) => {
const { id } = action.payload.data;
const { data } = selectModelInstalls(getState());
if (!data || !data.find((m) => m.id === id)) {
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
} else {
dispatch(
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
const modelImport = draft.find((m) => m.id === id);
if (modelImport) {
modelImport.status = 'cancelled';
}
return draft;
})
);
}
},
});
startAppListening({
actionCreator: socketModelInstallDownloadsComplete,
effect: (action, { dispatch, getState }) => {
const { id } = action.payload.data;
const { data } = selectModelInstalls(getState());
if (!data || !data.find((m) => m.id === id)) {
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
} else {
dispatch(
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
const modelImport = draft.find((m) => m.id === id);
if (modelImport) {
modelImport.status = 'downloads_done';
}
return draft;
})
);
}
},
});
};

View File

@@ -1,42 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { socketModelLoadComplete, socketModelLoadStarted } from 'services/events/actions';
const log = logger('socketio');
export const addModelLoadEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketModelLoadStarted,
effect: (action) => {
const { config, submodel_type } = action.payload.data;
const { name, base, type } = config;
const extras: string[] = [base, type];
if (submodel_type) {
extras.push(submodel_type);
}
const message = `Model load started: ${name} (${extras.join(', ')})`;
log.debug(action.payload, message);
},
});
startAppListening({
actionCreator: socketModelLoadComplete,
effect: (action) => {
const { config, submodel_type } = action.payload.data;
const { name, base, type } = config;
const extras: string[] = [base, type];
if (submodel_type) {
extras.push(submodel_type);
}
const message = `Model load complete: ${name} (${extras.join(', ')})`;
log.debug(action.payload, message);
},
});
};

View File

@@ -1,114 +0,0 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { deepClone } from 'common/util/deepClone';
import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState';
import { zNodeStatus } from 'features/nodes/types/invocation';
import ErrorToastDescription, { getTitleFromErrorType } from 'features/toast/ErrorToastDescription';
import { toast } from 'features/toast/toast';
import { forEach } from 'lodash-es';
import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue';
import { socketQueueItemStatusChanged } from 'services/events/actions';
const log = logger('socketio');
export const addSocketQueueItemStatusChangedEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketQueueItemStatusChanged,
effect: async (action, { dispatch, getState }) => {
// we've got new status for the queue item, batch and queue
const {
item_id,
session_id,
status,
started_at,
updated_at,
completed_at,
batch_status,
queue_status,
error_type,
error_message,
error_traceback,
} = action.payload.data;
log.debug(action.payload, `Queue item ${item_id} status updated: ${status}`);
// Update this specific queue item in the list of queue items (this is the queue item DTO, without the session)
dispatch(
queueApi.util.updateQueryData('listQueueItems', undefined, (draft) => {
queueItemsAdapter.updateOne(draft, {
id: String(item_id),
changes: {
status,
started_at,
updated_at: updated_at ?? undefined,
completed_at: completed_at ?? undefined,
error_type,
error_message,
error_traceback,
},
});
})
);
// Update the queue status (we do not get the processor status here)
dispatch(
queueApi.util.updateQueryData('getQueueStatus', undefined, (draft) => {
if (!draft) {
return;
}
Object.assign(draft.queue, queue_status);
})
);
// Update the batch status
dispatch(
queueApi.util.updateQueryData('getBatchStatus', { batch_id: batch_status.batch_id }, () => batch_status)
);
// Invalidate caches for things we cannot update
// TODO: technically, we could possibly update the current session queue item, but feels safer to just request it again
dispatch(
queueApi.util.invalidateTags([
'CurrentSessionQueueItem',
'NextSessionQueueItem',
'InvocationCacheStatus',
{ type: 'SessionQueueItem', id: item_id },
])
);
if (status === 'in_progress') {
forEach($nodeExecutionStates.get(), (nes) => {
if (!nes) {
return;
}
const clone = deepClone(nes);
clone.status = zNodeStatus.enum.PENDING;
clone.error = null;
clone.progress = null;
clone.progressImage = null;
clone.outputs = [];
$nodeExecutionStates.setKey(clone.nodeId, clone);
});
} else if (status === 'failed' && error_type) {
const isLocal = getState().config.isLocal ?? true;
const sessionId = session_id;
toast({
id: `INVOCATION_ERROR_${error_type}`,
title: getTitleFromErrorType(error_type),
status: 'error',
duration: null,
updateDescription: isLocal,
description: (
<ErrorToastDescription
errorType={error_type}
errorMessage={error_message}
sessionId={sessionId}
isLocal={isLocal}
/>
),
});
}
},
});
};

View File

@@ -1,43 +0,0 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { stagingAreaImageSaved } from 'features/canvas/store/actions';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
export const addStagingAreaImageSavedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: stagingAreaImageSaved,
effect: async (action, { dispatch, getState }) => {
const { imageDTO } = action.payload;
try {
const newImageDTO = await dispatch(
imagesApi.endpoints.changeImageIsIntermediate.initiate({
imageDTO,
is_intermediate: false,
})
).unwrap();
// we may need to add it to the autoadd board
const { autoAddBoardId } = getState().gallery;
if (autoAddBoardId && autoAddBoardId !== 'none') {
await dispatch(
imagesApi.endpoints.addImageToBoard.initiate({
imageDTO: newImageDTO,
board_id: autoAddBoardId,
})
);
}
toast({ id: 'IMAGE_SAVED', title: t('toast.imageSaved'), status: 'success' });
} catch (error) {
toast({
id: 'IMAGE_SAVE_FAILED',
title: t('toast.imageSavingFailed'),
description: (error as Error)?.message,
status: 'error',
});
}
},
});
};

View File

@@ -8,11 +8,12 @@ import { getNeedsUpdate, updateNode } from 'features/nodes/util/node/nodeUpdate'
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
const log = logger('workflows');
export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: updateAllNodesRequested,
effect: (action, { dispatch, getState }) => {
const log = logger('nodes');
const { nodes } = getState().nodes.present;
const templates = $templates.get();

View File

@@ -1,6 +1,6 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { parseify } from 'common/util/serialize';
import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState';
import { workflowLoaded, workflowLoadRequested } from 'features/nodes/store/actions';
import { $templates } from 'features/nodes/store/nodesSlice';
import { $needsFit } from 'features/nodes/store/reactFlowInstance';
@@ -10,11 +10,14 @@ import { graphToWorkflow } from 'features/nodes/util/workflow/graphToWorkflow';
import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { serializeError } from 'serialize-error';
import { checkBoardAccess, checkImageAccess, checkModelAccess } from 'services/api/hooks/accessChecks';
import type { GraphAndWorkflowResponse, NonNullableGraph } from 'services/api/types';
import { z } from 'zod';
import { fromZodError } from 'zod-validation-error';
const log = logger('workflows');
const getWorkflow = async (data: GraphAndWorkflowResponse, templates: Templates) => {
if (data.workflow) {
// Prefer to load the workflow if it's available - it has more information
@@ -34,7 +37,6 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
startAppListening({
actionCreator: workflowLoadRequested,
effect: async (action, { dispatch }) => {
const log = logger('nodes');
const { data, asCopy } = action.payload;
const nodeTemplates = $templates.get();
@@ -46,6 +48,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
delete workflow.id;
}
$nodeExecutionStates.set({});
dispatch(workflowLoaded(workflow));
if (!warnings.length) {
toast({
@@ -69,7 +72,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
} catch (e) {
if (e instanceof WorkflowVersionError) {
// The workflow version was not recognized in the valid list of versions
log.error({ error: parseify(e) }, e.message);
log.error({ error: serializeError(e) }, e.message);
toast({
id: 'UNABLE_TO_VALIDATE_WORKFLOW',
title: t('nodes.unableToValidateWorkflow'),
@@ -78,7 +81,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
});
} else if (e instanceof WorkflowMigrationError) {
// There was a problem migrating the workflow to the latest version
log.error({ error: parseify(e) }, e.message);
log.error({ error: serializeError(e) }, e.message);
toast({
id: 'UNABLE_TO_VALIDATE_WORKFLOW',
title: t('nodes.unableToValidateWorkflow'),
@@ -90,7 +93,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
const { message } = fromZodError(e, {
prefix: t('nodes.workflowValidation'),
});
log.error({ error: parseify(e) }, message);
log.error({ error: serializeError(e) }, message);
toast({
id: 'UNABLE_TO_VALIDATE_WORKFLOW',
title: t('nodes.unableToValidateWorkflow'),
@@ -99,7 +102,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
});
} else {
// Some other error occurred
log.error({ error: parseify(e) }, t('nodes.unknownErrorValidatingWorkflow'));
log.error({ error: serializeError(e) }, t('nodes.unknownErrorValidatingWorkflow'));
toast({
id: 'UNABLE_TO_VALIDATE_WORKFLOW',
title: t('nodes.unableToValidateWorkflow'),

View File

@@ -1,4 +1,5 @@
import type { createStore } from 'app/store/store';
import { useStore } from '@nanostores/react';
import type { AppStore } from 'app/store/store';
import { atom } from 'nanostores';
// Inject socket options and url into window for debugging
@@ -22,7 +23,7 @@ class ReduxStoreNotInitialized extends Error {
}
}
export const $store = atom<Readonly<ReturnType<typeof createStore>> | undefined>();
export const $store = atom<Readonly<AppStore | undefined>>();
export const getStore = () => {
const store = $store.get();
@@ -31,3 +32,11 @@ export const getStore = () => {
}
return store;
};
export const useAppStore = () => {
const store = useStore($store);
if (!store) {
throw new ReduxStoreNotInitialized();
}
return store;
};

View File

@@ -3,37 +3,26 @@ import { autoBatchEnhancer, combineReducers, configureStore } from '@reduxjs/too
import { logger } from 'app/logging/logger';
import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver';
import { errorHandler } from 'app/store/enhancers/reduxRemember/errors';
import type { JSONObject } from 'common/types';
import { canvasPersistConfig, canvasSlice } from 'features/canvas/store/canvasSlice';
import type { SerializableObject } from 'common/types';
import { deepClone } from 'common/util/deepClone';
import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
import {
controlAdaptersPersistConfig,
controlAdaptersSlice,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import {
controlLayersPersistConfig,
controlLayersSlice,
controlLayersUndoableConfig,
} from 'features/controlLayers/store/controlLayersSlice';
import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice';
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice';
import { loraPersistConfig, loraSlice } from 'features/lora/store/loraSlice';
import { modelManagerV2PersistConfig, modelManagerV2Slice } from 'features/modelManagerV2/store/modelManagerV2Slice';
import { nodesPersistConfig, nodesSlice, nodesUndoableConfig } from 'features/nodes/store/nodesSlice';
import { workflowSettingsPersistConfig, workflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice';
import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice';
import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/upscaleSlice';
import { queueSlice } from 'features/queue/store/queueSlice';
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
import { stylePresetPersistConfig, stylePresetSlice } from 'features/stylePresets/store/stylePresetSlice';
import { configSlice } from 'features/system/store/configSlice';
import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice';
import { uiPersistConfig, uiSlice } from 'features/ui/store/uiSlice';
import { diff } from 'jsondiffpatch';
import { defaultsDeep, keys, omit, pick } from 'lodash-es';
import { keys, mergeWith, omit, pick } from 'lodash-es';
import dynamicMiddlewares from 'redux-dynamic-middlewares';
import type { SerializeFunction, UnserializeFunction } from 'redux-remember';
import { rememberEnhancer, rememberReducer } from 'redux-remember';
@@ -48,27 +37,24 @@ import { actionsDenylist } from './middleware/devtools/actionsDenylist';
import { stateSanitizer } from './middleware/devtools/stateSanitizer';
import { listenerMiddleware } from './middleware/listenerMiddleware';
const log = logger('system');
const allReducers = {
[canvasSlice.name]: canvasSlice.reducer,
[api.reducerPath]: api.reducer,
[gallerySlice.name]: gallerySlice.reducer,
[generationSlice.name]: generationSlice.reducer,
[nodesSlice.name]: undoable(nodesSlice.reducer, nodesUndoableConfig),
[systemSlice.name]: systemSlice.reducer,
[configSlice.name]: configSlice.reducer,
[uiSlice.name]: uiSlice.reducer,
[controlAdaptersSlice.name]: controlAdaptersSlice.reducer,
[dynamicPromptsSlice.name]: dynamicPromptsSlice.reducer,
[deleteImageModalSlice.name]: deleteImageModalSlice.reducer,
[changeBoardModalSlice.name]: changeBoardModalSlice.reducer,
[loraSlice.name]: loraSlice.reducer,
[modelManagerV2Slice.name]: modelManagerV2Slice.reducer,
[sdxlSlice.name]: sdxlSlice.reducer,
[queueSlice.name]: queueSlice.reducer,
[workflowSlice.name]: workflowSlice.reducer,
[hrfSlice.name]: hrfSlice.reducer,
[controlLayersSlice.name]: undoable(controlLayersSlice.reducer, controlLayersUndoableConfig),
[canvasV2Slice.name]: canvasV2Slice.reducer,
[workflowSettingsSlice.name]: workflowSettingsSlice.reducer,
[api.reducerPath]: api.reducer,
[upscaleSlice.name]: upscaleSlice.reducer,
[stylePresetSlice.name]: stylePresetSlice.reducer,
};
@@ -100,27 +86,21 @@ export type PersistConfig<T = any> = {
};
const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
[canvasPersistConfig.name]: canvasPersistConfig,
[galleryPersistConfig.name]: galleryPersistConfig,
[generationPersistConfig.name]: generationPersistConfig,
[nodesPersistConfig.name]: nodesPersistConfig,
[systemPersistConfig.name]: systemPersistConfig,
[workflowPersistConfig.name]: workflowPersistConfig,
[uiPersistConfig.name]: uiPersistConfig,
[controlAdaptersPersistConfig.name]: controlAdaptersPersistConfig,
[dynamicPromptsPersistConfig.name]: dynamicPromptsPersistConfig,
[sdxlPersistConfig.name]: sdxlPersistConfig,
[loraPersistConfig.name]: loraPersistConfig,
[modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig,
[hrfPersistConfig.name]: hrfPersistConfig,
[controlLayersPersistConfig.name]: controlLayersPersistConfig,
[canvasV2PersistConfig.name]: canvasV2PersistConfig,
[workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig,
[upscalePersistConfig.name]: upscalePersistConfig,
[stylePresetPersistConfig.name]: stylePresetPersistConfig,
};
const unserialize: UnserializeFunction = (data, key) => {
const log = logger('system');
const persistConfig = persistConfigs[key as keyof typeof persistConfigs];
if (!persistConfig) {
throw new Error(`No persist config for slice "${key}"`);
@@ -130,17 +110,21 @@ const unserialize: UnserializeFunction = (data, key) => {
const parsed = JSON.parse(data);
// strip out old keys
const stripped = pick(parsed, keys(initialState));
const stripped = pick(deepClone(parsed), keys(initialState));
// run (additive) migrations
const migrated = migrate(stripped);
// merge in initial state as default values, covering any missing keys
const transformed = defaultsDeep(migrated, initialState);
/*
* Merge in initial state as default values, covering any missing keys. You might be tempted to use _.defaultsDeep,
* but that merges arrays by index and partial objects by key. Using an identity function as the customizer results
* in behaviour like defaultsDeep, but doesn't overwrite any values that are not undefined in the migrated state.
*/
const transformed = mergeWith(migrated, initialState, (objVal) => objVal);
log.debug(
{
persistedData: parsed,
rehydratedData: transformed,
diff: diff(parsed, transformed) as JSONObject, // this is always serializable
diff: diff(parsed, transformed) as SerializableObject, // this is always serializable
},
`Rehydrated slice "${key}"`
);
@@ -202,7 +186,8 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
},
});
export type RootState = ReturnType<ReturnType<typeof createStore>['getState']>;
export type AppStore = ReturnType<typeof createStore>;
export type RootState = ReturnType<AppStore['getState']>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AppThunkDispatch = ThunkDispatch<RootState, any, UnknownAction>;
export type AppDispatch = ReturnType<typeof createStore>['dispatch'];

View File

@@ -1,6 +1,6 @@
import type { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants';
import type { FilterType } from 'features/controlLayers/store/types';
import type { ParameterPrecision, ParameterScheduler } from 'features/parameters/types/parameterSchemas';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import type { TabName } from 'features/ui/store/uiTypes';
import type { O } from 'ts-toolbelt';
/**
@@ -72,7 +72,7 @@ export type AppConfig = {
maxUpscaleDimension?: number;
allowPrivateBoards: boolean;
allowPrivateStylePresets: boolean;
disabledTabs: InvokeTabName[];
disabledTabs: TabName[];
disabledFeatures: AppFeature[];
disabledSDFeatures: SDFeature[];
nodesAllowlist: string[] | undefined;
@@ -83,7 +83,7 @@ export type AppConfig = {
sd: {
defaultModel?: string;
disabledControlNetModels: string[];
disabledControlNetProcessors: (keyof typeof CONTROLNET_PROCESSORS)[];
disabledControlNetProcessors: FilterType[];
// Core parameters
iterations: NumericalParameterConfig;
width: NumericalParameterConfig; // initial value comes from model

View File

@@ -33,28 +33,23 @@ type IAINoImageFallbackProps = FlexProps & {
};
export const IAINoContentFallback = memo((props: IAINoImageFallbackProps) => {
const { icon = PiImageBold, boxSize = 16, sx, ...rest } = props;
const styles = useMemo(
() => ({
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'base',
flexDir: 'column',
gap: 2,
userSelect: 'none',
opacity: 0.7,
color: 'base.500',
fontSize: 'md',
...sx,
}),
[sx]
);
const { icon = PiImageBold, boxSize = 16, ...rest } = props;
return (
<Flex sx={styles} {...rest}>
<Flex
w="full"
h="full"
alignItems="center"
justifyContent="center"
borderRadius="base"
flexDir="column"
gap={2}
userSelect="none"
opacity={0.7}
color="base.500"
fontSize="md"
{...rest}
>
{icon && <Icon as={icon} boxSize={boxSize} opacity={0.7} />}
{props.label && <Text textAlign="center">{props.label}</Text>}
</Flex>

View File

@@ -0,0 +1,158 @@
import { logger } from 'app/logging/logger';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { objectKeys } from 'common/util/objectKeys';
import { isEqual } from 'lodash-es';
import type { Atom } from 'nanostores';
import { atom, computed } from 'nanostores';
import type { RefObject } from 'react';
import { useEffect, useMemo } from 'react';
const log = logger('system');
const _INTERACTION_SCOPES = ['gallery', 'canvas', 'stagingArea', 'workflows', 'imageViewer'] as const;
type InteractionScope = (typeof _INTERACTION_SCOPES)[number];
export const $activeScopes = atom<Set<InteractionScope>>(new Set());
type InteractionScopeData = {
targets: Set<HTMLElement>;
$isActive: Atom<boolean>;
};
export const INTERACTION_SCOPES: Record<InteractionScope, InteractionScopeData> = _INTERACTION_SCOPES.reduce(
(acc, region) => {
acc[region] = {
targets: new Set(),
$isActive: computed($activeScopes, (activeScopes) => activeScopes.has(region)),
};
return acc;
},
{} as Record<InteractionScope, InteractionScopeData>
);
const formatScopes = (interactionScopes: Set<InteractionScope>) => {
if (interactionScopes.size === 0) {
return 'none';
}
return Array.from(interactionScopes).join(', ');
};
export const addScope = (scope: InteractionScope) => {
const currentScopes = $activeScopes.get();
if (currentScopes.has(scope)) {
return;
}
const newScopes = new Set(currentScopes);
newScopes.add(scope);
$activeScopes.set(newScopes);
log.trace(`Added scope ${scope}: ${formatScopes($activeScopes.get())}`);
};
export const removeScope = (scope: InteractionScope) => {
const currentScopes = $activeScopes.get();
if (!currentScopes.has(scope)) {
return;
}
const newScopes = new Set(currentScopes);
newScopes.delete(scope);
$activeScopes.set(newScopes);
log.trace(`Removed scope ${scope}: ${formatScopes($activeScopes.get())}`);
};
export const setScopes = (scopes: InteractionScope[]) => {
const newScopes = new Set(scopes);
$activeScopes.set(newScopes);
log.trace(`Set scopes: ${formatScopes($activeScopes.get())}`);
};
export const useScopeOnFocus = (scope: InteractionScope, ref: RefObject<HTMLElement>) => {
useEffect(() => {
const element = ref.current;
if (!element) {
return;
}
INTERACTION_SCOPES[scope].targets.add(element);
return () => {
INTERACTION_SCOPES[scope].targets.delete(element);
};
}, [ref, scope]);
};
type UseScopeOnMountOptions = {
mount?: boolean;
unmount?: boolean;
};
const defaultUseScopeOnMountOptions: UseScopeOnMountOptions = {
mount: true,
unmount: true,
};
export const useScopeOnMount = (scope: InteractionScope, options?: UseScopeOnMountOptions) => {
useEffect(() => {
const { mount, unmount } = { ...defaultUseScopeOnMountOptions, ...options };
if (mount) {
addScope(scope);
}
return () => {
if (unmount) {
removeScope(scope);
}
};
}, [options, scope]);
};
export const useScopeImperativeApi = (scope: InteractionScope) => {
const api = useMemo(() => {
return {
add: () => {
addScope(scope);
},
remove: () => {
removeScope(scope);
},
};
}, [scope]);
return api;
};
const handleFocusEvent = (_event: FocusEvent) => {
const activeElement = document.activeElement;
if (!(activeElement instanceof HTMLElement)) {
return;
}
const newActiveScopes = new Set<InteractionScope>();
for (const scope of objectKeys(INTERACTION_SCOPES)) {
for (const element of INTERACTION_SCOPES[scope].targets) {
if (element.contains(activeElement)) {
newActiveScopes.add(scope);
}
}
}
const oldActiveScopes = $activeScopes.get();
if (!isEqual(oldActiveScopes, newActiveScopes)) {
$activeScopes.set(newActiveScopes);
log.trace(`Scopes changed: ${formatScopes($activeScopes.get())}`);
}
};
export const useScopeFocusWatcher = () => {
useAssertSingleton('useScopeFocusWatcher');
useEffect(() => {
window.addEventListener('focus', handleFocusEvent, true);
return () => {
window.removeEventListener('focus', handleFocusEvent, true);
};
}, []);
};

View File

@@ -1,3 +1,4 @@
import type { WritableAtom } from 'nanostores';
import { useCallback, useMemo, useState } from 'react';
export const useBoolean = (initialValue: boolean) => {
@@ -19,3 +20,33 @@ export const useBoolean = (initialValue: boolean) => {
return api;
};
export const buildUseBoolean = ($boolean: WritableAtom<boolean>) => {
return () => {
const setTrue = useCallback(() => {
$boolean.set(true);
}, []);
const setFalse = useCallback(() => {
$boolean.set(false);
}, []);
const set = useCallback((value: boolean) => {
$boolean.set(value);
}, []);
const toggle = useCallback(() => {
$boolean.set(!$boolean.get());
}, []);
const api = useMemo(
() => ({
setTrue,
setFalse,
set,
toggle,
$boolean,
}),
[set, setFalse, setTrue, toggle]
);
return api;
};
};

View File

@@ -1,7 +1,7 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { toast } from 'features/toast/toast';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { useCallback, useEffect, useState } from 'react';
import type { Accept, FileRejection } from 'react-dropzone';
import { useDropzone } from 'react-dropzone';
@@ -14,13 +14,9 @@ const accept: Accept = {
'image/jpeg': ['.jpg', '.jpeg', '.png'],
};
const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (activeTabName) => {
const selectPostUploadAction = createMemoizedSelector(selectActiveTab, (activeTabName) => {
let postUploadAction: PostUploadAction = { type: 'TOAST' };
if (activeTabName === 'canvas') {
postUploadAction = { type: 'SET_CANVAS_INITIAL_IMAGE' };
}
if (activeTabName === 'upscaling') {
postUploadAction = { type: 'SET_UPSCALE_INITIAL_IMAGE' };
}
@@ -30,10 +26,9 @@ const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (ac
export const useFullscreenDropzone = () => {
const { t } = useTranslation();
const postUploadAction = useAppSelector(selectPostUploadAction);
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
const postUploadAction = useAppSelector(selectPostUploadAction);
const [uploadImage] = useUploadImageMutation();
const fileRejectionCallback = useCallback(
@@ -51,7 +46,7 @@ export const useFullscreenDropzone = () => {
);
const fileAcceptedCallback = useCallback(
async (file: File) => {
(file: File) => {
uploadImage({
file,
image_category: 'user',
@@ -101,7 +96,7 @@ export const useFullscreenDropzone = () => {
useEffect(() => {
// This is a hack to allow pasting images into the uploader
const handlePaste = async (e: ClipboardEvent) => {
const handlePaste = (e: ClipboardEvent) => {
if (!dropzone.inputRef.current) {
return;
}

View File

@@ -1,4 +1,5 @@
import { useAppDispatch } from 'app/store/storeHooks';
import { addScope, removeScope, setScopes } from 'common/hooks/interactionScopes';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
import { useQueueBack } from 'features/queue/hooks/useQueueBack';
@@ -16,7 +17,7 @@ export const useGlobalHotkeys = () => {
['ctrl+enter', 'meta+enter'],
queueBack,
{
enabled: () => !isDisabledQueueBack && !isLoadingQueueBack,
enabled: !isDisabledQueueBack && !isLoadingQueueBack,
preventDefault: true,
enableOnFormTags: ['input', 'textarea', 'select'],
},
@@ -29,7 +30,7 @@ export const useGlobalHotkeys = () => {
['ctrl+shift+enter', 'meta+shift+enter'],
queueFront,
{
enabled: () => !isDisabledQueueFront && !isLoadingQueueFront,
enabled: !isDisabledQueueFront && !isLoadingQueueFront,
preventDefault: true,
enableOnFormTags: ['input', 'textarea', 'select'],
},
@@ -46,7 +47,7 @@ export const useGlobalHotkeys = () => {
['shift+x'],
cancelQueueItem,
{
enabled: () => !isDisabledCancelQueueItem && !isLoadingCancelQueueItem,
enabled: !isDisabledCancelQueueItem && !isLoadingCancelQueueItem,
preventDefault: true,
},
[cancelQueueItem, isDisabledCancelQueueItem, isLoadingCancelQueueItem]
@@ -58,7 +59,7 @@ export const useGlobalHotkeys = () => {
['ctrl+shift+x', 'meta+shift+x'],
clearQueue,
{
enabled: () => !isDisabledClearQueue && !isLoadingClearQueue,
enabled: !isDisabledClearQueue && !isLoadingClearQueue,
preventDefault: true,
},
[clearQueue, isDisabledClearQueue, isLoadingClearQueue]
@@ -68,6 +69,8 @@ export const useGlobalHotkeys = () => {
'1',
() => {
dispatch(setActiveTab('generation'));
addScope('canvas');
removeScope('workflows');
},
[dispatch]
);
@@ -75,7 +78,9 @@ export const useGlobalHotkeys = () => {
useHotkeys(
'2',
() => {
dispatch(setActiveTab('canvas'));
dispatch(setActiveTab('upscaling'));
removeScope('canvas');
removeScope('workflows');
},
[dispatch]
);
@@ -84,6 +89,8 @@ export const useGlobalHotkeys = () => {
'3',
() => {
dispatch(setActiveTab('workflows'));
removeScope('canvas');
addScope('workflows');
},
[dispatch]
);
@@ -93,6 +100,7 @@ export const useGlobalHotkeys = () => {
() => {
if (isModelManagerEnabled) {
dispatch(setActiveTab('models'));
setScopes([]);
}
},
[dispatch, isModelManagerEnabled]
@@ -102,6 +110,7 @@ export const useGlobalHotkeys = () => {
isModelManagerEnabled ? '5' : '4',
() => {
dispatch(setActiveTab('queue'));
setScopes([]);
},
[dispatch, isModelManagerEnabled]
);

View File

@@ -32,7 +32,7 @@ export const useGroupedModelCombobox = <T extends AnyModelConfig>(
arg: UseGroupedModelComboboxArg<T>
): UseGroupedModelComboboxReturn => {
const { t } = useTranslation();
const base_model = useAppSelector((s) => s.generation.model?.base ?? 'sdxl');
const base_model = useAppSelector((s) => s.canvasV2.params.model?.base ?? 'sdxl');
const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, groupByType = false } = arg;
const options = useMemo<GroupBase<ComboboxOption>[]>(() => {
if (!modelConfigs) {

View File

@@ -1,67 +1,46 @@
import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import {
selectControlAdapterAll,
selectControlAdaptersSlice,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import type { Layer } from 'features/controlLayers/store/types';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
import type { Templates } from 'features/nodes/store/types';
import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import { selectUpscalelice } from 'features/parameters/store/upscaleSlice';
import { selectConfigSlice } from 'features/system/store/configSlice';
import { selectSystemSlice } from 'features/system/store/systemSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import i18n from 'i18next';
import { forEach, upperFirst } from 'lodash-es';
import { useMemo } from 'react';
import { getConnectedEdges } from 'reactflow';
const LAYER_TYPE_TO_TKEY: Record<Layer['type'], string> = {
initial_image_layer: 'controlLayers.globalInitialImage',
control_adapter_layer: 'controlLayers.globalControlAdapter',
ip_adapter_layer: 'controlLayers.globalIPAdapter',
regional_guidance_layer: 'controlLayers.regionalGuidance',
};
const LAYER_TYPE_TO_TKEY = {
ip_adapter: 'controlLayers.ipAdapter',
inpaint_mask: 'controlLayers.inpaintMask',
regional_guidance: 'controlLayers.regionalGuidance',
raster_layer: 'controlLayers.raster',
control_layer: 'controlLayers.globalControlAdapter',
} as const;
const createSelector = (templates: Templates) =>
const createSelector = (templates: Templates, isConnected: boolean) =>
createMemoizedSelector(
[
selectControlAdaptersSlice,
selectGenerationSlice,
selectSystemSlice,
selectNodesSlice,
selectWorkflowSettingsSlice,
selectDynamicPromptsSlice,
selectControlLayersSlice,
activeTabNameSelector,
selectCanvasV2Slice,
selectUpscalelice,
selectConfigSlice,
selectActiveTab,
],
(
controlAdapters,
generation,
system,
nodes,
workflowSettings,
dynamicPrompts,
controlLayers,
activeTabName,
upscale,
config
) => {
const { model } = generation;
const { size } = controlLayers.present;
const { positivePrompt } = controlLayers.present;
const { isConnected } = system;
(system, nodes, workflowSettings, dynamicPrompts, canvasV2, upscale, config, activeTabName) => {
const { bbox } = canvasV2;
const { model, positivePrompt } = canvasV2.params;
const reasons: { prefix?: string; content: string }[] = [];
@@ -114,6 +93,26 @@ const createSelector = (templates: Templates) =>
});
});
}
} else if (activeTabName === 'upscaling') {
if (!upscale.upscaleInitialImage) {
reasons.push({ content: i18n.t('upscaling.missingUpscaleInitialImage') });
} else if (config.maxUpscaleDimension) {
const { width, height } = upscale.upscaleInitialImage;
const { scale } = upscale;
const maxPixels = config.maxUpscaleDimension ** 2;
const upscaledPixels = width * scale * height * scale;
if (upscaledPixels > maxPixels) {
reasons.push({ content: i18n.t('upscaling.exceedsMaxSize') });
}
}
if (!upscale.upscaleModel) {
reasons.push({ content: i18n.t('upscaling.missingUpscaleModel') });
}
if (!upscale.tileControlnetModel) {
reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') });
}
} else {
if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
reasons.push({ content: i18n.t('parameters.invoke.noPrompts') });
@@ -123,140 +122,115 @@ const createSelector = (templates: Templates) =>
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
}
if (activeTabName === 'generation') {
// Handling for generation tab
controlLayers.present.layers
.filter((l) => l.isEnabled)
.forEach((l, i) => {
const layerLiteral = i18n.t('controlLayers.layers_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[l.type]);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
if (l.type === 'control_adapter_layer') {
// Must have model
if (!l.controlAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected'));
}
// Model base must match
if (l.controlAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel'));
}
// Must have a control image OR, if it has a processor, it must have a processed image
if (!l.controlAdapter.image) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected'));
} else if (l.controlAdapter.processorConfig && !l.controlAdapter.processedImage) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed'));
}
// T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL)
if (l.controlAdapter.type === 't2i_adapter') {
const multiple = model?.base === 'sdxl' ? 32 : 64;
if (size.width % multiple !== 0 || size.height % multiple !== 0) {
problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple }));
}
}
}
if (l.type === 'ip_adapter_layer') {
// Must have model
if (!l.ipAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
}
// Model base must match
if (l.ipAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
}
// Must have an image
if (!l.ipAdapter.image) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
}
}
if (l.type === 'initial_image_layer') {
// Must have an image
if (!l.image) {
problems.push(i18n.t('parameters.invoke.layer.initialImageNoImageSelected'));
}
}
if (l.type === 'regional_guidance_layer') {
// Must have a region
if (l.maskObjects.length === 0) {
problems.push(i18n.t('parameters.invoke.layer.rgNoRegion'));
}
// Must have at least 1 prompt or IP Adapter
if (l.positivePrompt === null && l.negativePrompt === null && l.ipAdapters.length === 0) {
problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters'));
}
l.ipAdapters.forEach((ipAdapter) => {
// Must have model
if (!ipAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
}
// Model base must match
if (ipAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
}
// Must have an image
if (!ipAdapter.image) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
}
});
}
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
} else if (activeTabName === 'upscaling') {
if (!upscale.upscaleInitialImage) {
reasons.push({ content: i18n.t('upscaling.missingUpscaleInitialImage') });
} else if (config.maxUpscaleDimension) {
const { width, height } = upscale.upscaleInitialImage;
const { scale } = upscale;
const maxPixels = config.maxUpscaleDimension ** 2;
const upscaledPixels = width * scale * height * scale;
if (upscaledPixels > maxPixels) {
reasons.push({ content: i18n.t('upscaling.exceedsMaxSize') });
canvasV2.controlLayers.entities
.filter((controlLayer) => controlLayer.isEnabled)
.forEach((controlLayer, i) => {
const layerLiteral = i18n.t('controlLayers.layers_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY['control_layer']);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
// Must have model
if (!controlLayer.controlAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected'));
}
}
if (!upscale.upscaleModel) {
reasons.push({ content: i18n.t('upscaling.missingUpscaleModel') });
}
if (!upscale.tileControlnetModel) {
reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') });
}
} else {
// Handling for all other tabs
selectControlAdapterAll(controlAdapters)
.filter((ca) => ca.isEnabled)
.forEach((ca, i) => {
if (!ca.isEnabled) {
return;
// Model base must match
if (controlLayer.controlAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel'));
}
// T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL)
if (controlLayer.controlAdapter.type === 't2i_adapter') {
const multiple = model?.base === 'sdxl' ? 32 : 64;
if (bbox.rect.width % multiple !== 0 || bbox.rect.height % multiple !== 0) {
problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple }));
}
}
if (!ca.model) {
reasons.push({ content: i18n.t('parameters.invoke.noModelForControlAdapter', { number: i + 1 }) });
} else if (ca.model.base !== model?.base) {
// This should never happen, just a sanity check
reasons.push({
content: i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { number: i + 1 }),
});
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
canvasV2.ipAdapters.entities
.filter((entity) => entity.isEnabled)
.forEach((entity, i) => {
const layerLiteral = i18n.t('controlLayers.layers_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
// Must have model
if (!entity.ipAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
}
// Model base must match
if (entity.ipAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
}
// Must have an image
if (!entity.ipAdapter.image) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
}
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
canvasV2.regions.entities
.filter((entity) => entity.isEnabled)
.forEach((entity, i) => {
const layerLiteral = i18n.t('controlLayers.layers_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
// Must have a region
if (entity.objects.length === 0) {
problems.push(i18n.t('parameters.invoke.layer.rgNoRegion'));
}
// Must have at least 1 prompt or IP Adapter
if (entity.positivePrompt === null && entity.negativePrompt === null && entity.ipAdapters.length === 0) {
problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters'));
}
entity.ipAdapters.forEach((ipAdapter) => {
// Must have model
if (!ipAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
}
if (
!ca.controlImage ||
(isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none')
) {
reasons.push({
content: i18n.t('parameters.invoke.noControlImageForControlAdapter', { number: i + 1 }),
});
// Model base must match
if (ipAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
}
// Must have an image
if (!ipAdapter.image) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
}
});
}
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
canvasV2.rasterLayers.entities
.filter((entity) => entity.isEnabled)
.forEach((entity, i) => {
const layerLiteral = i18n.t('controlLayers.layers_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
}
return { isReady: !reasons.length, reasons };
@@ -265,7 +239,8 @@ const createSelector = (templates: Templates) =>
export const useIsReadyToEnqueue = () => {
const templates = useStore($templates);
const selector = useMemo(() => createSelector(templates), [templates]);
const isConnected = useStore($isConnected);
const selector = useMemo(() => createSelector(templates, isConnected), [templates, isConnected]);
const value = useAppSelector(selector);
return value;
};

View File

@@ -1,35 +0,0 @@
// https://stackoverflow.com/a/73731908
import { useCallback, useEffect, useState } from 'react';
type UseSingleAndDoubleClickOptions = {
onSingleClick: () => void;
onDoubleClick: () => void;
latency?: number;
};
export function useSingleAndDoubleClick({
onSingleClick,
onDoubleClick,
latency = 250,
}: UseSingleAndDoubleClickOptions): () => void {
const [click, setClick] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
if (click === 1) {
onSingleClick();
}
setClick(0);
}, latency);
if (click === 2) {
onDoubleClick();
}
return () => clearTimeout(timer);
}, [click, onDoubleClick, latency, onSingleClick]);
const onClick = useCallback(() => setClick((prev) => prev + 1), []);
return onClick;
}

View File

@@ -1,5 +1,4 @@
type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
export interface JSONObject {
[k: string]: JSONValue;
}
type SerializableValue = string | number | boolean | null | undefined | SerializableValue[] | SerializableObject;
export type SerializableObject = {
[k: string | number]: SerializableValue;
};

View File

@@ -0,0 +1,115 @@
import { describe, expect, it, vi } from 'vitest';
import { SyncableMap } from './SyncableMap';
describe('SyncableMap', () => {
it('should initialize with entries', () => {
const initialEntries = [
['key1', 'value1'],
['key2', 'value2'],
] as const;
const map = new SyncableMap(initialEntries);
expect(map.size).toBe(2);
expect(map.get('key1')).toBe('value1');
expect(map.get('key2')).toBe('value2');
});
it('should notify subscribers when a key is set', () => {
const map = new SyncableMap<string, string>();
const subscriber = vi.fn();
map.subscribe(subscriber);
map.set('key1', 'value1');
expect(subscriber).toHaveBeenCalledTimes(1);
expect(map.get('key1')).toBe('value1');
});
it('should notify subscribers when a key is deleted', () => {
const map = new SyncableMap<string, string>([['key1', 'value1']]);
const subscriber = vi.fn();
map.subscribe(subscriber);
map.delete('key1');
expect(subscriber).toHaveBeenCalledTimes(1);
expect(map.get('key1')).toBeUndefined();
});
it('should notify subscribers when the map is cleared', () => {
const map = new SyncableMap<string, string>([
['key1', 'value1'],
['key2', 'value2'],
]);
const subscriber = vi.fn();
map.subscribe(subscriber);
map.clear();
expect(subscriber).toHaveBeenCalledTimes(1);
expect(map.size).toBe(0);
});
it('should not notify unsubscribed callbacks', () => {
const map = new SyncableMap<string, string>();
const subscriber = vi.fn();
const unsubscribe = map.subscribe(subscriber);
unsubscribe();
map.set('key1', 'value1');
expect(subscriber).not.toHaveBeenCalled();
});
it('should return a snapshot of the current state', () => {
const map = new SyncableMap<string, string>([['key1', 'value1']]);
const snapshot = map.getSnapshot();
expect(snapshot.size).toBe(1);
expect(snapshot.get('key1')).toBe('value1');
});
it('should return the same snapshot if there were no changes', () => {
const map = new SyncableMap<string, string>([['key1', 'value1']]);
const firstSnapshot = map.getSnapshot();
const secondSnapshot = map.getSnapshot();
expect(firstSnapshot).toBe(secondSnapshot);
});
it('should return a new snapshot if changes were made', () => {
const map = new SyncableMap<string, string>([['key1', 'value1']]);
const firstSnapshot = map.getSnapshot();
map.set('key2', 'value2');
const secondSnapshot = map.getSnapshot();
expect(firstSnapshot).not.toBe(secondSnapshot);
expect(secondSnapshot.size).toBe(2);
});
it('should consider different snapshots unequal', () => {
const map = new SyncableMap<string, string>([['key1', 'value1']]);
const firstSnapshot = map.getSnapshot();
map.set('key2', 'value2');
const secondSnapshot = map.getSnapshot();
expect(map['areSnapshotsEqual'](firstSnapshot, secondSnapshot)).toBe(false);
});
it('should consider identical snapshots equal', () => {
const map = new SyncableMap<string, string>([
['key1', 'value1'],
['key2', 'value2'],
]);
const firstSnapshot = map.getSnapshot();
const secondSnapshot = map.getSnapshot();
expect(map['areSnapshotsEqual'](firstSnapshot, secondSnapshot)).toBe(true);
});
});

View File

@@ -0,0 +1,86 @@
/**
* A Map that allows for subscribing to changes and getting a snapshot of the current state.
*
* It can be used with the `useSyncExternalStore` hook to sync the state of the map with a React component.
*
* Reactivity is shallow, so changes to nested objects will not trigger a re-render.
*/
export class SyncableMap<K, V> extends Map<K, V> {
private subscriptions = new Set<() => void>();
private lastSnapshot: Map<K, V> | null = null;
constructor(entries?: readonly (readonly [K, V])[] | null) {
super(entries);
}
set = (key: K, value: V): this => {
super.set(key, value);
this.notifySubscribers();
return this;
};
delete = (key: K): boolean => {
const result = super.delete(key);
this.notifySubscribers();
return result;
};
clear = (): void => {
super.clear();
this.notifySubscribers();
};
/**
* Notify all subscribers that the map has changed.
*/
private notifySubscribers = () => {
for (const callback of this.subscriptions) {
callback();
}
};
/**
* Subscribe to changes to the map.
* @param callback A function to call when the map changes
* @returns A function to unsubscribe from changes
*/
subscribe = (callback: () => void): (() => void) => {
this.subscriptions.add(callback);
return () => {
this.subscriptions.delete(callback);
};
};
/**
* Get a snapshot of the current state of the map.
* @returns A snapshot of the current state of the map
*/
getSnapshot = (): Map<K, V> => {
const currentSnapshot = new Map(this);
if (!this.lastSnapshot || !this.areSnapshotsEqual(this.lastSnapshot, currentSnapshot)) {
this.lastSnapshot = currentSnapshot;
}
return this.lastSnapshot;
};
/**
* Compare two snapshots to determine if they are equal.
* @param snapshotA The first snapshot to compare
* @param snapshotB The second snapshot to compare
* @returns Whether the two snapshots are equal
*/
private areSnapshotsEqual = (snapshotA: Map<K, V>, snapshotB: Map<K, V>): boolean => {
if (snapshotA.size !== snapshotB.size) {
return false;
}
for (const [key, value] of snapshotA) {
if (!Object.is(value, snapshotB.get(key))) {
return false;
}
}
return true;
};
}

View File

@@ -1,28 +0,0 @@
export const getImageDataTransparency = (pixels: Uint8ClampedArray) => {
let isFullyTransparent = true;
let isPartiallyTransparent = false;
const len = pixels.length;
let i = 3;
for (i; i < len; i += 4) {
if (pixels[i] === 255) {
isFullyTransparent = false;
} else {
isPartiallyTransparent = true;
}
if (!isFullyTransparent && isPartiallyTransparent) {
return { isFullyTransparent, isPartiallyTransparent };
}
}
return { isFullyTransparent, isPartiallyTransparent };
};
export const areAnyPixelsBlack = (pixels: Uint8ClampedArray) => {
const len = pixels.length;
let i = 0;
for (i; i < len; ) {
if (pixels[i++] === 0 && pixels[i++] === 0 && pixels[i++] === 0 && pixels[i++] === 255) {
return true;
}
}
return false;
};

View File

@@ -1,85 +1,170 @@
import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { describe, expect, it } from 'vitest';
describe('Array Manipulation Functions', () => {
const originalArray = ['a', 'b', 'c', 'd'];
describe('moveForwardOne', () => {
it('should move an item forward by one position', () => {
const array = [...originalArray];
const result = moveForward(array, (item) => item === 'b');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
it('should do nothing if the item is at the end', () => {
const array = [...originalArray];
const result = moveForward(array, (item) => item === 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
describe('moveOneToEnd', () => {
describe('with callback', () => {
it('should move an item forward by one position', () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'b');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveForward(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
it('should do nothing if the item is at the end', () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
});
});
describe('with item', () => {
it('should move an item forward by one position', () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'b');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
it('should do nothing if the item is at the end', () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
});
});
});
describe('moveToFront', () => {
it('should move an item to the front', () => {
const array = [...originalArray];
const result = moveToFront(array, (item) => item === 'c');
expect(result).toEqual(['c', 'a', 'b', 'd']);
});
describe('moveToStart', () => {
describe('with callback', () => {
it('should move an item to the front', () => {
const array = [...originalArray];
const result = moveToStart(array, (item) => item === 'c');
expect(result).toEqual(['c', 'a', 'b', 'd']);
});
it('should do nothing if the item is already at the front', () => {
const array = [...originalArray];
const result = moveToFront(array, (item) => item === 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it('should do nothing if the item is already at the front', () => {
const array = [...originalArray];
const result = moveToStart(array, (item) => item === 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToFront(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToStart(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
});
});
describe('with item', () => {
it('should move an item to the front', () => {
const array = [...originalArray];
const result = moveToStart(array, 'c');
expect(result).toEqual(['c', 'a', 'b', 'd']);
});
it('should do nothing if the item is already at the front', () => {
const array = [...originalArray];
const result = moveToStart(array, 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToStart(array, 'z');
expect(result).toEqual(originalArray);
});
});
});
describe('moveBackwardsOne', () => {
it('should move an item backward by one position', () => {
const array = [...originalArray];
const result = moveBackward(array, (item) => item === 'c');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
describe('moveOneToStart', () => {
describe('with callback', () => {
it('should move an item backward by one position', () => {
const array = [...originalArray];
const result = moveOneToStart(array, (item) => item === 'c');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
it('should do nothing if the item is at the beginning', () => {
const array = [...originalArray];
const result = moveBackward(array, (item) => item === 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it('should do nothing if the item is at the beginning', () => {
const array = [...originalArray];
const result = moveOneToStart(array, (item) => item === 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveBackward(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveOneToStart(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
});
});
describe('with item', () => {
it('should move an item backward by one position', () => {
const array = [...originalArray];
const result = moveOneToStart(array, 'c');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
it('should do nothing if the item is at the beginning', () => {
const array = [...originalArray];
const result = moveOneToStart(array, 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveOneToStart(array, 'z');
expect(result).toEqual(originalArray);
});
});
});
describe('moveToBack', () => {
it('should move an item to the back', () => {
const array = [...originalArray];
const result = moveToBack(array, (item) => item === 'b');
expect(result).toEqual(['a', 'c', 'd', 'b']);
});
describe('moveToEnd', () => {
describe('with callback', () => {
it('should move an item to the back', () => {
const array = [...originalArray];
const result = moveToEnd(array, (item) => item === 'b');
expect(result).toEqual(['a', 'c', 'd', 'b']);
});
it('should do nothing if the item is already at the back', () => {
const array = [...originalArray];
const result = moveToBack(array, (item) => item === 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it('should do nothing if the item is already at the back', () => {
const array = [...originalArray];
const result = moveToEnd(array, (item) => item === 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToBack(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToEnd(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
});
});
describe('with item', () => {
it('should move an item to the back', () => {
const array = [...originalArray];
const result = moveToEnd(array, 'b');
expect(result).toEqual(['a', 'c', 'd', 'b']);
});
it('should do nothing if the item is already at the back', () => {
const array = [...originalArray];
const result = moveToEnd(array, 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToEnd(array, 'z');
expect(result).toEqual(originalArray);
});
});
});
});

View File

@@ -1,37 +1,45 @@
export const moveForward = <T>(array: T[], callback: (item: T) => boolean): T[] => {
const index = array.findIndex(callback);
if (index >= 0 && index < array.length - 1) {
//@ts-expect-error - These indicies are safe per the previous check
[array[index], array[index + 1]] = [array[index + 1], array[index]];
}
return array;
};
export const moveToFront = <T>(array: T[], callback: (item: T) => boolean): T[] => {
const index = array.findIndex(callback);
export function moveToStart<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
export function moveToStart<T>(array: T[], item: T): T[];
export function moveToStart<T>(array: T[], arg1: T | ((item: T) => boolean)): T[] {
const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1);
if (index > 0) {
const [item] = array.splice(index, 1);
//@ts-expect-error - These indicies are safe per the previous check
array.unshift(item);
}
return array;
};
}
export const moveBackward = <T>(array: T[], callback: (item: T) => boolean): T[] => {
const index = array.findIndex(callback);
export function moveOneToStart<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
export function moveOneToStart<T>(array: T[], item: T): T[];
export function moveOneToStart<T>(array: T[], arg1: T | ((item: T) => boolean)): T[] {
const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1);
if (index > 0) {
//@ts-expect-error - These indicies are safe per the previous check
[array[index], array[index - 1]] = [array[index - 1], array[index]];
}
return array;
};
}
export const moveToBack = <T>(array: T[], callback: (item: T) => boolean): T[] => {
const index = array.findIndex(callback);
export function moveToEnd<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
export function moveToEnd<T>(array: T[], item: T): T[];
export function moveToEnd<T>(array: T[], arg1: T | ((item: T) => boolean)): T[] {
const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1);
if (index >= 0 && index < array.length - 1) {
const [item] = array.splice(index, 1);
//@ts-expect-error - These indicies are safe per the previous check
array.push(item);
}
return array;
};
}
export function moveOneToEnd<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
export function moveOneToEnd<T>(array: T[], item: T): T[];
export function moveOneToEnd<T>(array: T[], arg1: T | ((item: T) => boolean)): T[] {
const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1);
if (index >= 0 && index < array.length - 1) {
//@ts-expect-error - These indicies are safe per the previous check
[array[index], array[index + 1]] = [array[index + 1], array[index]];
}
return array;
}

View File

@@ -1,4 +1,4 @@
import type { RgbaColor } from 'react-colorful';
import type { RgbaColor, RgbColor } from 'react-colorful';
export function rgbaToHex(color: RgbaColor, alpha: boolean = false): string {
const hex = ((1 << 24) + (color.r << 16) + (color.g << 8) + color.b).toString(16).slice(1);
@@ -15,3 +15,13 @@ export function hexToRGBA(hex: string, alpha: number) {
const b = parseInt(hex.substring(4, 6), 16);
return { r, g, b, a: alpha };
}
export const rgbaColorToString = (color: RgbaColor): string => {
const { r, g, b, a } = color;
return `rgba(${r}, ${g}, ${b}, ${a})`;
};
export const rgbColorToString = (color: RgbColor): string => {
const { r, g, b } = color;
return `rgba(${r}, ${g}, ${b})`;
};

View File

@@ -7,7 +7,7 @@ import { $authToken } from 'app/store/nanostores/authToken';
* @returns A function that takes a URL and returns a Promise that resolves with a Blob
*/
export const convertImageUrlToBlob = async (url: string) =>
export const convertImageUrlToBlob = (url: string) =>
new Promise<Blob | null>((resolve, reject) => {
const img = new Image();
img.onload = () => {

View File

@@ -1,7 +0,0 @@
export const isInputElement = (el: HTMLElement) => {
return (
el.tagName.toLowerCase() === 'input' ||
el.tagName.toLowerCase() === 'textarea' ||
el.tagName.toLowerCase() === 'select'
);
};

View File

@@ -1,23 +0,0 @@
type Base64AndCaption = {
base64: string;
caption: string;
};
const openBase64ImageInTab = (images: Base64AndCaption[]) => {
const w = window.open('');
if (!w) {
return;
}
images.forEach((i) => {
const image = new Image();
image.src = i.base64;
w.document.write(i.caption);
w.document.write('</br>');
w.document.write(image.outerHTML);
w.document.write('</br></br>');
});
};
export default openBase64ImageInTab;

View File

@@ -1,10 +1,11 @@
export const roundDownToMultiple = (num: number, multiple: number): number => {
return Math.floor(num / multiple) * multiple;
};
export const roundDownToMultipleMin = (num: number, multiple: number): number => {
return Math.max(multiple, Math.floor(num / multiple) * multiple);
};
export const roundToMultiple = (num: number, multiple: number): number => {
return Math.round(num / multiple) * multiple;
};
export const roundToMultipleMin = (num: number, multiple: number): number => {
return Math.max(Math.round(num / multiple) * multiple, multiple);
};

View File

@@ -1,35 +0,0 @@
import { Button, ConfirmationAlertDialog, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { clearCanvasHistory } from 'features/canvas/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleFill } from 'react-icons/pi';
const ClearCanvasHistoryButtonModal = () => {
const isStaging = useAppSelector(isStagingSelector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const acceptCallback = useCallback(() => dispatch(clearCanvasHistory()), [dispatch]);
return (
<>
<Button onClick={onOpen} size="sm" leftIcon={<PiTrashSimpleFill />} isDisabled={isStaging}>
{t('unifiedCanvas.clearCanvasHistory')}
</Button>
<ConfirmationAlertDialog
isOpen={isOpen}
onClose={onClose}
title={t('unifiedCanvas.clearCanvasHistory')}
acceptCallback={acceptCallback}
acceptButtonText={t('unifiedCanvas.clearHistory')}
>
<p>{t('unifiedCanvas.clearCanvasHistoryMessage')}</p>
<br />
<p>{t('unifiedCanvas.clearCanvasHistoryConfirm')}</p>
</ConfirmationAlertDialog>
</>
);
};
export default memo(ClearCanvasHistoryButtonModal);

View File

@@ -1,198 +0,0 @@
import { Box, chakra, Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import useCanvasDragMove from 'features/canvas/hooks/useCanvasDragMove';
import useCanvasHotkeys from 'features/canvas/hooks/useCanvasHotkeys';
import useCanvasMouseDown from 'features/canvas/hooks/useCanvasMouseDown';
import useCanvasMouseMove from 'features/canvas/hooks/useCanvasMouseMove';
import useCanvasMouseOut from 'features/canvas/hooks/useCanvasMouseOut';
import useCanvasMouseUp from 'features/canvas/hooks/useCanvasMouseUp';
import useCanvasWheel from 'features/canvas/hooks/useCanvasZoom';
import {
$canvasBaseLayer,
$canvasStage,
$isModifyingBoundingBox,
$isMouseOverBoundingBox,
$isMovingStage,
$isTransformingBoundingBox,
$tool,
} from 'features/canvas/store/canvasNanostore';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { canvasResized, selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import type Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node';
import type { Vector2d } from 'konva/lib/types';
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { Layer, Stage } from 'react-konva';
import IAICanvasBoundingBoxOverlay from './IAICanvasBoundingBoxOverlay';
import IAICanvasGrid from './IAICanvasGrid';
import IAICanvasIntermediateImage from './IAICanvasIntermediateImage';
import IAICanvasMaskCompositor from './IAICanvasMaskCompositor';
import IAICanvasMaskLines from './IAICanvasMaskLines';
import IAICanvasObjectRenderer from './IAICanvasObjectRenderer';
import IAICanvasStagingArea from './IAICanvasStagingArea';
import IAICanvasStagingAreaToolbar from './IAICanvasStagingAreaToolbar';
import IAICanvasStatusText from './IAICanvasStatusText';
import IAICanvasBoundingBox from './IAICanvasToolbar/IAICanvasBoundingBox';
import IAICanvasToolPreview from './IAICanvasToolPreview';
const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => {
return {
stageCoordinates: canvas.stageCoordinates,
stageDimensions: canvas.stageDimensions,
};
});
const ChakraStage = chakra(Stage, {
shouldForwardProp: (prop) => !['sx'].includes(prop),
});
const IAICanvas = () => {
const isStaging = useAppSelector(isStagingSelector);
const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled);
const shouldShowBoundingBox = useAppSelector((s) => s.canvas.shouldShowBoundingBox);
const shouldShowGrid = useAppSelector((s) => s.canvas.shouldShowGrid);
const stageScale = useAppSelector((s) => s.canvas.stageScale);
const shouldShowIntermediates = useAppSelector((s) => s.canvas.shouldShowIntermediates);
const shouldAntialias = useAppSelector((s) => s.canvas.shouldAntialias);
const shouldRestrictStrokesToBox = useAppSelector((s) => s.canvas.shouldRestrictStrokesToBox);
const { stageCoordinates, stageDimensions } = useAppSelector(selector);
const dispatch = useAppDispatch();
const containerRef = useRef<HTMLDivElement>(null);
const stageRef = useRef<Konva.Stage | null>(null);
const canvasBaseLayerRef = useRef<Konva.Layer | null>(null);
const isModifyingBoundingBox = useStore($isModifyingBoundingBox);
const isMovingStage = useStore($isMovingStage);
const isTransformingBoundingBox = useStore($isTransformingBoundingBox);
const isMouseOverBoundingBox = useStore($isMouseOverBoundingBox);
const tool = useStore($tool);
useCanvasHotkeys();
const canvasStageRefCallback = useCallback((stageElement: Konva.Stage) => {
$canvasStage.set(stageElement);
stageRef.current = stageElement;
}, []);
const stageCursor = useMemo(() => {
if (tool === 'move' || isStaging) {
if (isMovingStage) {
return 'grabbing';
} else {
return 'grab';
}
} else if (isTransformingBoundingBox) {
return undefined;
} else if (shouldRestrictStrokesToBox && !isMouseOverBoundingBox) {
return 'default';
}
return 'none';
}, [isMouseOverBoundingBox, isMovingStage, isStaging, isTransformingBoundingBox, shouldRestrictStrokesToBox, tool]);
const canvasBaseLayerRefCallback = useCallback((layerElement: Konva.Layer) => {
$canvasBaseLayer.set(layerElement);
canvasBaseLayerRef.current = layerElement;
}, []);
const lastCursorPositionRef = useRef<Vector2d>({ x: 0, y: 0 });
// Use refs for values that do not affect rendering, other values in redux
const didMouseMoveRef = useRef<boolean>(false);
const handleWheel = useCanvasWheel(stageRef);
const handleMouseDown = useCanvasMouseDown(stageRef);
const handleMouseUp = useCanvasMouseUp(stageRef, didMouseMoveRef);
const handleMouseMove = useCanvasMouseMove(stageRef, didMouseMoveRef, lastCursorPositionRef);
const { handleDragStart, handleDragMove, handleDragEnd } = useCanvasDragMove();
const handleMouseOut = useCanvasMouseOut();
const handleContextMenu = useCallback((e: KonvaEventObject<MouseEvent>) => e.evt.preventDefault(), []);
useEffect(() => {
if (!containerRef.current) {
return;
}
const resizeObserver = new ResizeObserver(() => {
if (!containerRef.current) {
return;
}
const { width, height } = containerRef.current.getBoundingClientRect();
dispatch(canvasResized({ width, height }));
});
resizeObserver.observe(containerRef.current);
const { width, height } = containerRef.current.getBoundingClientRect();
dispatch(canvasResized({ width, height }));
return () => {
resizeObserver.disconnect();
};
}, [dispatch]);
const stageStyles = useMemo(
() => ({
outline: 'none',
overflow: 'hidden',
cursor: stageCursor ? stageCursor : undefined,
canvas: {
outline: 'none',
},
}),
[stageCursor]
);
const scale = useMemo(() => ({ x: stageScale, y: stageScale }), [stageScale]);
return (
<Flex id="canvas-container" ref={containerRef} position="relative" height="100%" width="100%" borderRadius="base">
<Box position="absolute">
<ChakraStage
tabIndex={-1}
ref={canvasStageRefCallback}
sx={stageStyles}
x={stageCoordinates.x}
y={stageCoordinates.y}
width={stageDimensions.width}
height={stageDimensions.height}
scale={scale}
onTouchStart={handleMouseDown}
onTouchMove={handleMouseMove}
onTouchEnd={handleMouseUp}
onMouseDown={handleMouseDown}
onMouseLeave={handleMouseOut}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onDragStart={handleDragStart}
onDragMove={handleDragMove}
onDragEnd={handleDragEnd}
onContextMenu={handleContextMenu}
onWheel={handleWheel}
draggable={(tool === 'move' || isStaging) && !isModifyingBoundingBox}
>
<Layer id="grid" visible={shouldShowGrid} listening={false}>
<IAICanvasGrid />
</Layer>
<Layer id="base" ref={canvasBaseLayerRefCallback} listening={false} imageSmoothingEnabled={shouldAntialias}>
<IAICanvasObjectRenderer />
</Layer>
<Layer id="mask" visible={isMaskEnabled && !isStaging} listening={false}>
<IAICanvasMaskLines visible={true} listening={false} />
<IAICanvasMaskCompositor listening={false} />
</Layer>
<Layer listening={false}>
<IAICanvasBoundingBoxOverlay />
</Layer>
<Layer id="preview" imageSmoothingEnabled={shouldAntialias}>
{!isStaging && <IAICanvasToolPreview visible={tool !== 'move'} listening={false} />}
<IAICanvasStagingArea listening={false} visible={isStaging} />
{shouldShowIntermediates && <IAICanvasIntermediateImage />}
<IAICanvasBoundingBox visible={shouldShowBoundingBox && !isStaging} />
</Layer>
</ChakraStage>
</Box>
<IAICanvasStatusText />
<IAICanvasStagingAreaToolbar />
</Flex>
);
};
export default memo(IAICanvas);

View File

@@ -1,48 +0,0 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { memo } from 'react';
import { Group, Rect } from 'react-konva';
const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => {
const { boundingBoxCoordinates, boundingBoxDimensions, stageDimensions, stageCoordinates } = canvas;
return {
boundingBoxCoordinates,
boundingBoxDimensions,
stageCoordinates,
stageDimensions,
};
});
const IAICanvasBoundingBoxOverlay = () => {
const { boundingBoxCoordinates, boundingBoxDimensions, stageCoordinates, stageDimensions } = useAppSelector(selector);
const shouldDarkenOutsideBoundingBox = useAppSelector((s) => s.canvas.shouldDarkenOutsideBoundingBox);
const stageScale = useAppSelector((s) => s.canvas.stageScale);
return (
<Group listening={false}>
<Rect
offsetX={stageCoordinates.x / stageScale}
offsetY={stageCoordinates.y / stageScale}
height={stageDimensions.height / stageScale}
width={stageDimensions.width / stageScale}
fill="rgba(0,0,0,0.4)"
listening={false}
visible={shouldDarkenOutsideBoundingBox}
/>
<Rect
x={boundingBoxCoordinates.x}
y={boundingBoxCoordinates.y}
width={boundingBoxDimensions.width}
height={boundingBoxDimensions.height}
fill="rgb(255,255,255)"
listening={false}
visible={shouldDarkenOutsideBoundingBox}
globalCompositeOperation="destination-out"
/>
</Group>
);
};
export default memo(IAICanvasBoundingBoxOverlay);

View File

@@ -1,126 +0,0 @@
// Grid drawing adapted from https://longviewcoder.com/2021/12/08/konva-a-better-grid/
import { getArbitraryBaseColor } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import type { ReactElement } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { Group, Line as KonvaLine } from 'react-konva';
const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => {
return {
stageCoordinates: canvas.stageCoordinates,
stageDimensions: canvas.stageDimensions,
};
});
const baseGridLineColor = getArbitraryBaseColor(27);
const fineGridLineColor = getArbitraryBaseColor(18);
const IAICanvasGrid = () => {
const { stageCoordinates, stageDimensions } = useAppSelector(selector);
const stageScale = useAppSelector((s) => s.canvas.stageScale);
const gridSpacing = useMemo(() => {
if (stageScale >= 2) {
return 8;
}
if (stageScale >= 1 && stageScale < 2) {
return 16;
}
if (stageScale >= 0.5 && stageScale < 1) {
return 32;
}
return 64;
}, [stageScale]);
const unscale = useCallback(
(value: number) => {
return value / stageScale;
},
[stageScale]
);
const gridLines = useMemo(() => {
const { width, height } = stageDimensions;
const { x, y } = stageCoordinates;
const stageRect = {
x1: 0,
y1: 0,
x2: width,
y2: height,
offset: {
x: unscale(x),
y: unscale(y),
},
};
const gridOffset = {
x: Math.ceil(unscale(x) / gridSpacing) * gridSpacing,
y: Math.ceil(unscale(y) / gridSpacing) * gridSpacing,
};
const gridRect = {
x1: -gridOffset.x,
y1: -gridOffset.y,
x2: unscale(width) - gridOffset.x + gridSpacing,
y2: unscale(height) - gridOffset.y + gridSpacing,
};
const gridFullRect = {
x1: Math.min(stageRect.x1, gridRect.x1),
y1: Math.min(stageRect.y1, gridRect.y1),
x2: Math.max(stageRect.x2, gridRect.x2),
y2: Math.max(stageRect.y2, gridRect.y2),
};
const // find the x & y size of the grid
xSize = gridFullRect.x2 - gridFullRect.x1;
const ySize = gridFullRect.y2 - gridFullRect.y1;
// compute the number of steps required on each axis.
const xSteps = Math.round(xSize / gridSpacing) + 1;
const ySteps = Math.round(ySize / gridSpacing) + 1;
const strokeWidth = unscale(1);
const gridLines: ReactElement[] = new Array(xSteps + ySteps);
let _x = 0;
let _y = 0;
for (let i = 0; i < xSteps; i++) {
_x = gridFullRect.x1 + i * gridSpacing;
gridLines.push(
<KonvaLine
key={`x_${i}`}
x={_x}
y={gridFullRect.y1}
points={[0, 0, 0, ySize]}
stroke={_x % 64 ? fineGridLineColor : baseGridLineColor}
strokeWidth={strokeWidth}
listening={false}
/>
);
}
for (let i = 0; i < ySteps; i++) {
_y = gridFullRect.y1 + i * gridSpacing;
gridLines.push(
<KonvaLine
key={`y_${i}`}
x={gridFullRect.x1}
y={_y}
points={[0, 0, xSize, 0]}
stroke={_y % 64 ? fineGridLineColor : baseGridLineColor}
strokeWidth={strokeWidth}
listening={false}
/>
);
}
return gridLines;
}, [stageDimensions, stageCoordinates, unscale, gridSpacing]);
return <Group listening={false}>{gridLines}</Group>;
};
export default memo(IAICanvasGrid);

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