Compare commits

...

168 Commits

Author SHA1 Message Date
psychedelicious
8cf94d602f chore: bump version to v5.3.1 2024-11-01 13:31:51 +11:00
psychedelicious
016a6f182f Make T2I Adapters work with any resolution supported by the models (#7215)
## Summary

This change mimics the unet padding strategy to align T2I featuremaps
with the latents during denoising. It also slightly adjusts the crop and
scale logic so that the control will match the input image without
shifting when it needs to pad.

## Related Issues / Discussions

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

## QA Instructions

Image generated at 1032x1024

![image](https://github.com/user-attachments/assets/7ea579e4-61dc-4b6b-aa84-33d676d160c6)

Image generated at 1080x1040 to prove feature alignment.

![image](https://github.com/user-attachments/assets/ee6e5b6a-d0d5-474d-9fc4-f65c104964bd)

Edge artifacts on the bottom and right are a result of SDXL's unet
padding, and t2i influence will be cut off in those regions.

## Merge Plan

Contingent on #7205 
Currently the Canvas UI prevents users from generating non-64
resolutions while t2i adapter layers are active. Will leave this as a
draft until fixing that.

## Checklist

- [x] _The PR has a short but descriptive title, suitable for a
changelog_
- [ ] _Tests added / updated (if applicable)_
- [ ] _Documentation added / updated (if applicable)_
2024-11-01 13:22:00 +11:00
Kent Keirsey
6fbc019142 Merge branch 'main' into t2i_resolution_hack 2024-10-31 22:08:38 -04:00
psychedelicious
26f95d6a97 fix(ui): disable move tool when staging 2024-10-31 22:08:16 -04:00
psychedelicious
40f7b0d171 fix(ui): cursor disappearing on empty layers 2024-10-31 22:08:16 -04:00
psychedelicious
4904700751 feat(ui): more info in state module repr 2024-10-31 22:08:16 -04:00
psychedelicious
83538c4b2b fix(ui): flash of canvas state between last progress image and generation result 2024-10-31 22:08:16 -04:00
psychedelicious
eb7b559529 fix(ui): sync canvas layer visibility when staging state changes 2024-10-31 22:08:16 -04:00
Kent Keirsey
4945465cf0 Merge branch 'main' into t2i_resolution_hack 2024-10-31 21:17:06 -04:00
Will
7eed7282a9 removing periods from update link to prevent page not found error 2024-11-01 07:42:31 +11:00
psychedelicious
47f0781822 fix(ui): add missing translations
Closes #7229
2024-11-01 07:40:52 +11:00
Eugene Brodsky
88b8e3e3d5 chore(deps): adjust pins for torch, numpy, other dependencies, to satisfy stricted dependency resolution 2024-10-31 16:26:53 -04:00
dunkeroni
47c3ab9214 Remove UI restrictions for T2I resolutions 2024-10-31 16:07:46 -04:00
dunkeroni
d6d436b59c Merge branch 'invoke-ai:main' into t2i_resolution_hack 2024-10-31 15:52:24 -04:00
Hippalectryon
6ff7057967 fix broken link in installer 2024-10-31 09:50:08 -04:00
psychedelicious
e032ab1179 fix(ui): ensure compositing rect is rendered correctly
This fixes an issue uncovered by the previous commit in which we do not exit filter/select-object on save-as.
2024-10-31 08:57:10 -04:00
psychedelicious
65bddfcd93 feat(ui): filter/select-object do not exit on save-as 2024-10-31 08:57:10 -04:00
aidawanglion
2d3ce418dd translationBot(ui): update translation (Chinese (Simplified Han script))
Currently translated at 73.7% (1160 of 1573 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/zh_Hans/
2024-10-31 17:18:35 +11:00
Hosted Weblate
548d72f7b9 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
2024-10-31 17:18:35 +11:00
aidawanglion
19837a0f29 translationBot(ui): update translation (Chinese (Simplified Han script))
Currently translated at 73.3% (1146 of 1563 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/zh_Hans/
2024-10-31 17:18:35 +11:00
aidawanglion
483b65a1dc translationBot(ui): update translation (Chinese (Simplified Han script))
Currently translated at 69.4% (1086 of 1563 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/zh_Hans/
2024-10-31 17:18:35 +11:00
Riccardo Giovanetti
b85931c7ab translationBot(ui): update translation (Italian)
Currently translated at 99.4% (1554 of 1563 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
2024-10-31 17:18:35 +11:00
Hosted Weblate
9225f47338 translationBot(ui): update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
2024-10-31 17:18:35 +11:00
Riccardo Giovanetti
bccac5e4a6 translationBot(ui): update translation (Italian)
Currently translated at 99.4% (1553 of 1562 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
2024-10-31 17:18:35 +11:00
Hosted Weblate
7cb07fdc04 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
2024-10-31 17:18:35 +11:00
dakota2472
b137450026 translationBot(ui): update translation (Italian)
Currently translated at 100.0% (1562 of 1562 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
2024-10-31 17:18:35 +11:00
Hosted Weblate
dc5090469a translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
2024-10-31 17:18:35 +11:00
Thomas Bolteau
e0ae2ace89 translationBot(ui): update translation (French)
Currently translated at 100.0% (1561 of 1561 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/fr/
2024-10-31 17:18:35 +11:00
Riku
269faae04b translationBot(ui): update translation (German)
Currently translated at 71.4% (1115 of 1561 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/
2024-10-31 17:18:35 +11:00
Riccardo Giovanetti
e282acd41c translationBot(ui): update translation (Italian)
Currently translated at 98.8% (1543 of 1561 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
2024-10-31 17:18:35 +11:00
Ettore Atalan
a266668348 translationBot(ui): update translation (German)
Currently translated at 69.3% (1083 of 1561 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/
2024-10-31 17:18:35 +11:00
Riccardo Giovanetti
3bb3e142fc translationBot(ui): update translation (Italian)
Currently translated at 98.8% (1543 of 1561 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
2024-10-31 17:18:35 +11:00
Hosted Weblate
6ac6d70a22 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2024-10-31 17:18:35 +11:00
Riccardo Giovanetti
b0acf33ba5 translationBot(ui): update translation (Italian)
Currently translated at 98.5% (1496 of 1518 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2024-10-31 17:18:35 +11:00
qyouqme
b3eb64b64c translationBot(ui): update translation (Chinese (Simplified Han script))
Currently translated at 66.0% (1003 of 1518 strings)

Co-authored-by: qyouqme <camtasiacn@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/zh_Hans/
Translation: InvokeAI/Web UI
2024-10-31 17:18:35 +11:00
Riku
95f8ab1a29 translationBot(ui): update translation (German)
Currently translated at 71.3% (1083 of 1518 strings)

Co-authored-by: Riku <riku.block@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/
Translation: InvokeAI/Web UI
2024-10-31 17:18:35 +11:00
Hosted Weblate
4e043384db translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2024-10-31 17:18:35 +11:00
psychedelicious
0f5df8ba17 chore(ui): lint 2024-10-31 16:54:31 +11:00
psychedelicious
2826ab48a2 refactor(ui): layer interaction locking
Previously we maintained an `isInteractable` flag, which was derived from these layer flags:
- Locked/unlocked
- Enabled/disabled
- Layer's type visible/hidden

When a layer was not interactable, we blocked all layer actions.

After comparing to the behaviour in Affinity and considering user feedback, I've loosened these restrictions while maintaining safety. First, some definitions.

There two kinds of layer actions - mutating actions and non-mutating actions.
- Mutating actions are drawing on the layer, cropping it, filtering it, converting it, etc. Anything that changes the layer.
- Non-mutating actions are copying the layer, saving the layer to gallery, etc. Anything that _uses_ the layer.

Then, there are two broad canvas states - busy and not busy. "Busy" means the canvas is actively filtering, staging, compositing layers together, etc - something that is "single-threaded" by nature.

And here are the revised restrictions:
- When canvas is busy, you cannot initiate any layer actions.
- When the canvas is not busy, and the layer is locked, you initiate any mutating actions.
- When the canvas is not busy and the layer is not locked, you can initiate any layer action.

Besides safely giving users more freedom, it also fixes an issue where the context menu for a layer was disabled if it was not the selected layer.
2024-10-31 16:54:31 +11:00
psychedelicious
7ff1b635c8 docs: clarify comments for invoke method return annotation validation 2024-10-31 16:21:07 +11:00
psychedelicious
dfb5e8b5e5 tests: add invoke method & output annotation tests 2024-10-31 16:21:07 +11:00
psychedelicious
7259da799c feat(nodes): attempt to look up invoke return types by name 2024-10-31 16:21:07 +11:00
psychedelicious
965069fce1 tests: fix nodes tests
they now require a valid output
2024-10-31 16:21:07 +11:00
psychedelicious
90232806d9 feat(nodes): add validation for invoke method return types 2024-10-31 16:21:07 +11:00
Hippalectryon
81bc153399 Fix link in dev docs 2024-10-31 16:06:44 +11:00
Jonathan
c63e526f99 Update FAQ.md
Fixed typo
2024-10-31 16:04:23 +11:00
nirmal0001
2b74263007 Update patchmatch.md
add required Install dependencies for arch linux
2024-10-31 16:01:57 +11:00
psychedelicious
d3a82f7119 feat(ui): do not show hftoken error until user attempts to set it 2024-10-31 15:47:14 +11:00
Mary Hipp
291c5a0341 lint 2024-10-31 15:47:14 +11:00
Mary Hipp
bcb41399ca feat(ui,api): support for HF tokens in UI, handle Unauthorized and Forbidden errors 2024-10-31 15:47:14 +11:00
psychedelicious
6f0f53849b tests: reset config changes in test_deny_nodes when finished testing 2024-10-31 15:22:14 +11:00
psychedelicious
4e7d63761a fix(nodes): nodes denylist handling
- Add method to force a rebuild of the pydantic type adapter for the union of invocations, which is used to validate graphs.
- Update the xfail'd test.
2024-10-31 15:22:14 +11:00
psychedelicious
198c84105d fix(ui): compositor not setting processing flag when cleaning up 2024-10-30 16:27:36 +11:00
psychedelicious
2453b9f443 chore: bump version to v5.3.0rc1 2024-10-30 13:11:41 +11:00
psychedelicious
b091aca986 chore(ui): lint 2024-10-30 11:05:46 +11:00
psychedelicious
8f02ce54a0 perf(ui): cache image data & transparency mode during generation mode calculation
Perf boost and reduces the number of images we create on the backend.
2024-10-30 11:05:46 +11:00
psychedelicious
f4b7c63002 feat(ui): omit non-render-impacting keys when hashing entities
Had missed several of these, which means we were invalidating caches far too often. For example, when you changed a RG prompt, we were invalidating the cached canvas for that entity, even though changing the prompt doesn't affect the canvas at all.
2024-10-30 11:05:46 +11:00
psychedelicious
a4629280b5 feat(ui): use typeguard instead of string comparison 2024-10-30 11:05:46 +11:00
psychedelicious
855fb007da tidy(ui): minor type fix 2024-10-30 11:05:46 +11:00
psychedelicious
d805b52c1f feat(ui): merge down deletes merged entities 2024-10-30 11:05:46 +11:00
psychedelicious
2ea55685bb feat(ui): add save to assets for inpaint & rg 2024-10-30 11:05:46 +11:00
psychedelicious
bd6ff3deaa feat(ui): add merge down for all entity types 2024-10-30 11:05:46 +11:00
psychedelicious
82dd53ec88 tidy(ui): clean up merge visible logic 2024-10-30 11:05:46 +11:00
psychedelicious
71d749541d feat(ui): control layers supports merge visible
The "lighter" GlobalCompositeOperation is used. This seems to be the best one when merging control layers, as it retains edge maps.
2024-10-30 11:05:46 +11:00
psychedelicious
48a57fc4b9 feat(ui): support globalCompositeOperation when compositing canvas 2024-10-30 11:05:46 +11:00
psychedelicious
530e0910fc feat(ui): regional guidance supports merge visible 2024-10-30 11:05:46 +11:00
psychedelicious
2fdf8fc0a2 feat(ui): merge visible creates new layer
Previously, merge visible deleted all other visible layers. This is not how affinity works, I should have confirmed before making it work like this in the first place.Ï
2024-10-30 11:05:46 +11:00
psychedelicious
91db9c9300 refactor(ui): generalize compositor methods
`CanvasCompositorModule` had a fairly inflexible API, only supporting compositing all raster layers or inpaint masks.

The API has been generalized work with a list of canvas entities. This enables `Merge Down` and `Merge Selected` functionality (though `Merge Selected` is not part of this set of changes).
2024-10-30 11:05:46 +11:00
psychedelicious
bc42205593 fix(ui): remember to disable isFiltering when finishing filtering 2024-10-30 09:19:30 +11:00
psychedelicious
2e3cba6416 fix(ui): flash of original layer when applying filter/segment
Let the parent module adopt the filtered/segemented image instead of destroying it and making the parent re-create it, which results in a brief flash of the parent layer's original objects before the new image is rendered.
2024-10-30 09:19:30 +11:00
psychedelicious
7852aacd11 fix(uI): track whether graph succeeded in runGraphAndReturnImageOutput
This prevents extraneous graph cancel requests when cleaning up the abort signal after a successful run of a graph.
2024-10-30 09:19:30 +11:00
psychedelicious
6cccd67ecd feat(ui): update SAM module to w/ minor improvements from filter module 2024-10-30 09:19:30 +11:00
psychedelicious
a7a89c9de1 feat(ui): use more resilient logic in canvas filter module, same as in SAM module 2024-10-30 09:19:30 +11:00
psychedelicious
5ca8eed89e tidy(ui): remove all buffer renderer interactions in SAM module
We don't use the buffer rendere in this module; there's no reason to clear it.
2024-10-30 09:19:30 +11:00
psychedelicious
c885c3c9a6 fix(ui): filter layer data pushed to parent rendered when saving as 2024-10-30 09:19:30 +11:00
Mary Hipp
d81c38c350 update announcements 2024-10-29 09:53:13 -04:00
Riku
92d5b73215 fix(ui): seamless zod parameter cleanup 2024-10-29 20:43:44 +11:00
Riku
097e92db6a fix(ui): always write seamless metadata
Ensure images without seamless enabled correctly reset the setting
when all parameters are recalled
2024-10-29 20:43:44 +11:00
Riku
84c6209a45 feat(ui): display seamless values in metadata viewer 2024-10-29 20:43:44 +11:00
Riku
107e48808a fix(ui): recall seamless settings 2024-10-29 20:43:44 +11:00
dunkeroni
47168b5505 chore: make ruff 2024-10-29 14:07:20 +11:00
dunkeroni
58152ec981 fix preview progress bar pre-denoise 2024-10-29 14:07:20 +11:00
dunkeroni
c74afbf332 convert to bgr on sdxl t2i 2024-10-29 14:07:20 +11:00
psychedelicious
7cdda00a54 feat(ui): rearrange canvas paste back nodes to save an image step
We were scaling the unscaled image and mask down before doing the paste-back, but this adds an extraneous step & image output.

We can do the paste-back first, then scale to output size after. So instead of 2 resizes before the paste-back, we have 1 resize after.

The end result is the same.
2024-10-29 11:13:31 +11:00
psychedelicious
a74282bce6 feat(ui): graph builders use objects for arg instead of many args 2024-10-29 11:13:31 +11:00
psychedelicious
107f048c7a feat(ui): extract canvas output node prefix to constant 2024-10-29 11:13:31 +11:00
Ryan Dick
a2486a5f06 Remove unused prediction_type and upcast_attention from from_single_file(...) calls. 2024-10-28 13:05:17 -04:00
Ryan Dick
07ab116efb Remove load_safety_checker=False from calls to from_single_file(...).
This param has been deprecated, and by including it (even when set to
False) the safety checker automatically gets downloaded.
2024-10-28 13:05:17 -04:00
Ryan Dick
1a13af3c7a Fix huggingface_hub.errors imports after version bump. 2024-10-28 13:05:17 -04:00
Ryan Dick
f2966a2594 Fix changed import for FromOriginalControlNetMixin after diffusers bump. 2024-10-28 13:05:17 -04:00
Ryan Dick
58bb97e3c6 Bump diffusers, accelerate, and huggingface-hub. 2024-10-28 13:05:17 -04:00
dunkeroni
34569a2410 Make T2I Adapters compatible with x8 resolutions 2024-10-27 15:38:22 -04:00
psychedelicious
a84aa5c049 fix(ui): canvas alerts blocking metadata panel 2024-10-27 09:46:01 +11:00
dunkeroni
acfa9c87ef Merge branch 'main' into sdxl_t2i_bgr 2024-10-25 23:44:13 -04:00
dunkeroni
f245d8e429 chore: make ruff 2024-10-25 23:43:33 -04:00
dunkeroni
62cf0f54e0 fix preview progress bar pre-denoise 2024-10-25 23:22:06 -04:00
dunkeroni
5f015e76ba convert to bgr on sdxl t2i 2024-10-25 23:04:17 -04:00
psychedelicious
aebcec28e0 chore: bump version to v5.3.0 2024-10-25 22:37:59 -04:00
psychedelicious
db1c5a94f7 feat(ui): image ctx -> New from Image -> Canvas as Raster/Control Layer 2024-10-25 22:27:00 -04:00
psychedelicious
56222a8493 feat(ui): organize layer context menu items 2024-10-25 22:27:00 -04:00
psychedelicious
b7510ce709 feat(ui): filter, select object and transform UI buttons
- Restore dedicated `Apply` buttons
- Remove icons from the buttons, too much noise when the words are short and clear
- Update loading state to show a spinner next to the `Process` button instead of on _every_ button
2024-10-25 22:27:00 -04:00
psychedelicious
5739799e2e fix(ui): close viewer when transforming 2024-10-25 22:27:00 -04:00
psychedelicious
813cf87920 feat(ui): move canvas alerts to top-left corner 2024-10-25 22:27:00 -04:00
psychedelicious
c95b151daf feat(ui): add layer title heading for canvas ctx menu 2024-10-25 22:27:00 -04:00
psychedelicious
a0f823a3cf feat(ui): reset shouldShowStagedImage flag when starting staging 2024-10-25 22:27:00 -04:00
Hippalectryon
64e0f6d688 Improve dev install docs
Fix numbering
2024-10-25 08:27:26 -04:00
psychedelicious
ddd5b1087c fix(nodes): return copies of objects in invocation ctx
Closes #6820
2024-10-25 08:26:09 -04:00
psychedelicious
008be9b846 feat(ui): add all save as options to filter 2024-10-25 08:12:14 -04:00
psychedelicious
8e7cabdc04 feat(ui): add Replace Current open to Select Object -> Save As 2024-10-25 08:12:14 -04:00
psychedelicious
a4c4237f99 feat(ui): use PiPlayFill for process buttons for filter & select object 2024-10-25 08:12:14 -04:00
psychedelicious
bda3740dcd feat(ui): use fill style icons for Filter 2024-10-25 08:12:14 -04:00
psychedelicious
5b4633baa9 feat(ui): use PiShapesFill icon for Select Object 2024-10-25 08:12:14 -04:00
psychedelicious
96351181cb feat(ui): make canvas layer toolbar icons a bit larger 2024-10-25 08:12:14 -04:00
psychedelicious
957d591d99 feat(ui): "Auto-Mask" -> "Select Object" 2024-10-25 08:12:14 -04:00
psychedelicious
75f605ba1a feat(ui): support inverted selection in auto-mask 2024-10-25 08:12:14 -04:00
psychedelicious
ab898a7180 chore(ui): typegen 2024-10-25 08:12:14 -04:00
psychedelicious
c9a4516ab1 feat(nodes): add invert to apply_tensor_mask_to_image 2024-10-25 08:12:14 -04:00
psychedelicious
fe97c0d5eb tweak(ui): default settings verbiage 2024-10-25 16:09:59 +11:00
psychedelicious
6056764840 feat(ui): disable default settings button when synced
A blue button is begging to be clicked, but clicking it will do nothing. Instead, we should communicate that no action is needed by disabling the button when the default settings are already in use.
2024-10-25 16:09:59 +11:00
psychedelicious
8747c0dbb0 fix(ui): handle no model selection in default settings tooltip 2024-10-25 16:09:59 +11:00
psychedelicious
c5cdd5f9c6 fix(ui): use const EMPTY_OBJECT to prevent rerenders 2024-10-25 16:09:59 +11:00
psychedelicious
abc5d53159 fix(ui): use explicit null check when comparing default settings
Using `&&` will result in false negatives for settings where a falsy value might be valid. For example, any setting for which 0 is a valid number. To be on the safe side, just use an explicit null check on all values.
2024-10-25 16:09:59 +11:00
psychedelicious
2f76019a89 tweak(ui): defaults sync tooltip styling 2024-10-25 16:09:59 +11:00
Mary Hipp
3f45beb1ed feat(ui): add out of sync details to model default settings button 2024-10-25 16:09:59 +11:00
Mary Hipp
bc1126a85b (ui): add setting for showing model descriptions in dropdown defaulted to true 2024-10-25 14:52:33 +11:00
psychedelicious
380017041e fix(app): mutating an image also changes the in-memory cached image
We use an in-memory cache for PIL images to reduce I/O. If a node mutates the image in any way, the cached image object is also updated (but the on-disk image file is not).

We've lucked out that this hasn't caused major issues in the past (well, maybe it has but we didn't understand them?) mainly because of a happy accident. When you call `context.images.get_pil` in a node, if you provide an image mode (e.g. `mode="RGB"`), we call `convert`  on the image. This returns a copy. The node can do whatever it wants to that copy and nothing breaks.

However, when mode is not specified, we return the image directly. This is where we get in trouble - nodes that load the image like this, and then mutate the image, update the cache. Other nodes that reference that same image will now get the mutated version of it.

The fix is super simple - we make sure to return only copies from `get_pil`.
2024-10-25 10:22:22 +11:00
psychedelicious
ab7cdbb7e0 fix(ui): do not delete point on right-mouse click 2024-10-25 10:22:22 +11:00
psychedelicious
e5b78d0221 fix(ui): canvas drop area grid layout 2024-10-25 10:22:22 +11:00
psychedelicious
1acaa6c486 chore: bump version to v5.3.0rc2 2024-10-25 07:50:58 +11:00
psychedelicious
b0381076b7 revert(ui): drop targets for inpaint mask and rg 2024-10-25 07:42:46 +11:00
psychedelicious
ffff2d6dbb feat(ui): add New from Image submenu for image ctx menu 2024-10-25 07:42:46 +11:00
psychedelicious
afa9f07649 fix(ui): missing cursor when transforming 2024-10-25 07:42:46 +11:00
psychedelicious
addb5c49ea feat(ui): support dnd images onto inpaint mask/rg entities 2024-10-25 07:42:46 +11:00
psychedelicious
a112d2d55b feat(ui): add logging to useCopyLayerToClipboard 2024-10-25 07:42:46 +11:00
psychedelicious
619a271c8a feat(ui): disable copy to clipboard when layer is empty 2024-10-25 07:42:46 +11:00
psychedelicious
909f2ee36d feat(ui): add help tooltip to automask 2024-10-25 07:42:46 +11:00
psychedelicious
b4cf3d9d03 fix(ui): canvas context menu w/ eraser tool erases 2024-10-25 07:42:46 +11:00
psychedelicious
e6ab6e0293 chore(ui): lint 2024-10-24 08:39:29 -04:00
psychedelicious
66d9c7c631 fix(ui): icon for automask save as 2024-10-24 08:39:29 -04:00
psychedelicious
fec45f3eb6 feat(ui): animate automask preview overlay 2024-10-24 08:39:29 -04:00
psychedelicious
7211d1a6fc feat(ui): add context menu options for layer type convert/copy 2024-10-24 08:39:29 -04:00
psychedelicious
f3069754a9 feat(ui): add logic to convert/copy between all layer types 2024-10-24 08:39:29 -04:00
psychedelicious
4f43152aeb fix(ui): handle pen/touch events on submenu 2024-10-24 08:39:29 -04:00
psychedelicious
7125055d02 fix(ui): icon menu item group spacing 2024-10-24 08:39:29 -04:00
psychedelicious
c91a9ce390 feat(ui): add pull bbox to global ref image ctx menu 2024-10-24 08:39:29 -04:00
psychedelicious
3e7b73da2c feat(ui): add entity context menu as canvas context menu sub-menu 2024-10-24 08:39:29 -04:00
psychedelicious
61ac50c00d feat(ui): use sub-menu for image metadata recall 2024-10-24 08:39:29 -04:00
psychedelicious
c1201f0bce feat(ui): add useSubMenu hook to abstract logic for sub-menus 2024-10-24 08:39:29 -04:00
psychedelicious
acdffac5ad feat(ui): close viewer when filtering/transforming/automasking 2024-10-24 08:39:29 -04:00
psychedelicious
e420300fa4 feat(ui): replace automask apply w/ save as menu 2024-10-24 08:39:29 -04:00
psychedelicious
260a5a4f9a feat(ui): add automask button to toolbar 2024-10-24 08:39:29 -04:00
psychedelicious
ed0c2006fe feat(ui): rename "foreground"/"background" -> "include"/"exclude" 2024-10-24 08:39:29 -04:00
psychedelicious
9ffd888c86 feat(ui): remove neutral points 2024-10-24 08:39:29 -04:00
psychedelicious
175a9dc28d feat(ui): more resilient auto-masking processing
- Use a hash of the last processed points instead of a `hasProcessed` flag to determine whether or not we should re-process a given set of points.
- Store point coords in state instead of pulling them out of the konva node positions. This makes moving a point a more explicit action in code.
- Add a `roundCoord` util to round the x and y values of a coordinate.
- Ensure we always re-process when $points changes.
2024-10-24 08:39:29 -04:00
psychedelicious
5764e4f7f2 chore(ui): lint 2024-10-24 23:34:06 +11:00
psychedelicious
4275a494b9 tweak(ui): bundle info icon 2024-10-24 23:34:06 +11:00
psychedelicious
a3deb8d30d tweak(ui): bundle tooltip styling 2024-10-24 23:34:06 +11:00
Mary Hipp
aafdb0a37b update popover copy 2024-10-24 23:34:06 +11:00
Mary Hipp
56a815719a update schema 2024-10-24 23:34:06 +11:00
Mary Hipp
4db26bfa3a (ui): add information popovers for other layer types 2024-10-24 23:34:06 +11:00
Mary Hipp
8d84ccb12b bump UI dep for combobox descriptions 2024-10-24 23:34:06 +11:00
Mary Hipp
3321d14997 undo show descriptions for now 2024-10-24 23:34:06 +11:00
maryhipp
43cc4684e1 (api) make sure all controlnet starter models will still have pre-processors correctly assigned when probed based on name 2024-10-24 23:34:06 +11:00
Mary Hipp
afa5a4b17c (ui): add informational popover for controlnet layers 2024-10-24 23:34:06 +11:00
Mary Hipp
33c433fe59 (ui): show models in starter bundles on hover, use previous_names for isInstalled logic, allow grouped model combobox to optionally show descriptions 2024-10-24 23:34:06 +11:00
maryhipp
9cd47fa857 (api): update names of starter models, add ability to track previous_names so it does not mess up logic that prevents dupe starter model installs 2024-10-24 23:34:06 +11:00
psychedelicious
32d9abe802 tweak(ui): prevent show/hide boards button cutoff
The use of hard 25% widths caused issues for some translations. Adjusted styling to not rely on any hard numbers. Tested with a project name and URL.
2024-10-24 08:21:16 -04:00
psychedelicious
3947d4a165 fix(ui): normalize infill alpha to 0-255 when building infill nodes
The browser/UI uses float 0-1 for alpha, while backend uses 0-255. We need to normalize the value when building the infill nodes for outpaint.
2024-10-24 19:22:36 +11:00
181 changed files with 5626 additions and 2019 deletions

View File

@@ -5,7 +5,7 @@ If you're a new contributor to InvokeAI or Open Source Projects, this is the gui
## New Contributor Checklist
- [x] Set up your local development environment & fork of InvokAI by following [the steps outlined here](../dev-environment.md)
- [x] Set up your local tooling with [this guide](InvokeAI/contributing/LOCAL_DEVELOPMENT/#developing-invokeai-in-vscode). Feel free to skip this step if you already have tooling you're comfortable with.
- [x] Set up your local tooling with [this guide](../LOCAL_DEVELOPMENT.md). Feel free to skip this step if you already have tooling you're comfortable with.
- [x] Familiarize yourself with [Git](https://www.atlassian.com/git) & our project structure by reading through the [development documentation](development.md)
- [x] Join the [#dev-chat](https://discord.com/channels/1020123559063990373/1049495067846524939) channel of the Discord
- [x] Choose an issue to work on! This can be achieved by asking in the #dev-chat channel, tackling a [good first issue](https://github.com/invoke-ai/InvokeAI/contribute) or finding an item on the [roadmap](https://github.com/orgs/invoke-ai/projects/7). If nothing in any of those places catches your eye, feel free to work on something of interest to you!

View File

@@ -17,46 +17,49 @@ If you just want to use Invoke, you should use the [installer][installer link].
## Setup
1. Run through the [requirements][requirements link].
1. [Fork and clone][forking link] the [InvokeAI repo][repo link].
1. Create an directory for user data (images, models, db, etc). This is typically at `~/invokeai`, but if you already have a non-dev install, you may want to create a separate directory for the dev install.
1. Create a python virtual environment inside the directory you just created:
2. [Fork and clone][forking link] the [InvokeAI repo][repo link].
3. Create an directory for user data (images, models, db, etc). This is typically at `~/invokeai`, but if you already have a non-dev install, you may want to create a separate directory for the dev install.
4. Create a python virtual environment inside the directory you just created:
```sh
python3 -m venv .venv --prompt InvokeAI-Dev
```
```sh
python3 -m venv .venv --prompt InvokeAI-Dev
```
1. Activate the venv (you'll need to do this every time you want to run the app):
5. Activate the venv (you'll need to do this every time you want to run the app):
```sh
source .venv/bin/activate
```
```sh
source .venv/bin/activate
```
1. Install the repo as an [editable install][editable install link]:
6. Install the repo as an [editable install][editable install link]:
```sh
pip install -e ".[dev,test,xformers]" --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu121
```
```sh
pip install -e ".[dev,test,xformers]" --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu121
```
Refer to the [manual installation][manual install link]] instructions for more determining the correct install options. `xformers` is optional, but `dev` and `test` are not.
Refer to the [manual installation][manual install link]] instructions for more determining the correct install options. `xformers` is optional, but `dev` and `test` are not.
1. Install the frontend dev toolchain:
7. Install the frontend dev toolchain:
- [`nodejs`](https://nodejs.org/) (recommend v20 LTS)
- [`pnpm`](https://pnpm.io/installation#installing-a-specific-version) (must be v8 - not v9!)
- [`pnpm`](https://pnpm.io/8.x/installation) (must be v8 - not v9!)
1. Do a production build of the frontend:
8. Do a production build of the frontend:
```sh
pnpm build
```
```sh
cd PATH_TO_INVOKEAI_REPO/invokeai/frontend/web
pnpm i
pnpm build
```
1. Start the application:
9. Start the application:
```sh
python scripts/invokeai-web.py
```
```sh
cd PATH_TO_INVOKEAI_REPO
python scripts/invokeai-web.py
```
1. Access the UI at `localhost:9090`.
10. Access the UI at `localhost:9090`.
## Updating the UI

View File

@@ -209,7 +209,7 @@ checkpoint models.
To solve this, go to the Model Manager tab (the cube), select the
checkpoint model that's giving you trouble, and press the "Convert"
button in the upper right of your browser window. This will conver the
button in the upper right of your browser window. This will convert the
checkpoint into a diffusers model, after which loading should be
faster and less memory-intensive.

View File

@@ -97,16 +97,16 @@ Prior to installing PyPatchMatch, you need to take the following steps:
sudo pacman -S --needed base-devel
```
2. Install `opencv` and `blas`:
2. Install `opencv`, `blas`, and required dependencies:
```sh
sudo pacman -S opencv blas
sudo pacman -S opencv blas fmt glew vtk hdf5
```
or for CUDA support
```sh
sudo pacman -S opencv-cuda blas
sudo pacman -S opencv-cuda blas fmt glew vtk hdf5
```
3. Fix the naming of the `opencv` package configuration file:

View File

@@ -259,7 +259,7 @@ def select_gpu() -> GpuType:
[
f"Detected the [gold1]{OS}-{ARCH}[/] platform",
"",
"See [deep_sky_blue1]https://invoke-ai.github.io/InvokeAI/#system[/] to ensure your system meets the minimum requirements.",
"See [deep_sky_blue1]https://invoke-ai.github.io/InvokeAI/installation/requirements/[/] to ensure your system meets the minimum requirements.",
"",
"[red3]🠶[/] [b]Your GPU drivers must be correctly installed before using InvokeAI![/] [red3]🠴[/]",
]

View File

@@ -68,7 +68,7 @@ do_line_input() {
printf "2: Open the developer console\n"
printf "3: Command-line help\n"
printf "Q: Quit\n\n"
printf "To update, download and run the installer from https://github.com/invoke-ai/InvokeAI/releases/latest.\n\n"
printf "To update, download and run the installer from https://github.com/invoke-ai/InvokeAI/releases/latest\n\n"
read -p "Please enter 1-4, Q: [1] " yn
choice=${yn:='1'}
do_choice $choice

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2023 Lincoln D. Stein
"""FastAPI route for model configuration records."""
import contextlib
import io
import pathlib
import shutil
@@ -10,6 +11,7 @@ from enum import Enum
from tempfile import TemporaryDirectory
from typing import List, Optional, Type
import huggingface_hub
from fastapi import Body, Path, Query, Response, UploadFile
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.routing import APIRouter
@@ -27,6 +29,7 @@ from invokeai.app.services.model_records import (
ModelRecordChanges,
UnknownModelException,
)
from invokeai.app.util.suppress_output import SuppressOutput
from invokeai.backend.model_manager.config import (
AnyModelConfig,
BaseModelType,
@@ -808,7 +811,11 @@ def get_is_installed(
for model in installed_models:
if model.source == starter_model.source:
return True
if model.name == starter_model.name and model.base == starter_model.base and model.type == starter_model.type:
if (
(model.name == starter_model.name or model.name in starter_model.previous_names)
and model.base == starter_model.base
and model.type == starter_model.type
):
return True
return False
@@ -919,3 +926,51 @@ async def get_stats() -> Optional[CacheStats]:
"""Return performance statistics on the model manager's RAM cache. Will return null if no models have been loaded."""
return ApiDependencies.invoker.services.model_manager.load.ram_cache.stats
class HFTokenStatus(str, Enum):
VALID = "valid"
INVALID = "invalid"
UNKNOWN = "unknown"
class HFTokenHelper:
@classmethod
def get_status(cls) -> HFTokenStatus:
try:
if huggingface_hub.get_token_permission(huggingface_hub.get_token()):
# Valid token!
return HFTokenStatus.VALID
# No token set
return HFTokenStatus.INVALID
except Exception:
return HFTokenStatus.UNKNOWN
@classmethod
def set_token(cls, token: str) -> HFTokenStatus:
with SuppressOutput(), contextlib.suppress(Exception):
huggingface_hub.login(token=token, add_to_git_credential=False)
return cls.get_status()
@model_manager_router.get("/hf_login", operation_id="get_hf_login_status", response_model=HFTokenStatus)
async def get_hf_login_status() -> HFTokenStatus:
token_status = HFTokenHelper.get_status()
if token_status is HFTokenStatus.UNKNOWN:
ApiDependencies.invoker.services.logger.warning("Unable to verify HF token")
return token_status
@model_manager_router.post("/hf_login", operation_id="do_hf_login", response_model=HFTokenStatus)
async def do_hf_login(
token: str = Body(description="Hugging Face token to use for login", embed=True),
) -> HFTokenStatus:
HFTokenHelper.set_token(token)
token_status = HFTokenHelper.get_status()
if token_status is HFTokenStatus.UNKNOWN:
ApiDependencies.invoker.services.logger.warning("Unable to verify HF token")
return token_status

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import inspect
import re
import sys
import warnings
from abc import ABC, abstractmethod
from enum import Enum
@@ -192,12 +193,19 @@ class BaseInvocation(ABC, BaseModel):
"""Gets a pydantc TypeAdapter for the union of all invocation types."""
if not cls._typeadapter or cls._typeadapter_needs_update:
AnyInvocation = TypeAliasType(
"AnyInvocation", Annotated[Union[tuple(cls._invocation_classes)], Field(discriminator="type")]
"AnyInvocation", Annotated[Union[tuple(cls.get_invocations())], Field(discriminator="type")]
)
cls._typeadapter = TypeAdapter(AnyInvocation)
cls._typeadapter_needs_update = False
return cls._typeadapter
@classmethod
def invalidate_typeadapter(cls) -> None:
"""Invalidates the typeadapter, forcing it to be rebuilt on next access. If the invocation allowlist or
denylist is changed, this should be called to ensure the typeadapter is updated and validation respects
the updated allowlist and denylist."""
cls._typeadapter_needs_update = True
@classmethod
def get_invocations(cls) -> Iterable[BaseInvocation]:
"""Gets all invocations, respecting the allowlist and denylist."""
@@ -479,6 +487,26 @@ def invocation(
title="type", default=invocation_type, json_schema_extra={"field_kind": FieldKind.NodeAttribute}
)
# Validate the `invoke()` method is implemented
if "invoke" in cls.__abstractmethods__:
raise ValueError(f'Invocation "{invocation_type}" must implement the "invoke" method')
# And validate that `invoke()` returns a subclass of `BaseInvocationOutput
invoke_return_annotation = signature(cls.invoke).return_annotation
try:
# TODO(psyche): If `invoke()` is not defined, `return_annotation` ends up as the string "BaseInvocationOutput"
# instead of the class `BaseInvocationOutput`. This may be a pydantic bug: https://github.com/pydantic/pydantic/issues/7978
if isinstance(invoke_return_annotation, str):
invoke_return_annotation = getattr(sys.modules[cls.__module__], invoke_return_annotation)
assert invoke_return_annotation is not BaseInvocationOutput
assert issubclass(invoke_return_annotation, BaseInvocationOutput)
except Exception:
raise ValueError(
f'Invocation "{invocation_type}" must have a return annotation of a subclass of BaseInvocationOutput (got "{invoke_return_annotation}")'
)
docstring = cls.__doc__
cls = create_model(
cls.__qualname__,

View File

@@ -13,6 +13,7 @@ from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel
from diffusers.schedulers.scheduling_dpmsolver_sde import DPMSolverSDEScheduler
from diffusers.schedulers.scheduling_tcd import TCDScheduler
from diffusers.schedulers.scheduling_utils import SchedulerMixin as Scheduler
from PIL import Image
from pydantic import field_validator
from torchvision.transforms.functional import resize as tv_resize
from transformers import CLIPVisionModelWithProjection
@@ -510,6 +511,7 @@ class DenoiseLatentsInvocation(BaseInvocation):
context: InvocationContext,
t2i_adapters: Optional[Union[T2IAdapterField, list[T2IAdapterField]]],
ext_manager: ExtensionsManager,
bgr_mode: bool = False,
) -> None:
if t2i_adapters is None:
return
@@ -519,6 +521,10 @@ class DenoiseLatentsInvocation(BaseInvocation):
t2i_adapters = [t2i_adapters]
for t2i_adapter_field in t2i_adapters:
image = context.images.get_pil(t2i_adapter_field.image.image_name)
if bgr_mode: # SDXL t2i trained on cv2's BGR outputs, but PIL won't convert straight to BGR
r, g, b = image.split()
image = Image.merge("RGB", (b, g, r))
ext_manager.add_extension(
T2IAdapterExt(
node_context=context,
@@ -616,13 +622,17 @@ class DenoiseLatentsInvocation(BaseInvocation):
for t2i_adapter_field in t2i_adapter:
t2i_adapter_model_config = context.models.get_config(t2i_adapter_field.t2i_adapter_model.key)
t2i_adapter_loaded_model = context.models.load(t2i_adapter_field.t2i_adapter_model)
image = context.images.get_pil(t2i_adapter_field.image.image_name)
image = context.images.get_pil(t2i_adapter_field.image.image_name, mode="RGB")
# The max_unet_downscale is the maximum amount that the UNet model downscales the latent image internally.
if t2i_adapter_model_config.base == BaseModelType.StableDiffusion1:
max_unet_downscale = 8
elif t2i_adapter_model_config.base == BaseModelType.StableDiffusionXL:
max_unet_downscale = 4
# SDXL adapters are trained on cv2's BGR outputs
r, g, b = image.split()
image = Image.merge("RGB", (b, g, r))
else:
raise ValueError(f"Unexpected T2I-Adapter base model type: '{t2i_adapter_model_config.base}'.")
@@ -630,29 +640,39 @@ class DenoiseLatentsInvocation(BaseInvocation):
with t2i_adapter_loaded_model as t2i_adapter_model:
total_downscale_factor = t2i_adapter_model.total_downscale_factor
# Resize the T2I-Adapter input image.
# We select the resize dimensions so that after the T2I-Adapter's total_downscale_factor is applied, the
# result will match the latent image's dimensions after max_unet_downscale is applied.
t2i_input_height = latents_shape[2] // max_unet_downscale * total_downscale_factor
t2i_input_width = latents_shape[3] // max_unet_downscale * total_downscale_factor
# Note: We have hard-coded `do_classifier_free_guidance=False`. This is because we only want to prepare
# a single image. If CFG is enabled, we will duplicate the resultant tensor after applying the
# T2I-Adapter model.
#
# Note: We re-use the `prepare_control_image(...)` from ControlNet for T2I-Adapter, because it has many
# of the same requirements (e.g. preserving binary masks during resize).
# Assuming fixed dimensional scaling of LATENT_SCALE_FACTOR.
_, _, latent_height, latent_width = latents_shape
control_height_resize = latent_height * LATENT_SCALE_FACTOR
control_width_resize = latent_width * LATENT_SCALE_FACTOR
t2i_image = prepare_control_image(
image=image,
do_classifier_free_guidance=False,
width=t2i_input_width,
height=t2i_input_height,
width=control_width_resize,
height=control_height_resize,
num_channels=t2i_adapter_model.config["in_channels"], # mypy treats this as a FrozenDict
device=t2i_adapter_model.device,
dtype=t2i_adapter_model.dtype,
resize_mode=t2i_adapter_field.resize_mode,
)
# Resize the T2I-Adapter input image.
# We select the resize dimensions so that after the T2I-Adapter's total_downscale_factor is applied, the
# result will match the latent image's dimensions after max_unet_downscale is applied.
# We crop the image to this size so that the positions match the input image on non-standard resolutions
t2i_input_height = latents_shape[2] // max_unet_downscale * total_downscale_factor
t2i_input_width = latents_shape[3] // max_unet_downscale * total_downscale_factor
if t2i_image.shape[2] > t2i_input_height or t2i_image.shape[3] > t2i_input_width:
t2i_image = t2i_image[
:, :, : min(t2i_image.shape[2], t2i_input_height), : min(t2i_image.shape[3], t2i_input_width)
]
adapter_state = t2i_adapter_model(t2i_image)
if do_classifier_free_guidance:
@@ -900,7 +920,8 @@ class DenoiseLatentsInvocation(BaseInvocation):
# ext = extension_field.to_extension(exit_stack, context, ext_manager)
# ext_manager.add_extension(ext)
self.parse_controlnet_field(exit_stack, context, self.control, ext_manager)
self.parse_t2i_adapter_field(exit_stack, context, self.t2i_adapter, ext_manager)
bgr_mode = self.unet.unet.base == BaseModelType.StableDiffusionXL
self.parse_t2i_adapter_field(exit_stack, context, self.t2i_adapter, ext_manager, bgr_mode)
# ext: t2i/ip adapter
ext_manager.run_callback(ExtensionCallbackType.SETUP, denoise_ctx)

View File

@@ -165,6 +165,7 @@ class ApplyMaskTensorToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
mask: TensorField = InputField(description="The mask tensor to apply.")
image: ImageField = InputField(description="The image to apply the mask to.")
invert: bool = InputField(default=False, description="Whether to invert the mask.")
def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.images.get_pil(self.image.image_name, mode="RGBA")
@@ -179,6 +180,9 @@ class ApplyMaskTensorToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
mask = mask > 0.5
mask_np = (mask.float() * 255).byte().cpu().numpy().astype(np.uint8)
if self.invert:
mask_np = 255 - mask_np
# Apply the mask only to the alpha channel where the original alpha is non-zero. This preserves the original
# image's transparency - else the transparent regions would end up as opaque black.

View File

@@ -1,3 +1,4 @@
from copy import deepcopy
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Optional, Union
@@ -221,7 +222,7 @@ class ImagesInterface(InvocationContextInterface):
)
def get_pil(self, image_name: str, mode: IMAGE_MODES | None = None) -> Image:
"""Gets an image as a PIL Image object.
"""Gets an image as a PIL Image object. This method returns a copy of the image.
Args:
image_name: The name of the image to get.
@@ -233,11 +234,15 @@ class ImagesInterface(InvocationContextInterface):
image = self._services.images.get_pil_image(image_name)
if mode and mode != image.mode:
try:
# convert makes a copy!
image = image.convert(mode)
except ValueError:
self._services.logger.warning(
f"Could not convert image from {image.mode} to {mode}. Using original mode instead."
)
else:
# copy the image to prevent the user from modifying the original
image = image.copy()
return image
def get_metadata(self, image_name: str) -> Optional[MetadataField]:
@@ -290,15 +295,15 @@ class TensorsInterface(InvocationContextInterface):
return name
def load(self, name: str) -> Tensor:
"""Loads a tensor by name.
"""Loads a tensor by name. This method returns a copy of the tensor.
Args:
name: The name of the tensor to load.
Returns:
The loaded tensor.
The tensor.
"""
return self._services.tensors.load(name)
return self._services.tensors.load(name).clone()
class ConditioningInterface(InvocationContextInterface):
@@ -316,16 +321,16 @@ class ConditioningInterface(InvocationContextInterface):
return name
def load(self, name: str) -> ConditioningFieldData:
"""Loads conditioning data by name.
"""Loads conditioning data by name. This method returns a copy of the conditioning data.
Args:
name: The name of the conditioning data to load.
Returns:
The loaded conditioning data.
The conditioning data.
"""
return self._services.conditioning.load(name)
return deepcopy(self._services.conditioning.load(name))
class ModelsInterface(InvocationContextInterface):

View File

@@ -117,8 +117,6 @@ class StableDiffusionDiffusersModel(GenericDiffusersLoader):
load_class = load_classes[config.base][config.variant]
except KeyError as e:
raise Exception(f"No diffusers pipeline known for base={config.base}, variant={config.variant}") from e
prediction_type = config.prediction_type.value
upcast_attention = config.upcast_attention
# Without SilenceWarnings we get log messages like this:
# site-packages/huggingface_hub/file_download.py:1132: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.
@@ -129,13 +127,7 @@ class StableDiffusionDiffusersModel(GenericDiffusersLoader):
# ['text_model.embeddings.position_ids']
with SilenceWarnings():
pipeline = load_class.from_single_file(
config.path,
torch_dtype=self._torch_dtype,
prediction_type=prediction_type,
upcast_attention=upcast_attention,
load_safety_checker=False,
)
pipeline = load_class.from_single_file(config.path, torch_dtype=self._torch_dtype)
if not submodel_type:
return pipeline

View File

@@ -20,7 +20,7 @@ from typing import Optional
import requests
from huggingface_hub import HfApi, configure_http_backend, hf_hub_url
from huggingface_hub.utils._errors import RepositoryNotFoundError, RevisionNotFoundError
from huggingface_hub.errors import RepositoryNotFoundError, RevisionNotFoundError
from pydantic.networks import AnyHttpUrl
from requests.sessions import Session

View File

@@ -462,8 +462,9 @@ MODEL_NAME_TO_PREPROCESSOR = {
"normal": "normalbae_image_processor",
"sketch": "pidi_image_processor",
"scribble": "lineart_image_processor",
"lineart": "lineart_image_processor",
"lineart anime": "lineart_anime_image_processor",
"lineart_anime": "lineart_anime_image_processor",
"lineart": "lineart_image_processor",
"softedge": "hed_image_processor",
"hed": "hed_image_processor",
"shuffle": "content_shuffle_image_processor",

View File

@@ -13,6 +13,9 @@ class StarterModelWithoutDependencies(BaseModel):
type: ModelType
format: Optional[ModelFormat] = None
is_installed: bool = False
# allows us to track what models a user has installed across name changes within starter models
# if you update a starter model name, please add the old one to this list for that starter model
previous_names: list[str] = []
class StarterModel(StarterModelWithoutDependencies):
@@ -243,44 +246,49 @@ easy_neg_sd1 = StarterModel(
# endregion
# region IP Adapter
ip_adapter_sd1 = StarterModel(
name="IP Adapter",
name="Standard Reference (IP Adapter)",
base=BaseModelType.StableDiffusion1,
source="https://huggingface.co/InvokeAI/ip_adapter_sd15/resolve/main/ip-adapter_sd15.safetensors",
description="IP-Adapter for SD 1.5 models",
description="References images with a more generalized/looser degree of precision.",
type=ModelType.IPAdapter,
dependencies=[ip_adapter_sd_image_encoder],
previous_names=["IP Adapter"],
)
ip_adapter_plus_sd1 = StarterModel(
name="IP Adapter Plus",
name="Precise Reference (IP Adapter Plus)",
base=BaseModelType.StableDiffusion1,
source="https://huggingface.co/InvokeAI/ip_adapter_plus_sd15/resolve/main/ip-adapter-plus_sd15.safetensors",
description="Refined IP-Adapter for SD 1.5 models",
description="References images with a higher degree of precision.",
type=ModelType.IPAdapter,
dependencies=[ip_adapter_sd_image_encoder],
previous_names=["IP Adapter Plus"],
)
ip_adapter_plus_face_sd1 = StarterModel(
name="IP Adapter Plus Face",
name="Face Reference (IP Adapter Plus Face)",
base=BaseModelType.StableDiffusion1,
source="https://huggingface.co/InvokeAI/ip_adapter_plus_face_sd15/resolve/main/ip-adapter-plus-face_sd15.safetensors",
description="Refined IP-Adapter for SD 1.5 models, adapted for faces",
description="References images with a higher degree of precision, adapted for faces",
type=ModelType.IPAdapter,
dependencies=[ip_adapter_sd_image_encoder],
previous_names=["IP Adapter Plus Face"],
)
ip_adapter_sdxl = StarterModel(
name="IP Adapter SDXL",
name="Standard Reference (IP Adapter ViT-H)",
base=BaseModelType.StableDiffusionXL,
source="https://huggingface.co/InvokeAI/ip_adapter_sdxl_vit_h/resolve/main/ip-adapter_sdxl_vit-h.safetensors",
description="IP-Adapter for SDXL models",
description="References images with a higher degree of precision.",
type=ModelType.IPAdapter,
dependencies=[ip_adapter_sdxl_image_encoder],
previous_names=["IP Adapter SDXL"],
)
ip_adapter_flux = StarterModel(
name="XLabs FLUX IP-Adapter",
name="Standard Reference (XLabs FLUX IP-Adapter)",
base=BaseModelType.Flux,
source="https://huggingface.co/XLabs-AI/flux-ip-adapter/resolve/main/flux-ip-adapter.safetensors",
description="FLUX IP-Adapter",
description="References images with a more generalized/looser degree of precision.",
type=ModelType.IPAdapter,
dependencies=[clip_vit_l_image_encoder],
previous_names=["XLabs FLUX IP-Adapter"],
)
# endregion
# region ControlNet
@@ -299,157 +307,162 @@ qr_code_cnet_sdxl = StarterModel(
type=ModelType.ControlNet,
)
canny_sd1 = StarterModel(
name="canny",
name="Hard Edge Detection (canny)",
base=BaseModelType.StableDiffusion1,
source="lllyasviel/control_v11p_sd15_canny",
description="ControlNet weights trained on sd-1.5 with canny conditioning.",
description="Uses detected edges in the image to control composition.",
type=ModelType.ControlNet,
previous_names=["canny"],
)
inpaint_cnet_sd1 = StarterModel(
name="inpaint",
name="Inpainting",
base=BaseModelType.StableDiffusion1,
source="lllyasviel/control_v11p_sd15_inpaint",
description="ControlNet weights trained on sd-1.5 with canny conditioning, inpaint version",
type=ModelType.ControlNet,
previous_names=["inpaint"],
)
mlsd_sd1 = StarterModel(
name="mlsd",
name="Line Drawing (mlsd)",
base=BaseModelType.StableDiffusion1,
source="lllyasviel/control_v11p_sd15_mlsd",
description="ControlNet weights trained on sd-1.5 with canny conditioning, MLSD version",
description="Uses straight line detection for controlling the generation.",
type=ModelType.ControlNet,
previous_names=["mlsd"],
)
depth_sd1 = StarterModel(
name="depth",
name="Depth Map",
base=BaseModelType.StableDiffusion1,
source="lllyasviel/control_v11f1p_sd15_depth",
description="ControlNet weights trained on sd-1.5 with depth conditioning",
description="Uses depth information in the image to control the depth in the generation.",
type=ModelType.ControlNet,
previous_names=["depth"],
)
normal_bae_sd1 = StarterModel(
name="normal_bae",
name="Lighting Detection (Normals)",
base=BaseModelType.StableDiffusion1,
source="lllyasviel/control_v11p_sd15_normalbae",
description="ControlNet weights trained on sd-1.5 with normalbae image conditioning",
description="Uses detected lighting information to guide the lighting of the composition.",
type=ModelType.ControlNet,
previous_names=["normal_bae"],
)
seg_sd1 = StarterModel(
name="seg",
name="Segmentation Map",
base=BaseModelType.StableDiffusion1,
source="lllyasviel/control_v11p_sd15_seg",
description="ControlNet weights trained on sd-1.5 with seg image conditioning",
description="Uses segmentation maps to guide the structure of the composition.",
type=ModelType.ControlNet,
previous_names=["seg"],
)
lineart_sd1 = StarterModel(
name="lineart",
name="Lineart",
base=BaseModelType.StableDiffusion1,
source="lllyasviel/control_v11p_sd15_lineart",
description="ControlNet weights trained on sd-1.5 with lineart image conditioning",
description="Uses lineart detection to guide the lighting of the composition.",
type=ModelType.ControlNet,
previous_names=["lineart"],
)
lineart_anime_sd1 = StarterModel(
name="lineart_anime",
name="Lineart Anime",
base=BaseModelType.StableDiffusion1,
source="lllyasviel/control_v11p_sd15s2_lineart_anime",
description="ControlNet weights trained on sd-1.5 with anime image conditioning",
description="Uses anime lineart detection to guide the lighting of the composition.",
type=ModelType.ControlNet,
previous_names=["lineart_anime"],
)
openpose_sd1 = StarterModel(
name="openpose",
name="Pose Detection (openpose)",
base=BaseModelType.StableDiffusion1,
source="lllyasviel/control_v11p_sd15_openpose",
description="ControlNet weights trained on sd-1.5 with openpose image conditioning",
description="Uses pose information to control the pose of human characters in the generation.",
type=ModelType.ControlNet,
previous_names=["openpose"],
)
scribble_sd1 = StarterModel(
name="scribble",
name="Contour Detection (scribble)",
base=BaseModelType.StableDiffusion1,
source="lllyasviel/control_v11p_sd15_scribble",
description="ControlNet weights trained on sd-1.5 with scribble image conditioning",
description="Uses edges, contours, or line art in the image to control composition.",
type=ModelType.ControlNet,
previous_names=["scribble"],
)
softedge_sd1 = StarterModel(
name="softedge",
name="Soft Edge Detection (softedge)",
base=BaseModelType.StableDiffusion1,
source="lllyasviel/control_v11p_sd15_softedge",
description="ControlNet weights trained on sd-1.5 with soft edge conditioning",
description="Uses a soft edge detection map to control composition.",
type=ModelType.ControlNet,
previous_names=["softedge"],
)
shuffle_sd1 = StarterModel(
name="shuffle",
name="Remix (shuffle)",
base=BaseModelType.StableDiffusion1,
source="lllyasviel/control_v11e_sd15_shuffle",
description="ControlNet weights trained on sd-1.5 with shuffle image conditioning",
type=ModelType.ControlNet,
previous_names=["shuffle"],
)
tile_sd1 = StarterModel(
name="tile",
name="Tile",
base=BaseModelType.StableDiffusion1,
source="lllyasviel/control_v11f1e_sd15_tile",
description="ControlNet weights trained on sd-1.5 with tiled image conditioning",
type=ModelType.ControlNet,
)
ip2p_sd1 = StarterModel(
name="ip2p",
base=BaseModelType.StableDiffusion1,
source="lllyasviel/control_v11e_sd15_ip2p",
description="ControlNet weights trained on sd-1.5 with ip2p conditioning.",
description="Uses image data to replicate exact colors/structure in the resulting generation.",
type=ModelType.ControlNet,
previous_names=["tile"],
)
canny_sdxl = StarterModel(
name="canny-sdxl",
name="Hard Edge Detection (canny)",
base=BaseModelType.StableDiffusionXL,
source="xinsir/controlNet-canny-sdxl-1.0",
description="ControlNet weights trained on sdxl-1.0 with canny conditioning, by Xinsir.",
description="Uses detected edges in the image to control composition.",
type=ModelType.ControlNet,
previous_names=["canny-sdxl"],
)
depth_sdxl = StarterModel(
name="depth-sdxl",
name="Depth Map",
base=BaseModelType.StableDiffusionXL,
source="diffusers/controlNet-depth-sdxl-1.0",
description="ControlNet weights trained on sdxl-1.0 with depth conditioning.",
description="Uses depth information in the image to control the depth in the generation.",
type=ModelType.ControlNet,
previous_names=["depth-sdxl"],
)
softedge_sdxl = StarterModel(
name="softedge-dexined-sdxl",
name="Soft Edge Detection (softedge)",
base=BaseModelType.StableDiffusionXL,
source="SargeZT/controlNet-sd-xl-1.0-softedge-dexined",
description="ControlNet weights trained on sdxl-1.0 with dexined soft edge preprocessing.",
type=ModelType.ControlNet,
)
depth_zoe_16_sdxl = StarterModel(
name="depth-16bit-zoe-sdxl",
base=BaseModelType.StableDiffusionXL,
source="SargeZT/controlNet-sd-xl-1.0-depth-16bit-zoe",
description="ControlNet weights trained on sdxl-1.0 with Zoe's preprocessor (16 bits).",
type=ModelType.ControlNet,
)
depth_zoe_32_sdxl = StarterModel(
name="depth-zoe-sdxl",
base=BaseModelType.StableDiffusionXL,
source="diffusers/controlNet-zoe-depth-sdxl-1.0",
description="ControlNet weights trained on sdxl-1.0 with Zoe's preprocessor (32 bits).",
description="Uses a soft edge detection map to control composition.",
type=ModelType.ControlNet,
previous_names=["softedge-dexined-sdxl"],
)
openpose_sdxl = StarterModel(
name="openpose-sdxl",
name="Pose Detection (openpose)",
base=BaseModelType.StableDiffusionXL,
source="xinsir/controlNet-openpose-sdxl-1.0",
description="ControlNet weights trained on sdxl-1.0 compatible with the DWPose processor by Xinsir.",
description="Uses pose information to control the pose of human characters in the generation.",
type=ModelType.ControlNet,
previous_names=["openpose-sdxl", "controlnet-openpose-sdxl"],
)
scribble_sdxl = StarterModel(
name="scribble-sdxl",
name="Contour Detection (scribble)",
base=BaseModelType.StableDiffusionXL,
source="xinsir/controlNet-scribble-sdxl-1.0",
description="ControlNet weights trained on sdxl-1.0 compatible with various lineart processors and black/white sketches by Xinsir.",
description="Uses edges, contours, or line art in the image to control composition.",
type=ModelType.ControlNet,
previous_names=["scribble-sdxl", "controlnet-scribble-sdxl"],
)
tile_sdxl = StarterModel(
name="tile-sdxl",
name="Tile",
base=BaseModelType.StableDiffusionXL,
source="xinsir/controlNet-tile-sdxl-1.0",
description="ControlNet weights trained on sdxl-1.0 with tiled image conditioning",
description="Uses image data to replicate exact colors/structure in the resulting generation.",
type=ModelType.ControlNet,
previous_names=["tile-sdxl"],
)
union_cnet_sdxl = StarterModel(
name="Multi-Guidance Detection (Union Pro)",
base=BaseModelType.StableDiffusionXL,
source="InvokeAI/Xinsir-SDXL_Controlnet_Union",
description="A unified ControlNet for SDXL model that supports 10+ control types",
type=ModelType.ControlNet,
)
union_cnet_flux = StarterModel(
@@ -462,60 +475,52 @@ union_cnet_flux = StarterModel(
# endregion
# region T2I Adapter
t2i_canny_sd1 = StarterModel(
name="canny-sd15",
name="Hard Edge Detection (canny)",
base=BaseModelType.StableDiffusion1,
source="TencentARC/t2iadapter_canny_sd15v2",
description="T2I Adapter weights trained on sd-1.5 with canny conditioning.",
description="Uses detected edges in the image to control composition",
type=ModelType.T2IAdapter,
previous_names=["canny-sd15"],
)
t2i_sketch_sd1 = StarterModel(
name="sketch-sd15",
name="Sketch",
base=BaseModelType.StableDiffusion1,
source="TencentARC/t2iadapter_sketch_sd15v2",
description="T2I Adapter weights trained on sd-1.5 with sketch conditioning.",
description="Uses a sketch to control composition",
type=ModelType.T2IAdapter,
previous_names=["sketch-sd15"],
)
t2i_depth_sd1 = StarterModel(
name="depth-sd15",
name="Depth Map",
base=BaseModelType.StableDiffusion1,
source="TencentARC/t2iadapter_depth_sd15v2",
description="T2I Adapter weights trained on sd-1.5 with depth conditioning.",
type=ModelType.T2IAdapter,
)
t2i_zoe_depth_sd1 = StarterModel(
name="zoedepth-sd15",
base=BaseModelType.StableDiffusion1,
source="TencentARC/t2iadapter_zoedepth_sd15v1",
description="T2I Adapter weights trained on sd-1.5 with zoe depth conditioning.",
description="Uses depth information in the image to control the depth in the generation.",
type=ModelType.T2IAdapter,
previous_names=["depth-sd15"],
)
t2i_canny_sdxl = StarterModel(
name="canny-sdxl",
name="Hard Edge Detection (canny)",
base=BaseModelType.StableDiffusionXL,
source="TencentARC/t2i-adapter-canny-sdxl-1.0",
description="T2I Adapter weights trained on sdxl-1.0 with canny conditioning.",
type=ModelType.T2IAdapter,
)
t2i_zoe_depth_sdxl = StarterModel(
name="zoedepth-sdxl",
base=BaseModelType.StableDiffusionXL,
source="TencentARC/t2i-adapter-depth-zoe-sdxl-1.0",
description="T2I Adapter weights trained on sdxl-1.0 with zoe depth conditioning.",
description="Uses detected edges in the image to control composition",
type=ModelType.T2IAdapter,
previous_names=["canny-sdxl"],
)
t2i_lineart_sdxl = StarterModel(
name="lineart-sdxl",
name="Lineart",
base=BaseModelType.StableDiffusionXL,
source="TencentARC/t2i-adapter-lineart-sdxl-1.0",
description="T2I Adapter weights trained on sdxl-1.0 with lineart conditioning.",
description="Uses lineart detection to guide the lighting of the composition.",
type=ModelType.T2IAdapter,
previous_names=["lineart-sdxl"],
)
t2i_sketch_sdxl = StarterModel(
name="sketch-sdxl",
name="Sketch",
base=BaseModelType.StableDiffusionXL,
source="TencentARC/t2i-adapter-sketch-sdxl-1.0",
description="T2I Adapter weights trained on sdxl-1.0 with sketch conditioning.",
description="Uses a sketch to control composition",
type=ModelType.T2IAdapter,
previous_names=["sketch-sdxl"],
)
# endregion
# region SpandrelImageToImage
@@ -600,22 +605,18 @@ STARTER_MODELS: list[StarterModel] = [
softedge_sd1,
shuffle_sd1,
tile_sd1,
ip2p_sd1,
canny_sdxl,
depth_sdxl,
softedge_sdxl,
depth_zoe_16_sdxl,
depth_zoe_32_sdxl,
openpose_sdxl,
scribble_sdxl,
tile_sdxl,
union_cnet_sdxl,
union_cnet_flux,
t2i_canny_sd1,
t2i_sketch_sd1,
t2i_depth_sd1,
t2i_zoe_depth_sd1,
t2i_canny_sdxl,
t2i_zoe_depth_sdxl,
t2i_lineart_sdxl,
t2i_sketch_sdxl,
realesrgan_x4,
@@ -646,7 +647,6 @@ sd1_bundle: list[StarterModel] = [
softedge_sd1,
shuffle_sd1,
tile_sd1,
ip2p_sd1,
swinir,
]
@@ -657,8 +657,6 @@ sdxl_bundle: list[StarterModel] = [
canny_sdxl,
depth_sdxl,
softedge_sdxl,
depth_zoe_16_sdxl,
depth_zoe_32_sdxl,
openpose_sdxl,
scribble_sdxl,
tile_sdxl,

View File

@@ -499,6 +499,22 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline):
for idx, value in enumerate(single_t2i_adapter_data.adapter_state):
accum_adapter_state[idx] += value * t2i_adapter_weight
# Hack: force compatibility with irregular resolutions by padding the feature map with zeros
for idx, tensor in enumerate(accum_adapter_state):
# The tensor size is supposed to be some integer downscale factor of the latents size.
# Internally, the unet will pad the latents before downscaling between levels when it is no longer divisible by its downscale factor.
# If the latent size does not scale down evenly, we need to pad the tensor so that it matches the the downscaled padded latents later on.
scale_factor = latents.size()[-1] // tensor.size()[-1]
required_padding_width = math.ceil(latents.size()[-1] / scale_factor) - tensor.size()[-1]
required_padding_height = math.ceil(latents.size()[-2] / scale_factor) - tensor.size()[-2]
tensor = torch.nn.functional.pad(
tensor,
(0, required_padding_width, 0, required_padding_height, 0, 0, 0, 0),
mode="constant",
value=0,
)
accum_adapter_state[idx] = tensor
down_intrablock_additional_residuals = accum_adapter_state
# Handle inpainting models.

View File

@@ -33,7 +33,7 @@ class PreviewExt(ExtensionBase):
def initial_preview(self, ctx: DenoiseContext):
self.callback(
PipelineIntermediateState(
step=-1,
step=0,
order=ctx.scheduler.order,
total_steps=len(ctx.inputs.timesteps),
timestep=int(ctx.scheduler.config.num_train_timesteps), # TODO: is there any code which uses it?

View File

@@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union
import diffusers
import torch
from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.loaders import FromOriginalControlNetMixin
from diffusers.loaders.single_file_model import FromOriginalModelMixin
from diffusers.models.attention_processor import AttentionProcessor, AttnProcessor
from diffusers.models.controlnet import ControlNetConditioningEmbedding, ControlNetOutput, zero_module
from diffusers.models.embeddings import (
@@ -32,7 +32,9 @@ from invokeai.backend.util.logging import InvokeAILogger
logger = InvokeAILogger.get_logger(__name__)
class ControlNetModel(ModelMixin, ConfigMixin, FromOriginalControlNetMixin):
# NOTE(ryand): I'm not the origina author of this code, but for future reference, it appears that this class was copied
# from diffusers in order to add support for the encoder_attention_mask argument.
class ControlNetModel(ModelMixin, ConfigMixin, FromOriginalModelMixin):
"""
A ControlNet model.

View File

@@ -58,7 +58,7 @@
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fontsource-variable/inter": "^5.1.0",
"@invoke-ai/ui-library": "^0.0.42",
"@invoke-ai/ui-library": "^0.0.43",
"@nanostores/react": "^0.7.3",
"@reduxjs/toolkit": "2.2.3",
"@roarr/browser-log-writer": "^1.3.0",

View File

@@ -24,8 +24,8 @@ dependencies:
specifier: ^5.1.0
version: 5.1.0
'@invoke-ai/ui-library':
specifier: ^0.0.42
version: 0.0.42(@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.1.0)(@types/react@18.3.11)(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1)
specifier: ^0.0.43
version: 0.0.43(@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.1.0)(@types/react@18.3.11)(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1)
'@nanostores/react':
specifier: ^0.7.3
version: 0.7.3(nanostores@0.11.3)(react@18.3.1)
@@ -1696,20 +1696,20 @@ packages:
prettier: 3.3.3
dev: true
/@invoke-ai/ui-library@0.0.42(@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.1.0)(@types/react@18.3.11)(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-OuDXRipBO5mu+Nv4qN8cd8MiwiGBdq6h4PirVgPI9/ltbdcIzePgUJ0dJns26lflHSTRWW38I16wl4YTw3mNWA==}
/@invoke-ai/ui-library@0.0.43(@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.1.0)(@types/react@18.3.11)(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-t3fPYyks07ue3dEBPJuTHbeDLnDckDCOrtvc07mMDbLOnlPEZ0StaeiNGH+oO8qLzAuMAlSTdswgHfzTc2MmPw==}
peerDependencies:
'@fontsource-variable/inter': ^5.0.16
react: ^18.2.0
react-dom: ^18.2.0
dependencies:
'@chakra-ui/anatomy': 2.2.2
'@chakra-ui/anatomy': 2.3.4
'@chakra-ui/icons': 2.2.4(@chakra-ui/react@2.10.2)(react@18.3.1)
'@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1)
'@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1)
'@chakra-ui/react': 2.10.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1)
'@chakra-ui/styled-system': 2.9.2
'@chakra-ui/theme-tools': 2.1.2(@chakra-ui/styled-system@2.9.2)
'@chakra-ui/styled-system': 2.11.2(react@18.3.1)
'@chakra-ui/theme-tools': 2.2.6(@chakra-ui/styled-system@2.11.2)(react@18.3.1)
'@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1)
'@emotion/styled': 11.13.0(@emotion/react@11.13.3)(@types/react@18.3.11)(react@18.3.1)
'@fontsource-variable/inter': 5.1.0

View File

@@ -95,7 +95,8 @@
"none": "Keine",
"new": "Neu",
"ok": "OK",
"close": "Schließen"
"close": "Schließen",
"clipboard": "Zwischenablage"
},
"gallery": {
"galleryImageSize": "Bildgröße",
@@ -535,14 +536,12 @@
"addModels": "Model hinzufügen",
"deleteModelImage": "Lösche Model Bild",
"huggingFaceRepoID": "HuggingFace Repo ID",
"hfToken": "HuggingFace Schlüssel",
"huggingFacePlaceholder": "besitzer/model-name",
"modelSettings": "Modelleinstellungen",
"typePhraseHere": "Phrase hier eingeben",
"spandrelImageToImage": "Bild zu Bild (Spandrel)",
"starterModels": "Einstiegsmodelle",
"t5Encoder": "T5-Kodierer",
"useDefaultSettings": "Standardeinstellungen verwenden",
"uploadImage": "Bild hochladen",
"urlOrLocalPath": "URL oder lokaler Pfad",
"install": "Installieren",
@@ -678,10 +677,41 @@
"toast": {
"uploadFailed": "Hochladen fehlgeschlagen",
"imageCopied": "Bild kopiert",
"parametersNotSet": "Parameter nicht festgelegt",
"parametersNotSet": "Parameter nicht zurückgerufen",
"addedToBoard": "Dem Board hinzugefügt",
"loadedWithWarnings": "Workflow mit Warnungen geladen",
"imageSaved": "Bild gespeichert"
"imageSaved": "Bild gespeichert",
"linkCopied": "Link kopiert",
"problemCopyingLayer": "Ebene kann nicht kopiert werden",
"problemSavingLayer": "Ebene kann nicht gespeichert werden",
"parameterSetDesc": "{{parameter}} zurückgerufen",
"imageUploaded": "Bild hochgeladen",
"problemCopyingImage": "Bild kann nicht kopiert werden",
"parameterNotSetDesc": "{{parameter}} kann nicht zurückgerufen werden",
"prunedQueue": "Warteschlange bereinigt",
"modelAddedSimple": "Modell zur Warteschlange hinzugefügt",
"parametersSet": "Parameter zurückgerufen",
"imageNotLoadedDesc": "Bild konnte nicht gefunden werden",
"setControlImage": "Als Kontrollbild festlegen",
"sentToUpscale": "An Vergrößerung gesendet",
"parameterNotSetDescWithMessage": "{{parameter}} kann nicht zurückgerufen werden: {{message}}",
"unableToLoadImageMetadata": "Bildmetadaten können nicht geladen werden",
"unableToLoadImage": "Bild kann nicht geladen werden",
"serverError": "Serverfehler",
"parameterNotSet": "Parameter nicht zurückgerufen",
"sessionRef": "Sitzung: {{sessionId}}",
"problemDownloadingImage": "Bild kann nicht heruntergeladen werden",
"parameters": "Parameter",
"parameterSet": "Parameter zurückgerufen",
"importFailed": "Import fehlgeschlagen",
"importSuccessful": "Import erfolgreich",
"setNodeField": "Als Knotenfeld festlegen",
"somethingWentWrong": "Etwas ist schief gelaufen",
"workflowLoaded": "Arbeitsablauf geladen",
"workflowDeleted": "Arbeitsablauf gelöscht",
"errorCopied": "Fehler kopiert",
"layerCopiedToClipboard": "Ebene in die Zwischenablage kopiert",
"sentToCanvas": "An Leinwand gesendet"
},
"accessibility": {
"uploadImage": "Bild hochladen",
@@ -825,7 +855,6 @@
"width": "Breite",
"createdBy": "Erstellt von",
"steps": "Schritte",
"seamless": "Nahtlos",
"positivePrompt": "Positiver Prompt",
"generationMode": "Generierungsmodus",
"Threshold": "Rauschen-Schwelle",
@@ -1170,7 +1199,19 @@
"workflowVersion": "Version",
"saveToGallery": "In Galerie speichern",
"noWorkflows": "Keine Arbeitsabläufe",
"noMatchingWorkflows": "Keine passenden Arbeitsabläufe"
"noMatchingWorkflows": "Keine passenden Arbeitsabläufe",
"unknownErrorValidatingWorkflow": "Unbekannter Fehler beim Validieren des Arbeitsablaufes",
"inputFieldTypeParseError": "Typ des Eingabefelds {{node}}.{{field}} kann nicht analysiert werden ({{message}})",
"workflowSettings": "Arbeitsablauf Editor Einstellungen",
"unableToLoadWorkflow": "Arbeitsablauf kann nicht geladen werden",
"viewMode": "In linearen Ansicht verwenden",
"unableToValidateWorkflow": "Arbeitsablauf kann nicht validiert werden",
"outputFieldTypeParseError": "Typ des Ausgabefelds {{node}}.{{field}} kann nicht analysiert werden ({{message}})",
"unableToGetWorkflowVersion": "Version des Arbeitsablaufschemas kann nicht bestimmt werden",
"unknownFieldType": "$t(nodes.unknownField) Typ: {{type}}",
"unknownField": "Unbekanntes Feld",
"unableToUpdateNodes_one": "{{count}} Knoten kann nicht aktualisiert werden",
"unableToUpdateNodes_other": "{{count}} Knoten können nicht aktualisiert werden"
},
"hrf": {
"enableHrf": "Korrektur für hohe Auflösungen",
@@ -1300,15 +1341,7 @@
"enableLogging": "Protokollierung aktivieren"
},
"whatsNew": {
"whatsNewInInvoke": "Was gibt's Neues",
"canvasV2Announcement": {
"fluxSupport": "Unterstützung für Flux-Modelle",
"newCanvas": "Eine leistungsstarke neue Kontrollfläche",
"newLayerTypes": "Neue Ebenentypen für noch mehr Kontrolle",
"readReleaseNotes": "Anmerkungen zu dieser Version lesen",
"watchReleaseVideo": "Video über diese Version anzeigen",
"watchUiUpdatesOverview": "Interface-Updates Übersicht"
}
"whatsNewInInvoke": "Was gibt's Neues"
},
"stylePresets": {
"name": "Name",

View File

@@ -94,6 +94,7 @@
"close": "Close",
"copy": "Copy",
"copyError": "$t(gallery.copy) Error",
"clipboard": "Clipboard",
"on": "On",
"off": "Off",
"or": "or",
@@ -681,7 +682,8 @@
"recallParameters": "Recall Parameters",
"recallParameter": "Recall {{label}}",
"scheduler": "Scheduler",
"seamless": "Seamless",
"seamlessXAxis": "Seamless X Axis",
"seamlessYAxis": "Seamless Y Axis",
"seed": "Seed",
"steps": "Steps",
"strength": "Image to image strength",
@@ -712,8 +714,12 @@
"convertToDiffusersHelpText4": "This is a one time process only. It might take around 30s-60s depending on the specifications of your computer.",
"convertToDiffusersHelpText5": "Please make sure you have enough disk space. Models generally vary between 2GB-7GB in size.",
"convertToDiffusersHelpText6": "Do you wish to convert this model?",
"noDefaultSettings": "No default settings configured for this model. Visit the Model Manager to add default settings.",
"defaultSettings": "Default Settings",
"defaultSettingsSaved": "Default Settings Saved",
"defaultSettingsOutOfSync": "Some settings do not match the model's defaults:",
"restoreDefaultSettings": "Click to use the model's default settings.",
"usingDefaultSettings": "Using model's default settings",
"delete": "Delete",
"deleteConfig": "Delete Config",
"deleteModel": "Delete Model",
@@ -727,7 +733,17 @@
"huggingFacePlaceholder": "owner/model-name",
"huggingFaceRepoID": "HuggingFace Repo ID",
"huggingFaceHelper": "If multiple models are found in this repo, you will be prompted to select one to install.",
"hfToken": "HuggingFace Token",
"hfTokenLabel": "HuggingFace Token (Required for some models)",
"hfTokenHelperText": "A HF token is required to use some models. Click here to create or get your token.",
"hfTokenInvalid": "Invalid or Missing HF Token",
"hfForbidden": "You do not have access to this HF model",
"hfForbiddenErrorMessage": "We recommend visiting the repo page on HuggingFace.com. The owner may require acceptance of terms in order to download.",
"hfTokenInvalidErrorMessage": "Invalid or missing HuggingFace token.",
"hfTokenRequired": "You are trying to download a model that requires a valid HuggingFace Token.",
"hfTokenInvalidErrorMessage2": "Update it in the ",
"hfTokenUnableToVerify": "Unable to Verify HF Token",
"hfTokenUnableToVerifyErrorMessage": "Unable to verify HuggingFace token. This is likely due to a network error. Please try again later.",
"hfTokenSaved": "HF Token Saved",
"imageEncoderModelId": "Image Encoder Model ID",
"includesNModels": "Includes {{n}} models and their dependencies",
"installQueue": "Install Queue",
@@ -798,7 +814,6 @@
"uploadImage": "Upload Image",
"urlOrLocalPath": "URL or Local Path",
"urlOrLocalPathHelper": "URLs should point to a single file. Local paths can point to a single file or folder for a single diffusers model.",
"useDefaultSettings": "Use Default Settings",
"vae": "VAE",
"vaePrecision": "VAE Precision",
"variant": "Variant",
@@ -1032,6 +1047,7 @@
"patchmatchDownScaleSize": "Downscale",
"perlinNoise": "Perlin Noise",
"positivePromptPlaceholder": "Positive Prompt",
"recallMetadata": "Recall Metadata",
"iterations": "Iterations",
"scale": "Scale",
"scaleBeforeProcessing": "Scale Before Processing",
@@ -1108,6 +1124,9 @@
"enableInformationalPopovers": "Enable Informational Popovers",
"informationalPopoversDisabled": "Informational Popovers Disabled",
"informationalPopoversDisabledDesc": "Informational popovers have been disabled. Enable them in Settings.",
"enableModelDescriptions": "Enable Model Descriptions in Dropdowns",
"modelDescriptionsDisabled": "Model Descriptions in Dropdowns Disabled",
"modelDescriptionsDisabledDesc": "Model descriptions in dropdowns have been disabled. Enable them in Settings.",
"enableInvisibleWatermark": "Enable Invisible Watermark",
"enableNSFWChecker": "Enable NSFW Checker",
"general": "General",
@@ -1251,6 +1270,33 @@
"heading": "Mask Adjustments",
"paragraphs": ["Adjust the mask."]
},
"inpainting": {
"heading": "Inpainting",
"paragraphs": ["Controls which area is modified, guided by Denoising Strength."]
},
"rasterLayer": {
"heading": "Raster Layer",
"paragraphs": ["Pixel-based content of your canvas, used during image generation."]
},
"regionalGuidance": {
"heading": "Regional Guidance",
"paragraphs": ["Brush to guide where elements from global prompts should appear."]
},
"regionalGuidanceAndReferenceImage": {
"heading": "Regional Guidance and Regional Reference Image",
"paragraphs": [
"For Regional Guidance, brush to guide where elements from global prompts should appear.",
"For Regional Reference Image, brush to apply a reference image to specific areas."
]
},
"globalReferenceImage": {
"heading": "Global Reference Image",
"paragraphs": ["Applies a reference image to influence the entire generation."]
},
"regionalReferenceImage": {
"heading": "Regional Reference Image",
"paragraphs": ["Brush to apply a reference image to specific areas."]
},
"controlNet": {
"heading": "ControlNet",
"paragraphs": [
@@ -1606,14 +1652,16 @@
"newControlLayerError": "Problem Creating Control Layer",
"newRasterLayerOk": "Created Raster Layer",
"newRasterLayerError": "Problem Creating Raster Layer",
"newFromImage": "New from Image",
"pullBboxIntoLayerOk": "Bbox Pulled Into Layer",
"pullBboxIntoLayerError": "Problem Pulling BBox Into Layer",
"pullBboxIntoReferenceImageOk": "Bbox Pulled Into ReferenceImage",
"pullBboxIntoReferenceImageError": "Problem Pulling BBox Into ReferenceImage",
"regionIsEmpty": "Selected region is empty",
"mergeVisible": "Merge Visible",
"mergeVisibleOk": "Merged visible layers",
"mergeVisibleError": "Error merging visible layers",
"mergeDown": "Merge Down",
"mergeVisibleOk": "Merged layers",
"mergeVisibleError": "Error merging layers",
"clearHistory": "Clear History",
"bboxOverlay": "Show Bbox Overlay",
"resetCanvas": "Reset Canvas",
@@ -1648,6 +1696,8 @@
"controlLayer": "Control Layer",
"inpaintMask": "Inpaint Mask",
"regionalGuidance": "Regional Guidance",
"canvasAsRasterLayer": "$t(controlLayers.canvas) as $t(controlLayers.rasterLayer)",
"canvasAsControlLayer": "$t(controlLayers.canvas) as $t(controlLayers.controlLayer)",
"referenceImage": "Reference Image",
"regionalReferenceImage": "Regional Reference Image",
"globalReferenceImage": "Global Reference Image",
@@ -1688,8 +1738,18 @@
"layer_other": "Layers",
"layer_withCount_one": "Layer ({{count}})",
"layer_withCount_other": "Layers ({{count}})",
"convertToControlLayer": "Convert to Control Layer",
"convertToRasterLayer": "Convert to Raster Layer",
"convertRasterLayerTo": "Convert $t(controlLayers.rasterLayer) To",
"convertControlLayerTo": "Convert $t(controlLayers.controlLayer) To",
"convertInpaintMaskTo": "Convert $t(controlLayers.inpaintMask) To",
"convertRegionalGuidanceTo": "Convert $t(controlLayers.regionalGuidance) To",
"copyRasterLayerTo": "Copy $t(controlLayers.rasterLayer) To",
"copyControlLayerTo": "Copy $t(controlLayers.controlLayer) To",
"copyInpaintMaskTo": "Copy $t(controlLayers.inpaintMask) To",
"copyRegionalGuidanceTo": "Copy $t(controlLayers.regionalGuidance) To",
"newRasterLayer": "New $t(controlLayers.rasterLayer)",
"newControlLayer": "New $t(controlLayers.controlLayer)",
"newInpaintMask": "New $t(controlLayers.inpaintMask)",
"newRegionalGuidance": "New $t(controlLayers.regionalGuidance)",
"transparency": "Transparency",
"enableTransparencyEffect": "Enable Transparency Effect",
"disableTransparencyEffect": "Disable Transparency Effect",
@@ -1713,6 +1773,7 @@
"newGallerySessionDesc": "This will clear the canvas and all settings except for your model selection. Generations will be sent to the gallery.",
"newCanvasSession": "New Canvas Session",
"newCanvasSessionDesc": "This will clear the canvas and all settings except for your model selection. Generations will be staged on the canvas.",
"replaceCurrent": "Replace Current",
"controlMode": {
"controlMode": "Control Mode",
"balanced": "Balanced",
@@ -1842,16 +1903,24 @@
"apply": "Apply",
"cancel": "Cancel"
},
"segment": {
"autoMask": "Auto Mask",
"selectObject": {
"selectObject": "Select Object",
"pointType": "Point Type",
"foreground": "Foreground",
"background": "Background",
"invertSelection": "Invert Selection",
"include": "Include",
"exclude": "Exclude",
"neutral": "Neutral",
"reset": "Reset",
"apply": "Apply",
"reset": "Reset",
"saveAs": "Save As",
"cancel": "Cancel",
"process": "Process"
"process": "Process",
"help1": "Select a single target object. Add <Bold>Include</Bold> and <Bold>Exclude</Bold> points to indicate which parts of the layer are part of the target object.",
"help2": "Start with one <Bold>Include</Bold> point within the target object. Add more points to refine the selection. Fewer points typically produce better results.",
"help3": "Invert the selection to select everything except the target object.",
"clickToAdd": "Click on the layer to add a point",
"dragToMove": "Drag a point to move it",
"clickToRemove": "Click on a point to remove it"
},
"settings": {
"snapToGrid": {
@@ -1892,6 +1961,8 @@
"newRegionalReferenceImage": "New Regional Reference Image",
"newControlLayer": "New Control Layer",
"newRasterLayer": "New Raster Layer",
"newInpaintMask": "New Inpaint Mask",
"newRegionalGuidance": "New Regional Guidance",
"cropCanvasToBbox": "Crop Canvas to Bbox"
},
"stagingArea": {
@@ -2024,13 +2095,11 @@
},
"whatsNew": {
"whatsNewInInvoke": "What's New in Invoke",
"canvasV2Announcement": {
"newCanvas": "A powerful new control canvas",
"newLayerTypes": "New layer types for even more control",
"fluxSupport": "Support for the Flux family of models",
"readReleaseNotes": "Read Release Notes",
"watchReleaseVideo": "Watch Release Video",
"watchUiUpdatesOverview": "Watch UI Updates Overview"
}
"line1": "<ItalicComponent>Select Object</ItalicComponent> tool for precise object selection and editing",
"line2": "Expanded Flux support, now with Global Reference Images",
"line3": "Improved tooltips and context menus",
"readReleaseNotes": "Read Release Notes",
"watchRecentReleaseVideos": "Watch Recent Release Videos",
"watchUiUpdatesOverview": "Watch UI Updates Overview"
}
}

View File

@@ -5,7 +5,7 @@
"reportBugLabel": "Signaler un bug",
"settingsLabel": "Paramètres",
"img2img": "Image vers Image",
"nodes": "Processus",
"nodes": "Workflows",
"upload": "Importer",
"load": "Charger",
"back": "Retour",
@@ -95,7 +95,8 @@
"positivePrompt": "Prompt Positif",
"negativePrompt": "Prompt Négatif",
"ok": "Ok",
"close": "Fermer"
"close": "Fermer",
"clipboard": "Presse-papier"
},
"gallery": {
"galleryImageSize": "Taille de l'image",
@@ -161,7 +162,7 @@
"unstarImage": "Retirer le marquage de l'Image",
"viewerImage": "Visualisation de l'Image",
"imagesSettings": "Paramètres des images de la galerie",
"assetsTab": "Fichiers que vous avez importé pour vos projets.",
"assetsTab": "Fichiers que vous avez importés pour vos projets.",
"imagesTab": "Images que vous avez créées et enregistrées dans Invoke.",
"boardsSettings": "Paramètres des planches"
},
@@ -219,7 +220,6 @@
"typePhraseHere": "Écrire une phrase ici",
"cancel": "Annuler",
"defaultSettingsSaved": "Paramètres par défaut enregistrés",
"hfToken": "Token HuggingFace",
"imageEncoderModelId": "ID du modèle d'encodeur d'image",
"path": "Chemin sur le disque",
"repoVariant": "Variante de dépôt",
@@ -254,7 +254,6 @@
"loraModels": "LoRAs",
"main": "Principal",
"urlOrLocalPathHelper": "Les URL doivent pointer vers un seul fichier. Les chemins locaux peuvent pointer vers un seul fichier ou un dossier pour un seul modèle de diffuseurs.",
"useDefaultSettings": "Utiliser les paramètres par défaut",
"modelImageUpdateFailed": "Mise à jour de l'image du modèle échouée",
"loraTriggerPhrases": "Phrases de déclenchement LoRA",
"mainModelTriggerPhrases": "Phrases de déclenchement du modèle principal",
@@ -284,24 +283,28 @@
"skippingXDuplicates_many": ", en ignorant {{count}} doublons",
"skippingXDuplicates_other": ", en ignorant {{count}} doublons",
"installingModel": "Modèle en cours d'installation",
"installingBundle": "Pack en cours d'installation"
"installingBundle": "Pack en cours d'installation",
"noDefaultSettings": "Aucun paramètre par défaut configuré pour ce modèle. Visitez le Gestionnaire de Modèles pour ajouter des paramètres par défaut.",
"usingDefaultSettings": "Utilisation des paramètres par défaut du modèle",
"defaultSettingsOutOfSync": "Certain paramètres ne correspondent pas aux valeurs par défaut du modèle :",
"restoreDefaultSettings": "Cliquez pour utiliser les paramètres par défaut du modèle."
},
"parameters": {
"images": "Images",
"steps": "Etapes",
"cfgScale": "CFG Echelle",
"steps": "Étapes",
"cfgScale": "Échelle CFG",
"width": "Largeur",
"height": "Hauteur",
"seed": "Graine",
"shuffle": "Mélanger la graine",
"shuffle": "Nouvelle graine",
"noiseThreshold": "Seuil de Bruit",
"perlinNoise": "Bruit de Perlin",
"type": "Type",
"strength": "Force",
"upscaling": "Agrandissement",
"scale": "Echelle",
"scale": "Échelle",
"imageFit": "Ajuster Image Initiale à la Taille de Sortie",
"scaleBeforeProcessing": "Echelle Avant Traitement",
"scaleBeforeProcessing": "Échelle Avant Traitement",
"scaledWidth": "Larg. Échelle",
"scaledHeight": "Haut. Échelle",
"infillMethod": "Méthode de Remplissage",
@@ -422,7 +425,10 @@
"clearIntermediatesWithCount_other": "Effacé {{count}} Intermédiaires",
"informationalPopoversDisabled": "Pop-ups d'information désactivés",
"informationalPopoversDisabledDesc": "Les pop-ups d'information ont été désactivés. Activez-les dans les paramètres.",
"confirmOnNewSession": "Confirmer lors d'une nouvelle session"
"confirmOnNewSession": "Confirmer lors d'une nouvelle session",
"modelDescriptionsDisabledDesc": "Les descriptions des modèles dans les menus déroulants ont été désactivées. Activez-les dans les paramètres.",
"enableModelDescriptions": "Activer les descriptions de modèle dans les menus déroulants",
"modelDescriptionsDisabled": "Descriptions de modèle dans les menus déroulants désactivés"
},
"toast": {
"uploadFailed": "Importation échouée",
@@ -435,22 +441,22 @@
"parameterNotSet": "Paramètre non Rappelé",
"canceled": "Traitement annulé",
"addedToBoard": "Ajouté aux ressources de la planche {{name}}",
"workflowLoaded": "Processus chargé",
"workflowLoaded": "Workflow chargé",
"connected": "Connecté au serveur",
"setNodeField": "Définir comme champ de nœud",
"imageUploadFailed": "Échec de l'importation de l'image",
"loadedWithWarnings": "Processus chargé avec des avertissements",
"loadedWithWarnings": "Workflow chargé avec des avertissements",
"imageUploaded": "Image importée",
"modelAddedSimple": "Modèle ajouté à la file d'attente",
"setControlImage": "Définir comme image de contrôle",
"workflowDeleted": "Processus supprimé",
"workflowDeleted": "Workflow supprimé",
"baseModelChangedCleared_one": "Effacé ou désactivé {{count}} sous-modèle incompatible",
"baseModelChangedCleared_many": "Effacé ou désactivé {{count}} sous-modèles incompatibles",
"baseModelChangedCleared_other": "Effacé ou désactivé {{count}} sous-modèles incompatibles",
"invalidUpload": "Importation invalide",
"problemDownloadingImage": "Impossible de télécharger l'image",
"problemRetrievingWorkflow": "Problème de récupération du processus",
"problemDeletingWorkflow": "Problème de suppression du processus",
"problemRetrievingWorkflow": "Problème de récupération du Workflow",
"problemDeletingWorkflow": "Problème de suppression du Workflow",
"prunedQueue": "File d'attente vidée",
"parameters": "Paramètres",
"modelImportCanceled": "Importation du modèle annulée",
@@ -550,7 +556,7 @@
"accordions": {
"advanced": {
"title": "Avancé",
"options": "$t(accordions.advanced.title) Options"
"options": "Options $t(accordions.advanced.title)"
},
"image": {
"title": "Image"
@@ -631,7 +637,7 @@
"graphQueued": "Graph ajouté à la file d'attente",
"other": "Autre",
"generation": "Génération",
"workflows": "Processus",
"workflows": "Workflows",
"batchFailedToQueue": "Impossible d'ajouter le Lot dans à la file d'attente",
"graphFailedToQueue": "Impossible d'ajouter le graph à la file d'attente",
"item": "Élément",
@@ -704,8 +710,8 @@
"desc": "Rappelle toutes les métadonnées pour l'image actuelle."
},
"loadWorkflow": {
"title": "Charger le processus",
"desc": "Charge le processus enregistré de l'image actuelle (s'il en a un)."
"title": "Ouvrir un Workflow",
"desc": "Charge le workflow enregistré lié à l'image actuelle (s'il en a un)."
},
"recallSeed": {
"desc": "Rappelle la graine pour l'image actuelle.",
@@ -756,8 +762,8 @@
"desc": "Séléctionne l'onglet Agrandissement."
},
"selectWorkflowsTab": {
"desc": "Sélectionne l'onglet Processus.",
"title": "Sélectionner l'onglet Processus"
"desc": "Sélectionne l'onglet Workflows.",
"title": "Sélectionner l'onglet Workflows"
},
"togglePanels": {
"desc": "Affiche ou masque les panneaux gauche et droit en même temps.",
@@ -963,11 +969,11 @@
},
"undo": {
"title": "Annuler",
"desc": "Annule la dernière action de processus."
"desc": "Annule la dernière action de workflow."
},
"redo": {
"title": "Rétablir",
"desc": "Rétablit la dernière action de processus."
"desc": "Rétablit la dernière action de workflow."
},
"addNode": {
"desc": "Ouvre le menu d'ajout de nœud.",
@@ -985,7 +991,7 @@
"desc": "Colle les nœuds et les connections copiés.",
"title": "Coller"
},
"title": "Processus"
"title": "Workflows"
}
},
"popovers": {
@@ -1372,6 +1378,43 @@
"Des valeurs de guidage élevées peuvent entraîner une saturation excessive, et un guidage élevé ou faible peut entraîner des résultats de génération déformés. Le guidage ne s'applique qu'aux modèles FLUX DEV."
],
"heading": "Guidage"
},
"globalReferenceImage": {
"heading": "Image de Référence Globale",
"paragraphs": [
"Applique une image de référence pour influencer l'ensemble de la génération."
]
},
"regionalReferenceImage": {
"heading": "Image de Référence Régionale",
"paragraphs": [
"Pinceau pour appliquer une image de référence à des zones spécifiques."
]
},
"inpainting": {
"heading": "Inpainting",
"paragraphs": [
"Contrôle la zone qui est modifiée, guidé par la force de débruitage."
]
},
"regionalGuidance": {
"heading": "Guide Régional",
"paragraphs": [
"Pinceau pour guider l'emplacement des éléments provenant des prompts globaux."
]
},
"regionalGuidanceAndReferenceImage": {
"heading": "Guide régional et image de référence régionale",
"paragraphs": [
"Pour le Guide Régional, utilisez le pinceau pour indiquer où les éléments des prompts globaux doivent apparaître.",
"Pour l'image de référence régionale, pinceau pour appliquer une image de référence à des zones spécifiques."
]
},
"rasterLayer": {
"heading": "Couche Rastérisation",
"paragraphs": [
"Contenu basé sur les pixels de votre toile, utilisé lors de la génération d'images."
]
}
},
"dynamicPrompts": {
@@ -1392,12 +1435,11 @@
"positivePrompt": "Prompt Positif",
"allPrompts": "Tous les Prompts",
"negativePrompt": "Prompt Négatif",
"seamless": "Sans jointure",
"metadata": "Métadonné",
"scheduler": "Planificateur",
"imageDetails": "Détails de l'Image",
"seed": "Graine",
"workflow": "Processus",
"workflow": "Workflow",
"width": "Largeur",
"Threshold": "Seuil de bruit",
"noMetaData": "Aucune métadonnée trouvée",
@@ -1446,8 +1488,8 @@
"hideMinimapnodes": "Masquer MiniCarte",
"zoomOutNodes": "Dézoomer",
"zoomInNodes": "Zoomer",
"downloadWorkflow": "Télécharger processus en JSON",
"loadWorkflow": "Charger le processus",
"downloadWorkflow": "Exporter le Workflow au format JSON",
"loadWorkflow": "Charger un Workflow",
"reloadNodeTemplates": "Recharger les modèles de nœuds",
"animatedEdges": "Connexions animées",
"cannotConnectToSelf": "Impossible de se connecter à soi-même",
@@ -1470,16 +1512,16 @@
"float": "Flottant",
"mismatchedVersion": "Nœud invalide : le nœud {{node}} de type {{type}} a une version incompatible (essayez de mettre à jour?)",
"missingTemplate": "Nœud invalide : le nœud {{node}} de type {{type}} modèle manquant (non installé?)",
"noWorkflow": "Pas de processus",
"noWorkflow": "Pas de Workflow",
"validateConnectionsHelp": "Prévenir la création de connexions invalides et l'invocation de graphes invalides",
"workflowSettings": "Paramètres de l'Éditeur de Processus",
"workflowValidation": "Erreur de validation du processus",
"workflowSettings": "Paramètres de l'Éditeur de Workflow",
"workflowValidation": "Erreur de validation du Workflow",
"executionStateInProgress": "En cours",
"node": "Noeud",
"scheduler": "Planificateur",
"notes": "Notes",
"notesDescription": "Ajouter des notes sur votre processus",
"unableToLoadWorkflow": "Impossible de charger le processus",
"notesDescription": "Ajouter des notes sur votre workflow",
"unableToLoadWorkflow": "Impossible de charger le Workflow",
"addNode": "Ajouter un nœud",
"problemSettingTitle": "Problème lors de définition du Titre",
"connectionWouldCreateCycle": "La connexion créerait un cycle",
@@ -1502,7 +1544,7 @@
"noOutputRecorded": "Aucun résultat enregistré",
"removeLinearView": "Retirer de la vue linéaire",
"snapToGrid": "Aligner sur la grille",
"workflow": "Processus",
"workflow": "Workflow",
"updateApp": "Mettre à jour l'application",
"updateNode": "Mettre à jour le nœud",
"nodeOutputs": "Sorties de nœud",
@@ -1515,7 +1557,7 @@
"string": "Chaîne de caractères",
"workflowName": "Nom",
"snapToGridHelp": "Aligner les nœuds sur la grille lors du déplacement",
"unableToValidateWorkflow": "Impossible de valider le processus",
"unableToValidateWorkflow": "Impossible de valider le Workflow",
"validateConnections": "Valider les connexions et le graphique",
"unableToUpdateNodes_one": "Impossible de mettre à jour {{count}} nœud",
"unableToUpdateNodes_many": "Impossible de mettre à jour {{count}} nœuds",
@@ -1528,15 +1570,15 @@
"nodePack": "Paquet de nœuds",
"sourceNodeDoesNotExist": "Connexion invalide : le nœud source/de sortie {{node}} n'existe pas",
"sourceNodeFieldDoesNotExist": "Connexion invalide : {{node}}.{{field}} n'existe pas",
"unableToGetWorkflowVersion": "Impossible d'obtenir la version du schéma de processus",
"newWorkflowDesc2": "Votre processus actuel comporte des modifications non enregistrées.",
"unableToGetWorkflowVersion": "Impossible d'obtenir la version du schéma du Workflow",
"newWorkflowDesc2": "Votre workflow actuel comporte des modifications non enregistrées.",
"deletedInvalidEdge": "Connexion invalide supprimé {{source}} -> {{target}}",
"targetNodeDoesNotExist": "Connexion invalide : le nœud cible/entrée {{node}} n'existe pas",
"targetNodeFieldDoesNotExist": "Connexion invalide : le champ {{node}}.{{field}} n'existe pas",
"nodeVersion": "Version du noeud",
"clearWorkflowDesc2": "Votre processus actuel comporte des modifications non enregistrées.",
"clearWorkflow": "Effacer le Processus",
"clearWorkflowDesc": "Effacer ce processus et en commencer un nouveau?",
"clearWorkflowDesc2": "Votre workflow actuel comporte des modifications non enregistrées.",
"clearWorkflow": "Effacer le Workflow",
"clearWorkflowDesc": "Effacer ce workflow et en commencer un nouveau?",
"unsupportedArrayItemType": "type d'élément de tableau non pris en charge \"{{type}}\"",
"addLinearView": "Ajouter à la vue linéaire",
"collectionOrScalarFieldType": "{{name}} (Unique ou Collection)",
@@ -1545,7 +1587,7 @@
"ipAdapter": "IP-Adapter",
"viewMode": "Utiliser en vue linéaire",
"collectionFieldType": "{{name}} (Collection)",
"newWorkflow": "Nouveau processus",
"newWorkflow": "Nouveau Workflow",
"reorderLinearView": "Réorganiser la vue linéaire",
"unknownOutput": "Sortie inconnue : {{name}}",
"outputFieldTypeParseError": "Impossible d'analyser le type du champ de sortie {{node}}.{{field}} ({{message}})",
@@ -1555,13 +1597,13 @@
"unknownFieldType": "$t(nodes.unknownField) type : {{type}}",
"inputFieldTypeParseError": "Impossible d'analyser le type du champ d'entrée {{node}}.{{field}} ({{message}})",
"unableToExtractSchemaNameFromRef": "impossible d'extraire le nom du schéma à partir de la référence",
"editMode": "Modifier dans l'éditeur de processus",
"unknownErrorValidatingWorkflow": "Erreur inconnue lors de la validation du processus",
"editMode": "Modifier dans l'éditeur de Workflow",
"unknownErrorValidatingWorkflow": "Erreur inconnue lors de la validation du Workflow",
"updateAllNodes": "Mettre à jour les nœuds",
"allNodesUpdated": "Tous les nœuds mis à jour",
"newWorkflowDesc": "Créer un nouveau processus?",
"newWorkflowDesc": "Créer un nouveau workflow?",
"edit": "Modifier",
"noFieldsViewMode": "Ce processus n'a aucun champ sélectionné à afficher. Consultez le processus complet pour configurer les valeurs.",
"noFieldsViewMode": "Ce workflow n'a aucun champ sélectionné à afficher. Consultez le workflow complet pour configurer les valeurs.",
"graph": "Graph",
"modelAccessError": "Impossible de trouver le modèle {{key}}, réinitialisation aux paramètres par défaut",
"showEdgeLabelsHelp": "Afficher le nom sur les connections, indiquant les nœuds connectés",
@@ -1575,9 +1617,9 @@
"missingInvocationTemplate": "Modèle d'invocation manquant",
"imageAccessError": "Impossible de trouver l'image {{image_name}}, réinitialisation à la valeur par défaut",
"boardAccessError": "Impossible de trouver la planche {{board_id}}, réinitialisation à la valeur par défaut",
"workflowHelpText": "Besoin d'aide? Consultez notre guide sur <LinkComponent>Comment commencer avec les Processus</LinkComponent>.",
"noWorkflows": "Aucun Processus",
"noMatchingWorkflows": "Aucun processus correspondant"
"workflowHelpText": "Besoin d'aide? Consultez notre guide sur <LinkComponent>Comment commencer avec les Workflows</LinkComponent>.",
"noWorkflows": "Aucun Workflows",
"noMatchingWorkflows": "Aucun Workflows correspondant"
},
"models": {
"noMatchingModels": "Aucun modèle correspondant",
@@ -1594,59 +1636,51 @@
},
"workflows": {
"workflowLibrary": "Bibliothèque",
"loading": "Chargement des processus",
"searchWorkflows": "Rechercher des processus",
"workflowCleared": "Processus effacé",
"loading": "Chargement des Workflows",
"searchWorkflows": "Chercher des Workflows",
"workflowCleared": "Workflow effacé",
"noDescription": "Aucune description",
"deleteWorkflow": "Supprimer le processus",
"openWorkflow": "Ouvrir le processus",
"deleteWorkflow": "Supprimer le Workflow",
"openWorkflow": "Ouvrir le Workflow",
"uploadWorkflow": "Charger à partir d'un fichier",
"workflowName": "Nom du processus",
"unnamedWorkflow": "Processus sans nom",
"saveWorkflowAs": "Enregistrer le processus sous",
"workflows": "Processus",
"savingWorkflow": "Enregistrement du processus...",
"saveWorkflowToProject": "Enregistrer le processus dans le projet",
"workflowName": "Nom du Workflow",
"unnamedWorkflow": "Workflow sans nom",
"saveWorkflowAs": "Enregistrer le Workflow sous",
"workflows": "Workflows",
"savingWorkflow": "Enregistrement du Workflow...",
"saveWorkflowToProject": "Enregistrer le Workflow dans le projet",
"downloadWorkflow": "Enregistrer dans le fichier",
"saveWorkflow": "Enregistrer le processus",
"problemSavingWorkflow": "Problème de sauvegarde du processus",
"workflowEditorMenu": "Menu de l'Éditeur de Processus",
"newWorkflowCreated": "Nouveau processus créé",
"clearWorkflowSearchFilter": "Réinitialiser le filtre de recherche de processus",
"problemLoading": "Problème de chargement des processus",
"workflowSaved": "Processus enregistré",
"noWorkflows": "Pas de processus",
"saveWorkflow": "Enregistrer le Workflow",
"problemSavingWorkflow": "Problème de sauvegarde du Workflow",
"workflowEditorMenu": "Menu de l'Éditeur de Workflow",
"newWorkflowCreated": "Nouveau Workflow créé",
"clearWorkflowSearchFilter": "Réinitialiser le filtre de recherche de Workflow",
"problemLoading": "Problème de chargement des Workflows",
"workflowSaved": "Workflow enregistré",
"noWorkflows": "Pas de Workflows",
"ascending": "Ascendant",
"loadFromGraph": "Charger le processus à partir du graphique",
"loadFromGraph": "Charger le Workflow à partir du graphique",
"descending": "Descendant",
"created": "Créé",
"updated": "Mis à jour",
"loadWorkflow": "$t(common.load) Processus",
"loadWorkflow": "$t(common.load) Workflow",
"convertGraph": "Convertir le graphique",
"opened": "Ouvert",
"name": "Nom",
"autoLayout": "Mise en page automatique",
"defaultWorkflows": "Processus par défaut",
"userWorkflows": "Processus utilisateur",
"projectWorkflows": "Processus du projet",
"defaultWorkflows": "Workflows par défaut",
"userWorkflows": "Workflows de l'utilisateur",
"projectWorkflows": "Workflows du projet",
"copyShareLink": "Copier le lien de partage",
"chooseWorkflowFromLibrary": "Choisir le Processus dans la Bibliothèque",
"chooseWorkflowFromLibrary": "Choisir le Workflow dans la Bibliothèque",
"uploadAndSaveWorkflow": "Importer dans la bibliothèque",
"edit": "Modifer",
"deleteWorkflow2": "Êtes-vous sûr de vouloir supprimer ce processus? Ceci ne peut pas être annulé.",
"deleteWorkflow2": "Êtes-vous sûr de vouloir supprimer ce Workflow? Cette action ne peut pas être annulé.",
"download": "Télécharger",
"copyShareLinkForWorkflow": "Copier le lien de partage pour le processus",
"copyShareLinkForWorkflow": "Copier le lien de partage pour le Workflow",
"delete": "Supprimer"
},
"whatsNew": {
"canvasV2Announcement": {
"watchReleaseVideo": "Regarder la vidéo de lancement",
"newLayerTypes": "Nouveaux types de couches pour un contrôle encore plus précis",
"fluxSupport": "Support pour la famille de modèles Flux",
"readReleaseNotes": "Lire les notes de version",
"newCanvas": "Une nouvelle Toile de contrôle puissant",
"watchUiUpdatesOverview": "Regarder l'aperçu des mises à jour de l'UI"
},
"whatsNewInInvoke": "Quoi de neuf dans Invoke"
},
"ui": {
@@ -1657,7 +1691,7 @@
"gallery": "Galerie",
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)",
"generation": "Génération",
"workflows": "Processus",
"workflows": "Workflows",
"workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)",
"models": "Modèles",
"modelsTab": "$t(ui.tabs.models) $t(common.tab)"
@@ -1767,7 +1801,9 @@
"bboxGroup": "Créer à partir de la bounding box",
"newRegionalReferenceImage": "Nouvelle image de référence régionale",
"newGlobalReferenceImage": "Nouvelle image de référence globale",
"newControlLayer": "Nouveau couche de contrôle"
"newControlLayer": "Nouveau couche de contrôle",
"newInpaintMask": "Nouveau Masque Inpaint",
"newRegionalGuidance": "Nouveau Guide Régional"
},
"bookmark": "Marque-page pour Changement Rapide",
"saveLayerToAssets": "Enregistrer la couche dans les ressources",
@@ -1780,8 +1816,6 @@
"on": "Activé",
"label": "Aligner sur la grille"
},
"isolatedFilteringPreview": "Aperçu de filtrage isolé",
"isolatedTransformingPreview": "Aperçu de transformation isolée",
"invertBrushSizeScrollDirection": "Inverser le défilement pour la taille du pinceau",
"pressureSensitivity": "Sensibilité à la pression",
"preserveMask": {
@@ -1789,9 +1823,10 @@
"alert": "Préserver la zone masquée"
},
"isolatedPreview": "Aperçu Isolé",
"isolatedStagingPreview": "Aperçu de l'attente isolé"
"isolatedStagingPreview": "Aperçu de l'attente isolé",
"isolatedLayerPreview": "Aperçu de la couche isolée",
"isolatedLayerPreviewDesc": "Pour afficher uniquement cette couche lors de l'exécution d'opérations telles que le filtrage ou la transformation."
},
"convertToRasterLayer": "Convertir en Couche de Rastérisation",
"transparency": "Transparence",
"moveBackward": "Reculer",
"rectangle": "Rectangle",
@@ -1914,7 +1949,6 @@
"globalReferenceImage_withCount_one": "$t(controlLayers.globalReferenceImage)",
"globalReferenceImage_withCount_many": "Images de référence globales",
"globalReferenceImage_withCount_other": "Images de référence globales",
"convertToControlLayer": "Convertir en Couche de Contrôle",
"layer_withCount_one": "Couche {{count}}",
"layer_withCount_many": "Couches {{count}}",
"layer_withCount_other": "Couches {{count}}",
@@ -1977,7 +2011,41 @@
"pullBboxIntoReferenceImageOk": "Bounding Box insérée dans l'Image de référence",
"controlLayer_withCount_one": "$t(controlLayers.controlLayer)",
"controlLayer_withCount_many": "Controler les couches",
"controlLayer_withCount_other": "Controler les couches"
"controlLayer_withCount_other": "Controler les couches",
"copyInpaintMaskTo": "Copier $t(controlLayers.inpaintMask) vers",
"copyRegionalGuidanceTo": "Copier $t(controlLayers.regionalGuidance) vers",
"convertRasterLayerTo": "Convertir $t(controlLayers.rasterLayer) vers",
"selectObject": {
"selectObject": "Sélectionner l'objet",
"clickToAdd": "Cliquez sur la couche pour ajouter un point",
"apply": "Appliquer",
"cancel": "Annuler",
"dragToMove": "Faites glisser un point pour le déplacer",
"clickToRemove": "Cliquez sur un point pour le supprimer",
"include": "Inclure",
"invertSelection": "Sélection Inversée",
"saveAs": "Enregistrer sous",
"neutral": "Neutre",
"pointType": "Type de point",
"exclude": "Exclure",
"process": "Traiter",
"reset": "Réinitialiser",
"help1": "Sélectionnez un seul objet cible. Ajoutez des points <Bold>Inclure</Bold> et <Bold>Exclure</Bold> pour indiquer quelles parties de la couche font partie de l'objet cible.",
"help2": "Commencez par un point <Bold>Inclure</Bold> au sein de l'objet cible. Ajoutez d'autres points pour affiner la sélection. Moins de points produisent généralement de meilleurs résultats.",
"help3": "Inversez la sélection pour sélectionner tout sauf l'objet cible."
},
"canvasAsControlLayer": "$t(controlLayers.canvas) en tant que $t(controlLayers.controlLayer)",
"convertRegionalGuidanceTo": "Convertir $t(controlLayers.regionalGuidance) vers",
"copyRasterLayerTo": "Copier $t(controlLayers.rasterLayer) vers",
"newControlLayer": "Nouveau $t(controlLayers.controlLayer)",
"newRegionalGuidance": "Nouveau $t(controlLayers.regionalGuidance)",
"replaceCurrent": "Remplacer Actuel",
"convertControlLayerTo": "Convertir $t(controlLayers.controlLayer) vers",
"convertInpaintMaskTo": "Convertir $t(controlLayers.inpaintMask) vers",
"copyControlLayerTo": "Copier $t(controlLayers.controlLayer) vers",
"newInpaintMask": "Nouveau $t(controlLayers.inpaintMask)",
"newRasterLayer": "Nouveau $t(controlLayers.rasterLayer)",
"canvasAsRasterLayer": "$t(controlLayers.canvas) en tant que $t(controlLayers.rasterLayer)"
},
"upscaling": {
"exceedsMaxSizeDetails": "La limite maximale d'agrandissement est de {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixels. Veuillez essayer une image plus petite ou réduire votre sélection d'échelle.",
@@ -2048,7 +2116,7 @@
"config": "Configuration",
"canvas": "Toile",
"generation": "Génération",
"workflows": "Processus",
"workflows": "Workflows",
"system": "Système",
"models": "Modèles",
"logNamespaces": "Journalisation des espaces de noms",
@@ -2071,9 +2139,9 @@
"newUserExperience": {
"toGetStarted": "Pour commencer, saisissez un prompt dans la boîte et cliquez sur <StrongComponent>Invoke</StrongComponent> pour générer votre première image. Sélectionnez un template de prompt pour améliorer les résultats. Vous pouvez choisir de sauvegarder vos images directement dans la <StrongComponent>Galerie</StrongComponent> ou de les modifier sur la <StrongComponent>Toile</StrongComponent>.",
"gettingStartedSeries": "Vous souhaitez plus de conseils? Consultez notre <LinkComponent>Série de démarrage</LinkComponent> pour des astuces sur l'exploitation du plein potentiel de l'Invoke Studio.",
"noModelsInstalled": "Il semblerait qu'aucun modèle ne soit installé",
"noModelsInstalled": "Il semble qu'aucun modèle ne soit installé",
"downloadStarterModels": "Télécharger les modèles de démarrage",
"importModels": "Importer Modèles",
"importModels": "Importer des Modèles",
"toGetStartedLocal": "Pour commencer, assurez-vous de télécharger ou d'importer des modèles nécessaires pour exécuter Invoke. Ensuite, saisissez le prompt dans la boîte et cliquez sur <StrongComponent>Invoke</StrongComponent> pour générer votre première image. Sélectionnez un template de prompt pour améliorer les résultats. Vous pouvez choisir de sauvegarder vos images directement sur <StrongComponent>Galerie</StrongComponent> ou les modifier sur la <StrongComponent>Toile</StrongComponent>."
},
"upsell": {

View File

@@ -92,7 +92,9 @@
"none": "Niente",
"new": "Nuovo",
"view": "Vista",
"close": "Chiudi"
"close": "Chiudi",
"clipboard": "Appunti",
"ok": "Ok"
},
"gallery": {
"galleryImageSize": "Dimensione dell'immagine",
@@ -542,7 +544,6 @@
"defaultSettingsSaved": "Impostazioni predefinite salvate",
"defaultSettings": "Impostazioni predefinite",
"metadata": "Metadati",
"useDefaultSettings": "Usa le impostazioni predefinite",
"triggerPhrases": "Frasi Trigger",
"deleteModelImage": "Elimina l'immagine del modello",
"localOnly": "solo locale",
@@ -588,7 +589,15 @@
"installingXModels_many": "Installazione di {{count}} modelli",
"installingXModels_other": "Installazione di {{count}} modelli",
"includesNModels": "Include {{n}} modelli e le loro dipendenze",
"starterBundleHelpText": "Installa facilmente tutti i modelli necessari per iniziare con un modello base, tra cui un modello principale, controlnet, adattatori IP e altro. Selezionando un pacchetto salterai tutti i modelli che hai già installato."
"starterBundleHelpText": "Installa facilmente tutti i modelli necessari per iniziare con un modello base, tra cui un modello principale, controlnet, adattatori IP e altro. Selezionando un pacchetto salterai tutti i modelli che hai già installato.",
"noDefaultSettings": "Nessuna impostazione predefinita configurata per questo modello. Visita Gestione Modelli per aggiungere impostazioni predefinite.",
"defaultSettingsOutOfSync": "Alcune impostazioni non corrispondono a quelle predefinite del modello:",
"restoreDefaultSettings": "Fare clic per utilizzare le impostazioni predefinite del modello.",
"usingDefaultSettings": "Utilizzo delle impostazioni predefinite del modello",
"huggingFace": "HuggingFace",
"huggingFaceRepoID": "HuggingFace Repository ID",
"clipEmbed": "CLIP Embed",
"t5Encoder": "T5 Encoder"
},
"parameters": {
"images": "Immagini",
@@ -689,7 +698,8 @@
"boxBlur": "Sfocatura Box",
"staged": "Maschera espansa",
"optimizedImageToImage": "Immagine-a-immagine ottimizzata",
"sendToCanvas": "Invia alla Tela"
"sendToCanvas": "Invia alla Tela",
"coherenceMinDenoise": "Riduzione minima del rumore"
},
"settings": {
"models": "Modelli",
@@ -724,7 +734,10 @@
"reloadingIn": "Ricaricando in",
"informationalPopoversDisabled": "Testo informativo a comparsa disabilitato",
"informationalPopoversDisabledDesc": "I testi informativi a comparsa sono disabilitati. Attivali nelle impostazioni.",
"confirmOnNewSession": "Conferma su nuova sessione"
"confirmOnNewSession": "Conferma su nuova sessione",
"enableModelDescriptions": "Abilita le descrizioni dei modelli nei menu a discesa",
"modelDescriptionsDisabled": "Descrizioni dei modelli nei menu a discesa disabilitate",
"modelDescriptionsDisabledDesc": "Le descrizioni dei modelli nei menu a discesa sono state disabilitate. Abilitale nelle Impostazioni."
},
"toast": {
"uploadFailed": "Caricamento fallito",
@@ -1076,7 +1089,8 @@
"noLoRAsInstalled": "Nessun LoRA installato",
"addLora": "Aggiungi LoRA",
"defaultVAE": "VAE predefinito",
"concepts": "Concetti"
"concepts": "Concetti",
"lora": "LoRA"
},
"invocationCache": {
"disable": "Disabilita",
@@ -1133,7 +1147,8 @@
"paragraphs": [
"Scegli quanti livelli del modello CLIP saltare.",
"Alcuni modelli funzionano meglio con determinate impostazioni di CLIP Skip."
]
],
"heading": "CLIP Skip"
},
"compositingCoherencePass": {
"heading": "Passaggio di Coerenza",
@@ -1492,6 +1507,42 @@
"Controlla quanto il prompt influenza il processo di generazione.",
"Valori di guida elevati possono causare sovrasaturazione e una guida elevata o bassa può causare risultati di generazione distorti. La guida si applica solo ai modelli FLUX DEV."
]
},
"regionalReferenceImage": {
"paragraphs": [
"Pennello per applicare un'immagine di riferimento ad aree specifiche."
],
"heading": "Immagine di riferimento Regionale"
},
"rasterLayer": {
"paragraphs": [
"Contenuto basato sui pixel della tua tela, utilizzato durante la generazione dell'immagine."
],
"heading": "Livello Raster"
},
"regionalGuidance": {
"heading": "Guida Regionale",
"paragraphs": [
"Pennello per guidare la posizione in cui devono apparire gli elementi dei prompt globali."
]
},
"regionalGuidanceAndReferenceImage": {
"heading": "Guida regionale e immagine di riferimento regionale",
"paragraphs": [
"Per la Guida Regionale, utilizzare il pennello per indicare dove devono apparire gli elementi dei prompt globali.",
"Per l'immagine di riferimento regionale, utilizzare il pennello per applicare un'immagine di riferimento ad aree specifiche."
]
},
"globalReferenceImage": {
"heading": "Immagine di riferimento Globale",
"paragraphs": [
"Applica un'immagine di riferimento per influenzare l'intera generazione."
]
},
"inpainting": {
"paragraphs": [
"Controlla quale area viene modificata, in base all'intensità di riduzione del rumore."
]
}
},
"sdxl": {
@@ -1513,7 +1564,6 @@
"refinerSteps": "Passi Affinamento"
},
"metadata": {
"seamless": "Senza giunture",
"positivePrompt": "Prompt positivo",
"negativePrompt": "Prompt negativo",
"generationMode": "Modalità generazione",
@@ -1541,7 +1591,10 @@
"parsingFailed": "Analisi non riuscita",
"recallParameter": "Richiama {{label}}",
"canvasV2Metadata": "Tela",
"guidance": "Guida"
"guidance": "Guida",
"seamlessXAxis": "Asse X senza giunte",
"seamlessYAxis": "Asse Y senza giunte",
"vae": "VAE"
},
"hrf": {
"enableHrf": "Abilita Correzione Alta Risoluzione",
@@ -1638,11 +1691,11 @@
"regionalGuidance": "Guida regionale",
"opacity": "Opacità",
"mergeVisible": "Fondi il visibile",
"mergeVisibleOk": "Livelli visibili uniti",
"mergeVisibleOk": "Livelli uniti",
"deleteReferenceImage": "Elimina l'immagine di riferimento",
"referenceImage": "Immagine di riferimento",
"fitBboxToLayers": "Adatta il riquadro di delimitazione ai livelli",
"mergeVisibleError": "Errore durante l'unione dei livelli visibili",
"mergeVisibleError": "Errore durante l'unione dei livelli",
"regionalReferenceImage": "Immagine di riferimento Regionale",
"newLayerFromImage": "Nuovo livello da immagine",
"newCanvasFromImage": "Nuova tela da immagine",
@@ -1734,7 +1787,7 @@
"composition": "Solo Composizione",
"ipAdapterMethod": "Metodo Adattatore IP"
},
"showingType": "Mostrare {{type}}",
"showingType": "Mostra {{type}}",
"dynamicGrid": "Griglia dinamica",
"tool": {
"view": "Muovi",
@@ -1862,8 +1915,6 @@
"layer_withCount_one": "Livello ({{count}})",
"layer_withCount_many": "Livelli ({{count}})",
"layer_withCount_other": "Livelli ({{count}})",
"convertToControlLayer": "Converti in livello di controllo",
"convertToRasterLayer": "Converti in livello raster",
"unlocked": "Sbloccato",
"enableTransparencyEffect": "Abilita l'effetto trasparenza",
"replaceLayer": "Sostituisci livello",
@@ -1876,9 +1927,7 @@
"newCanvasSession": "Nuova sessione Tela",
"deleteSelected": "Elimina selezione",
"settings": {
"isolatedFilteringPreview": "Anteprima del filtraggio isolata",
"isolatedStagingPreview": "Anteprima di generazione isolata",
"isolatedTransformingPreview": "Anteprima di trasformazione isolata",
"isolatedPreview": "Anteprima isolata",
"invertBrushSizeScrollDirection": "Inverti scorrimento per dimensione pennello",
"snapToGrid": {
@@ -1890,7 +1939,9 @@
"preserveMask": {
"alert": "Preservare la regione mascherata",
"label": "Preserva la regione mascherata"
}
},
"isolatedLayerPreview": "Anteprima livello isolato",
"isolatedLayerPreviewDesc": "Se visualizzare solo questo livello quando si eseguono operazioni come il filtraggio o la trasformazione."
},
"transform": {
"reset": "Reimposta",
@@ -1935,9 +1986,46 @@
"canvasGroup": "Tela",
"newRasterLayer": "Nuovo Livello Raster",
"saveCanvasToGallery": "Salva la Tela nella Galleria",
"saveToGalleryGroup": "Salva nella Galleria"
"saveToGalleryGroup": "Salva nella Galleria",
"newInpaintMask": "Nuova maschera Inpaint",
"newRegionalGuidance": "Nuova Guida Regionale"
},
"newImg2ImgCanvasFromImage": "Nuova Immagine da immagine"
"newImg2ImgCanvasFromImage": "Nuova Immagine da immagine",
"copyRasterLayerTo": "Copia $t(controlLayers.rasterLayer) in",
"copyControlLayerTo": "Copia $t(controlLayers.controlLayer) in",
"copyInpaintMaskTo": "Copia $t(controlLayers.inpaintMask) in",
"selectObject": {
"dragToMove": "Trascina un punto per spostarlo",
"clickToAdd": "Fare clic sul livello per aggiungere un punto",
"clickToRemove": "Clicca su un punto per rimuoverlo",
"help3": "Inverte la selezione per selezionare tutto tranne l'oggetto di destinazione.",
"pointType": "Tipo punto",
"apply": "Applica",
"reset": "Reimposta",
"cancel": "Annulla",
"selectObject": "Seleziona oggetto",
"invertSelection": "Inverti selezione",
"exclude": "Escludi",
"include": "Includi",
"neutral": "Neutro",
"saveAs": "Salva come",
"process": "Elabora",
"help1": "Seleziona un singolo oggetto di destinazione. Aggiungi i punti <Bold>Includi</Bold> e <Bold>Escludi</Bold> per indicare quali parti del livello fanno parte dell'oggetto di destinazione.",
"help2": "Inizia con un punto <Bold>Include</Bold> all'interno dell'oggetto di destinazione. Aggiungi altri punti per perfezionare la selezione. Meno punti in genere producono risultati migliori."
},
"convertControlLayerTo": "Converti $t(controlLayers.controlLayer) in",
"newRasterLayer": "Nuovo $t(controlLayers.rasterLayer)",
"newRegionalGuidance": "Nuova $t(controlLayers.regionalGuidance)",
"canvasAsRasterLayer": "$t(controlLayers.canvas) come $t(controlLayers.rasterLayer)",
"canvasAsControlLayer": "$t(controlLayers.canvas) come $t(controlLayers.controlLayer)",
"convertInpaintMaskTo": "Converti $t(controlLayers.inpaintMask) in",
"copyRegionalGuidanceTo": "Copia $t(controlLayers.regionalGuidance) in",
"convertRasterLayerTo": "Converti $t(controlLayers.rasterLayer) in",
"convertRegionalGuidanceTo": "Converti $t(controlLayers.regionalGuidance) in",
"newControlLayer": "Nuovo $t(controlLayers.controlLayer)",
"newInpaintMask": "Nuova $t(controlLayers.inpaintMask)",
"replaceCurrent": "Sostituisci corrente",
"mergeDown": "Unire in basso"
},
"ui": {
"tabs": {
@@ -2030,15 +2118,13 @@
"toGetStartedLocal": "Per iniziare, assicurati di scaricare o importare i modelli necessari per eseguire Invoke. Quindi, inserisci un prompt nella casella e fai clic su <StrongComponent>Invoke</StrongComponent> per generare la tua prima immagine. Seleziona un modello di prompt per migliorare i risultati. Puoi scegliere di salvare le tue immagini direttamente nella <StrongComponent>Galleria</StrongComponent> o modificarle nella <StrongComponent>Tela</StrongComponent>."
},
"whatsNew": {
"canvasV2Announcement": {
"readReleaseNotes": "Leggi le Note di Rilascio",
"fluxSupport": "Supporto per la famiglia di modelli Flux",
"newCanvas": "Una nuova potente tela di controllo",
"watchReleaseVideo": "Guarda il video di rilascio",
"watchUiUpdatesOverview": "Guarda le novità dell'interfaccia",
"newLayerTypes": "Nuovi tipi di livello per un miglior controllo"
},
"whatsNewInInvoke": "Novità in Invoke"
"whatsNewInInvoke": "Novità in Invoke",
"line2": "Supporto Flux esteso, ora con immagini di riferimento globali",
"line3": "Tooltip e menu contestuali migliorati",
"readReleaseNotes": "Leggi le note di rilascio",
"watchRecentReleaseVideos": "Guarda i video su questa versione",
"line1": "Strumento <ItalicComponent>Seleziona oggetto</ItalicComponent> per la selezione e la modifica precise degli oggetti",
"watchUiUpdatesOverview": "Guarda le novità dell'interfaccia"
},
"system": {
"logLevel": {

View File

@@ -229,7 +229,6 @@
"submitSupportTicket": "サポート依頼を送信する"
},
"metadata": {
"seamless": "シームレス",
"Threshold": "ノイズ閾値",
"seed": "シード",
"width": "幅",

View File

@@ -155,7 +155,6 @@
"path": "Pad",
"triggerPhrases": "Triggerzinnen",
"typePhraseHere": "Typ zin hier in",
"useDefaultSettings": "Gebruik standaardinstellingen",
"modelImageDeleteFailed": "Fout bij verwijderen modelafbeelding",
"modelImageUpdated": "Modelafbeelding bijgewerkt",
"modelImageUpdateFailed": "Fout bij bijwerken modelafbeelding",
@@ -666,7 +665,6 @@
}
},
"metadata": {
"seamless": "Naadloos",
"positivePrompt": "Positieve prompt",
"negativePrompt": "Negatieve prompt",
"generationMode": "Genereermodus",

View File

@@ -544,7 +544,6 @@
"scanResults": "Результаты сканирования",
"source": "Источник",
"triggerPhrases": "Триггерные фразы",
"useDefaultSettings": "Использовать стандартные настройки",
"modelName": "Название модели",
"modelSettings": "Настройки модели",
"upcastAttention": "Внимание",
@@ -573,7 +572,6 @@
"simpleModelPlaceholder": "URL или путь к локальному файлу или папке diffusers",
"urlOrLocalPath": "URL или локальный путь",
"urlOrLocalPathHelper": "URL-адреса должны указывать на один файл. Локальные пути могут указывать на один файл или папку для одной модели диффузоров.",
"hfToken": "Токен HuggingFace",
"starterModels": "Стартовые модели",
"textualInversions": "Текстовые инверсии",
"loraModels": "LoRAs",
@@ -1402,7 +1400,6 @@
}
},
"metadata": {
"seamless": "Бесшовность",
"positivePrompt": "Запрос",
"negativePrompt": "Негативный запрос",
"generationMode": "Режим генерации",
@@ -1836,14 +1833,12 @@
},
"settings": {
"isolatedPreview": "Изолированный предпросмотр",
"isolatedTransformingPreview": "Изолированный предпросмотр преобразования",
"invertBrushSizeScrollDirection": "Инвертировать прокрутку для размера кисти",
"snapToGrid": {
"label": "Привязка к сетке",
"on": "Вкл",
"off": "Выкл"
},
"isolatedFilteringPreview": "Изолированный предпросмотр фильтрации",
"pressureSensitivity": "Чувствительность к давлению",
"isolatedStagingPreview": "Изолированный предпросмотр на промежуточной стадии",
"preserveMask": {
@@ -1865,7 +1860,6 @@
"enableAutoNegative": "Включить авто негатив",
"maskFill": "Заполнение маски",
"viewProgressInViewer": "Просматривайте прогресс и результаты в <Btn>Просмотрщике изображений</Btn>.",
"convertToRasterLayer": "Конвертировать в растровый слой",
"tool": {
"move": "Двигать",
"bbox": "Ограничительная рамка",
@@ -1933,7 +1927,6 @@
"newGallerySession": "Новая сессия галереи",
"sendToCanvasDesc": "Нажатие кнопки Invoke отображает вашу текущую работу на холсте.",
"globalReferenceImages_withCount_hidden": "Глобальные эталонные изображения ({{count}} скрыто)",
"convertToControlLayer": "Конвертировать в контрольный слой",
"layer_withCount_one": "Слой ({{count}})",
"layer_withCount_few": "Слои ({{count}})",
"layer_withCount_many": "Слои ({{count}})",
@@ -2063,14 +2056,6 @@
}
},
"whatsNew": {
"canvasV2Announcement": {
"newLayerTypes": "Новые типы слоев для еще большего контроля",
"readReleaseNotes": "Прочитать информацию о выпуске",
"watchReleaseVideo": "Смотреть видео о выпуске",
"fluxSupport": "Поддержка семейства моделей Flux",
"newCanvas": "Новый мощный холст управления",
"watchUiUpdatesOverview": "Обзор обновлений пользовательского интерфейса"
},
"whatsNewInInvoke": "Что нового в Invoke"
},
"newUserExperience": {

View File

@@ -82,7 +82,21 @@
"dontShowMeThese": "请勿显示这些内容",
"beta": "测试版",
"toResolve": "解决",
"tab": "标签页"
"tab": "标签页",
"apply": "应用",
"edit": "编辑",
"off": "关",
"loadingImage": "正在加载图片",
"ok": "确定",
"placeholderSelectAModel": "选择一个模型",
"close": "关闭",
"reset": "重设",
"none": "无",
"new": "新建",
"view": "视图",
"alpha": "透明度通道",
"openInViewer": "在查看器中打开",
"clipboard": "剪贴板"
},
"gallery": {
"galleryImageSize": "预览大小",
@@ -124,7 +138,7 @@
"selectAllOnPage": "选择本页全部",
"swapImages": "交换图像",
"exitBoardSearch": "退出面板搜索",
"exitSearch": "退出搜索",
"exitSearch": "退出图像搜索",
"oldestFirst": "最旧在前",
"sortDirection": "排序方向",
"showStarredImagesFirst": "优先显示收藏的图片",
@@ -135,17 +149,333 @@
"searchImages": "按元数据搜索",
"jump": "跳过",
"compareHelp2": "按 <Kbd>M</Kbd> 键切换不同的比较模式。",
"displayBoardSearch": "显示面板搜索",
"displaySearch": "显示搜索",
"displayBoardSearch": "板搜索",
"displaySearch": "图像搜索",
"stretchToFit": "拉伸以适应",
"exitCompare": "退出对比",
"compareHelp1": "在点击图库中的图片或使用箭头键切换比较图片时,请按住<Kbd>Alt</Kbd> 键。",
"go": "运行"
"go": "运行",
"boardsSettings": "画板设置",
"imagesSettings": "画廊图片设置",
"gallery": "画廊",
"move": "移动",
"imagesTab": "您在Invoke中创建和保存的图片。",
"openViewer": "打开查看器",
"closeViewer": "关闭查看器",
"assetsTab": "您已上传用于项目的文件。"
},
"hotkeys": {
"searchHotkeys": "检索快捷键",
"noHotkeysFound": "未找到快捷键",
"clearSearch": "清除检索项"
"clearSearch": "清除检索项",
"app": {
"cancelQueueItem": {
"title": "取消",
"desc": "取消当前正在处理的队列项目。"
},
"selectQueueTab": {
"title": "选择队列标签",
"desc": "选择队列标签。"
},
"toggleLeftPanel": {
"desc": "显示或隐藏左侧面板。",
"title": "开关左侧面板"
},
"resetPanelLayout": {
"title": "重设面板布局",
"desc": "将左侧和右侧面板重置为默认大小和布局。"
},
"togglePanels": {
"title": "开关面板",
"desc": "同时显示或隐藏左右两侧的面板。"
},
"selectWorkflowsTab": {
"title": "选择工作流标签",
"desc": "选择工作流标签。"
},
"selectModelsTab": {
"title": "选择模型标签",
"desc": "选择模型标签。"
},
"toggleRightPanel": {
"title": "开关右侧面板",
"desc": "显示或隐藏右侧面板。"
},
"clearQueue": {
"title": "清除队列",
"desc": "取消并清除所有队列条目。"
},
"selectCanvasTab": {
"title": "选择画布标签",
"desc": "选择画布标签。"
},
"invokeFront": {
"desc": "将生成请求排队,添加到队列的前面。",
"title": "调用(前台)"
},
"selectUpscalingTab": {
"title": "选择放大选项卡",
"desc": "选择高清放大选项卡。"
},
"focusPrompt": {
"title": "聚焦提示",
"desc": "将光标焦点移动到正向提示。"
},
"title": "应用程序",
"invoke": {
"title": "调用",
"desc": "将生成请求排队,添加到队列的末尾。"
}
},
"canvas": {
"selectBrushTool": {
"title": "画笔工具",
"desc": "选择画笔工具。"
},
"selectEraserTool": {
"title": "橡皮擦工具",
"desc": "选择橡皮擦工具。"
},
"title": "画布",
"selectColorPickerTool": {
"title": "拾色器工具",
"desc": "选择拾色器工具。"
},
"fitBboxToCanvas": {
"title": "使边界框适应画布",
"desc": "缩放并调整视图以适应边界框。"
},
"setZoomTo400Percent": {
"title": "缩放到400%",
"desc": "将画布的缩放设置为400%。"
},
"setZoomTo800Percent": {
"desc": "将画布的缩放设置为800%。",
"title": "缩放到800%"
},
"redo": {
"desc": "重做上一次画布操作。",
"title": "重做"
},
"nextEntity": {
"title": "下一层",
"desc": "在列表中选择下一层。"
},
"selectRectTool": {
"title": "矩形工具",
"desc": "选择矩形工具。"
},
"selectViewTool": {
"title": "视图工具",
"desc": "选择视图工具。"
},
"prevEntity": {
"desc": "在列表中选择上一层。",
"title": "上一层"
},
"transformSelected": {
"desc": "变换所选图层。",
"title": "变换"
},
"selectBboxTool": {
"title": "边界框工具",
"desc": "选择边界框工具。"
},
"setZoomTo200Percent": {
"title": "缩放到200%",
"desc": "将画布的缩放设置为200%。"
},
"applyFilter": {
"title": "应用过滤器",
"desc": "将待处理的过滤器应用于所选图层。"
},
"filterSelected": {
"title": "过滤器",
"desc": "对所选图层进行过滤。仅适用于栅格层和控制层。"
},
"cancelFilter": {
"title": "取消过滤器",
"desc": "取消待处理的过滤器。"
},
"incrementToolWidth": {
"title": "增加工具宽度",
"desc": "增加所选的画笔或橡皮擦工具的宽度。"
},
"decrementToolWidth": {
"desc": "减少所选的画笔或橡皮擦工具的宽度。",
"title": "减少工具宽度"
},
"selectMoveTool": {
"title": "移动工具",
"desc": "选择移动工具。"
},
"setFillToWhite": {
"title": "将颜色设置为白色",
"desc": "将当前工具的颜色设置为白色。"
},
"cancelTransform": {
"desc": "取消待处理的变换。",
"title": "取消变换"
},
"applyTransform": {
"title": "应用变换",
"desc": "将待处理的变换应用于所选图层。"
},
"setZoomTo100Percent": {
"title": "缩放到100%",
"desc": "将画布的缩放设置为100%。"
},
"resetSelected": {
"title": "重置图层",
"desc": "重置选定的图层。仅适用于修复蒙版和区域指导。"
},
"undo": {
"title": "撤消",
"desc": "撤消上一次画布操作。"
},
"quickSwitch": {
"title": "图层快速切换",
"desc": "在最后两个选定的图层之间切换。如果某个图层被书签标记,则始终在该图层和最后一个未标记的图层之间切换。"
},
"fitLayersToCanvas": {
"title": "使图层适应画布",
"desc": "缩放并调整视图以适应所有可见图层。"
},
"deleteSelected": {
"title": "删除图层",
"desc": "删除选定的图层。"
}
},
"hotkeys": "快捷键",
"workflows": {
"pasteSelection": {
"title": "粘贴",
"desc": "粘贴复制的节点和边。"
},
"title": "工作流",
"addNode": {
"title": "添加节点",
"desc": "打开添加节点菜单。"
},
"copySelection": {
"desc": "复制选定的节点和边。",
"title": "复制"
},
"pasteSelectionWithEdges": {
"title": "带边缘的粘贴",
"desc": "粘贴复制的节点、边,以及与复制的节点连接的所有边。"
},
"selectAll": {
"title": "全选",
"desc": "选择所有节点和边。"
},
"deleteSelection": {
"title": "删除",
"desc": "删除选定的节点和边。"
},
"undo": {
"title": "撤销",
"desc": "撤销上一个工作流操作。"
},
"redo": {
"desc": "重做上一个工作流操作。",
"title": "重做"
}
},
"gallery": {
"title": "画廊",
"galleryNavUp": {
"title": "向上导航",
"desc": "在图库网格中向上导航,选择该图像。如果在页面顶部,则转到上一页。"
},
"galleryNavUpAlt": {
"title": "向上导航(比较图像)",
"desc": "与向上导航相同,但选择比较图像,如果比较模式尚未打开,则将其打开。"
},
"selectAllOnPage": {
"desc": "选择当前页面上的所有图像。",
"title": "选页面上的所有内容"
},
"galleryNavDownAlt": {
"title": "向下导航(比较图像)",
"desc": "与向下导航相同,但选择比较图像,如果比较模式尚未打开,则将其打开。"
},
"galleryNavLeftAlt": {
"title": "向左导航(比较图像)",
"desc": "与向左导航相同,但选择比较图像,如果比较模式尚未打开,则将其打开。"
},
"clearSelection": {
"title": "清除选择",
"desc": "清除当前的选择(如果有的话)。"
},
"deleteSelection": {
"title": "删除",
"desc": "删除所有选定的图像。默认情况下,系统会提示您确认删除。如果这些图像当前在应用中使用,系统将发出警告。"
},
"galleryNavLeft": {
"title": "向左导航",
"desc": "在图库网格中向左导航,选择该图像。如果处于行的第一张图像,转到上一行。如果处于页面的第一张图像,转到上一页。"
},
"galleryNavRight": {
"title": "向右导航",
"desc": "在图库网格中向右导航,选择该图像。如果在行的最后一张图像,转到下一行。如果在页面的最后一张图像,转到下一页。"
},
"galleryNavDown": {
"desc": "在图库网格中向下导航,选择该图像。如果在页面底部,则转到下一页。",
"title": "向下导航"
},
"galleryNavRightAlt": {
"title": "向右导航(比较图像)",
"desc": "与向右导航相同,但选择比较图像,如果比较模式尚未打开,则将其打开。"
}
},
"viewer": {
"toggleMetadata": {
"desc": "显示或隐藏当前图像的元数据覆盖。",
"title": "显示/隐藏元数据"
},
"recallPrompts": {
"desc": "召回当前图像的正面和负面提示。",
"title": "召回提示"
},
"toggleViewer": {
"title": "显示/隐藏图像查看器",
"desc": "显示或隐藏图像查看器。仅在画布选项卡上可用。"
},
"recallAll": {
"desc": "召回当前图像的所有元数据。",
"title": "召回所有元数据"
},
"recallSeed": {
"title": "召回种子",
"desc": "召回当前图像的种子。"
},
"swapImages": {
"title": "交换比较图像",
"desc": "交换正在比较的图像。"
},
"nextComparisonMode": {
"title": "下一个比较模式",
"desc": "环浏览比较模式。"
},
"loadWorkflow": {
"title": "加载工作流",
"desc": "加载当前图像的保存工作流程(如果有的话)。"
},
"title": "图像查看器",
"remix": {
"title": "混合",
"desc": "召回当前图像的所有元数据,除了种子。"
},
"useSize": {
"title": "使用尺寸",
"desc": "使用当前图像的尺寸作为边界框尺寸。"
},
"runPostprocessing": {
"title": "行后处理",
"desc": "对当前图像运行所选的后处理。"
}
}
},
"modelManager": {
"modelManager": "模型管理器",
@@ -210,7 +540,6 @@
"noModelsInstalled": "无已安装的模型",
"urlOrLocalPathHelper": "链接应该指向单个文件.本地路径可以指向单个文件,或者对于单个扩散模型(diffusers model),可以指向一个文件夹.",
"modelSettings": "模型设置",
"useDefaultSettings": "使用默认设置",
"scanPlaceholder": "本地文件夹路径",
"installRepo": "安装仓库",
"modelImageDeleted": "模型图像已删除",
@@ -249,7 +578,16 @@
"loraTriggerPhrases": "LoRA 触发词",
"ipAdapters": "IP适配器",
"spandrelImageToImage": "图生图(Spandrel)",
"starterModelsInModelManager": "您可以在模型管理器中找到初始模型"
"starterModelsInModelManager": "您可以在模型管理器中找到初始模型",
"noDefaultSettings": "此模型没有配置默认设置。请访问模型管理器添加默认设置。",
"clipEmbed": "CLIP 嵌入",
"defaultSettingsOutOfSync": "某些设置与模型的默认值不匹配:",
"restoreDefaultSettings": "点击以使用模型的默认设置。",
"usingDefaultSettings": "使用模型的默认设置",
"huggingFace": "HuggingFace",
"hfTokenInvalid": "HF 令牌无效或缺失",
"hfTokenLabel": "HuggingFace 令牌(某些模型所需)",
"hfTokenHelperText": "使用某些模型需要 HF 令牌。点击这里创建或获取你的令牌。"
},
"parameters": {
"images": "图像",
@@ -367,7 +705,7 @@
"uploadFailed": "上传失败",
"imageCopied": "图像已复制",
"parametersNotSet": "参数未恢复",
"uploadFailedInvalidUploadDesc": "必须是单张的 PNG 或 JPEG 图",
"uploadFailedInvalidUploadDesc": "必须是单 PNG 或 JPEG 图像。",
"connected": "服务器连接",
"parameterSet": "参数已恢复",
"parameterNotSet": "参数未恢复",
@@ -379,7 +717,7 @@
"setControlImage": "设为控制图像",
"setNodeField": "设为节点字段",
"imageUploaded": "图像已上传",
"addedToBoard": "添加到面板",
"addedToBoard": "添加到{{name}}的资产中",
"workflowLoaded": "工作流已加载",
"imageUploadFailed": "图像上传失败",
"baseModelChangedCleared_other": "已清除或禁用{{count}}个不兼容的子模型",
@@ -416,7 +754,9 @@
"createIssue": "创建问题",
"about": "关于",
"submitSupportTicket": "提交支持工单",
"toggleRightPanel": "切换右侧面板(G)"
"toggleRightPanel": "切换右侧面板(G)",
"uploadImages": "上传图片",
"toggleLeftPanel": "开关左侧面板(T)"
},
"nodes": {
"zoomInNodes": "放大",
@@ -569,7 +909,7 @@
"cancelSucceeded": "项目已取消",
"queue": "队列",
"batch": "批处理",
"clearQueueAlertDialog": "清队列时会立即取消所有处理的项目并且会完全清队列。",
"clearQueueAlertDialog": "清队列立即取消所有正在处理的项目并完全清队列。待处理的过滤器将被取消。",
"pending": "待定",
"completedIn": "完成于",
"resumeFailed": "恢复处理器时出现问题",
@@ -610,7 +950,15 @@
"openQueue": "打开队列",
"prompts_other": "提示词",
"iterations_other": "迭代",
"generations_other": "生成"
"generations_other": "生成",
"canvas": "画布",
"workflows": "工作流",
"generation": "生成",
"other": "其他",
"gallery": "画廊",
"destination": "目标存储",
"upscaling": "高清放大",
"origin": "来源"
},
"sdxl": {
"refinerStart": "Refiner 开始作用时机",
@@ -649,7 +997,6 @@
"workflow": "工作流",
"steps": "步数",
"scheduler": "调度器",
"seamless": "无缝",
"recallParameters": "召回参数",
"noRecallParameters": "未找到要召回的参数",
"vae": "VAE",
@@ -658,7 +1005,11 @@
"parsingFailed": "解析失败",
"recallParameter": "调用{{label}}",
"imageDimensions": "图像尺寸",
"parameterSet": "已设置参数{{parameter}}"
"parameterSet": "已设置参数{{parameter}}",
"guidance": "指导",
"seamlessXAxis": "无缝 X 轴",
"seamlessYAxis": "无缝 Y 轴",
"canvasV2Metadata": "画布"
},
"models": {
"noMatchingModels": "无相匹配的模型",
@@ -709,7 +1060,8 @@
"shared": "共享面板",
"archiveBoard": "归档面板",
"archived": "已归档",
"assetsWithCount_other": "{{count}}项资源"
"assetsWithCount_other": "{{count}}项资源",
"updateBoardError": "更新画板出错"
},
"dynamicPrompts": {
"seedBehaviour": {
@@ -1175,7 +1527,8 @@
},
"prompt": {
"addPromptTrigger": "添加提示词触发器",
"noMatchingTriggers": "没有匹配的触发器"
"noMatchingTriggers": "没有匹配的触发器",
"compatibleEmbeddings": "兼容的嵌入"
},
"controlLayers": {
"autoNegative": "自动反向",
@@ -1186,8 +1539,8 @@
"moveToFront": "移动到前面",
"addLayer": "添加层",
"deletePrompt": "删除提示词",
"addPositivePrompt": "添加 $t(common.positivePrompt)",
"addNegativePrompt": "添加 $t(common.negativePrompt)",
"addPositivePrompt": "添加 $t(controlLayers.prompt)",
"addNegativePrompt": "添加 $t(controlLayers.negativePrompt)",
"rectangle": "矩形",
"opacity": "透明度"
},

View File

@@ -58,7 +58,6 @@
"model": "模型",
"seed": "種子",
"vae": "VAE",
"seamless": "無縫",
"metadata": "元數據",
"width": "寬度",
"height": "高度"

View File

@@ -8,6 +8,7 @@ import {
controlLayerAdded,
entityRasterized,
entitySelected,
inpaintMaskAdded,
rasterLayerAdded,
referenceImageAdded,
referenceImageIPAdapterImageChanged,
@@ -17,6 +18,7 @@ import {
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type {
CanvasControlLayerState,
CanvasInpaintMaskState,
CanvasRasterLayerState,
CanvasReferenceImageState,
CanvasRegionalGuidanceState,
@@ -110,6 +112,46 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
return;
}
/**
/**
* Image dropped on Inpaint Mask
*/
if (
overData.actionType === 'ADD_INPAINT_MASK_FROM_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
const { x, y } = selectCanvasSlice(getState()).bbox.rect;
const overrides: Partial<CanvasInpaintMaskState> = {
objects: [imageObject],
position: { x, y },
};
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
return;
}
/**
/**
* Image dropped on Regional Guidance
*/
if (
overData.actionType === 'ADD_REGIONAL_GUIDANCE_FROM_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
const { x, y } = selectCanvasSlice(getState()).bbox.rect;
const overrides: Partial<CanvasRegionalGuidanceState> = {
objects: [imageObject],
position: { x, y },
};
dispatch(rgAdded({ overrides, isSelected: true }));
return;
}
/**
* Image dropped on Raster layer
*/

View File

@@ -5,7 +5,7 @@ import { atom } from 'nanostores';
* A fallback non-writable atom that always returns `false`, used when a nanostores atom is only conditionally available
* in a hook or component.
*/
export const $false: ReadableAtom<boolean> = atom(false);
// export const $false: ReadableAtom<boolean> = atom(false);
/**
* A fallback non-writable atom that always returns `true`, used when a nanostores atom is only conditionally available
* in a hook or component.

View File

@@ -26,5 +26,9 @@ export const IconMenuItem = ({ tooltip, icon, ...props }: Props) => {
};
export const IconMenuItemGroup = ({ children }: { children: ReactNode }) => {
return <Flex gap={2}>{children}</Flex>;
return (
<Flex gap={2} justifyContent="space-between">
{children}
</Flex>
);
};

View File

@@ -23,8 +23,10 @@ export type Feature =
| 'dynamicPrompts'
| 'dynamicPromptsMaxPrompts'
| 'dynamicPromptsSeedBehaviour'
| 'globalReferenceImage'
| 'imageFit'
| 'infillMethod'
| 'inpainting'
| 'ipAdapterMethod'
| 'lora'
| 'loraWeight'
@@ -46,6 +48,7 @@ export type Feature =
| 'paramVAEPrecision'
| 'paramWidth'
| 'patchmatchDownScaleSize'
| 'rasterLayer'
| 'refinerModel'
| 'refinerNegativeAestheticScore'
| 'refinerPositiveAestheticScore'
@@ -53,6 +56,9 @@ export type Feature =
| 'refinerStart'
| 'refinerSteps'
| 'refinerCfgScale'
| 'regionalGuidance'
| 'regionalGuidanceAndReferenceImage'
| 'regionalReferenceImage'
| 'scaleBeforeProcessing'
| 'seamlessTilingXAxis'
| 'seamlessTilingYAxis'
@@ -76,6 +82,24 @@ export const POPOVER_DATA: { [key in Feature]?: PopoverData } = {
clipSkip: {
href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings',
},
inpainting: {
href: 'https://support.invoke.ai/support/solutions/articles/151000096702-inpainting-outpainting-and-bounding-box',
},
rasterLayer: {
href: 'https://support.invoke.ai/support/solutions/articles/151000094998-raster-layers-and-initial-images',
},
regionalGuidance: {
href: 'https://support.invoke.ai/support/solutions/articles/151000165024-regional-guidance-layers',
},
regionalGuidanceAndReferenceImage: {
href: 'https://support.invoke.ai/support/solutions/articles/151000165024-regional-guidance-layers',
},
globalReferenceImage: {
href: 'https://support.invoke.ai/support/solutions/articles/151000159340-global-and-regional-reference-images-ip-adapters-',
},
regionalReferenceImage: {
href: 'https://support.invoke.ai/support/solutions/articles/151000159340-global-and-regional-reference-images-ip-adapters-',
},
controlNet: {
href: 'https://support.invoke.ai/support/solutions/articles/151000105880',
},

View File

@@ -127,8 +127,6 @@ export const buildUseDisclosure = (defaultIsOpen: boolean): [() => UseDisclosure
*
* Hook to manage a boolean state. Use this for a local boolean state.
* @param defaultIsOpen Initial state of the disclosure
*
* @knipignore
*/
export const useDisclosure = (defaultIsOpen: boolean): UseDisclosure => {
const [isOpen, set] = useState(defaultIsOpen);

View File

@@ -4,6 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import type { GroupBase } from 'chakra-react-select';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import type { ModelIdentifierField } from 'features/nodes/types/common';
import { selectSystemShouldEnableModelDescriptions } from 'features/system/store/systemSlice';
import { groupBy, reduce } from 'lodash-es';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -37,6 +38,7 @@ export const useGroupedModelCombobox = <T extends AnyModelConfig>(
): UseGroupedModelComboboxReturn => {
const { t } = useTranslation();
const base = useAppSelector(selectBaseWithSDXLFallback);
const shouldShowModelDescriptions = useAppSelector(selectSystemShouldEnableModelDescriptions);
const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, groupByType = false } = arg;
const options = useMemo<GroupBase<ComboboxOption>[]>(() => {
if (!modelConfigs) {
@@ -51,6 +53,7 @@ export const useGroupedModelCombobox = <T extends AnyModelConfig>(
options: val.map((model) => ({
label: model.name,
value: model.key,
description: (shouldShowModelDescriptions && model.description) || undefined,
isDisabled: getIsDisabled ? getIsDisabled(model) : false,
})),
});
@@ -60,7 +63,7 @@ export const useGroupedModelCombobox = <T extends AnyModelConfig>(
);
_options.sort((a) => (a.label?.split('/')[0]?.toLowerCase().includes(base) ? -1 : 1));
return _options;
}, [modelConfigs, groupByType, getIsDisabled, base]);
}, [modelConfigs, groupByType, getIsDisabled, base, shouldShowModelDescriptions]);
const value = useMemo(
() =>

View File

@@ -202,46 +202,6 @@ const createSelector = (
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.scaleMethod === 'none') {
if (bbox.rect.width % 16 !== 0) {
reasons.push({
content: i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleBboxWidth', {
multiple,
width: bbox.rect.width,
}),
});
}
if (bbox.rect.height % 16 !== 0) {
reasons.push({
content: i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleBboxHeight', {
multiple,
height: bbox.rect.height,
}),
});
}
} else {
if (bbox.scaledSize.width % 16 !== 0) {
reasons.push({
content: i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleScaledBboxWidth', {
multiple,
width: bbox.scaledSize.width,
}),
});
}
if (bbox.scaledSize.height % 16 !== 0) {
reasons.push({
content: i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleScaledBboxHeight', {
multiple,
height: bbox.scaledSize.height,
}),
});
}
}
}
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });

View File

@@ -1,5 +1,7 @@
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import type { ModelIdentifierField } from 'features/nodes/types/common';
import { selectSystemShouldEnableModelDescriptions } from 'features/system/store/systemSlice';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import type { AnyModelConfig } from 'services/api/types';
@@ -24,13 +26,16 @@ type UseModelComboboxReturn = {
export const useModelCombobox = <T extends AnyModelConfig>(arg: UseModelComboboxArg<T>): UseModelComboboxReturn => {
const { t } = useTranslation();
const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, optionsFilter = () => true } = arg;
const shouldShowModelDescriptions = useAppSelector(selectSystemShouldEnableModelDescriptions);
const options = useMemo<ComboboxOption[]>(() => {
return modelConfigs.filter(optionsFilter).map((model) => ({
label: model.name,
value: model.key,
description: (shouldShowModelDescriptions && model.description) || undefined,
isDisabled: getIsDisabled ? getIsDisabled(model) : false,
}));
}, [optionsFilter, getIsDisabled, modelConfigs]);
}, [optionsFilter, getIsDisabled, modelConfigs, shouldShowModelDescriptions]);
const value = useMemo(
() => options.find((m) => (selectedModel ? m.value === selectedModel.key : false)),

View File

@@ -0,0 +1,161 @@
import type { MenuButtonProps, MenuItemProps, MenuListProps, MenuProps } from '@invoke-ai/ui-library';
import { Box, Flex, Icon, Text } from '@invoke-ai/ui-library';
import { useDisclosure } from 'common/hooks/useBoolean';
import type { FocusEventHandler, PointerEvent, RefObject } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { PiCaretRightBold } from 'react-icons/pi';
import { useDebouncedCallback } from 'use-debounce';
const offset: [number, number] = [0, 8];
type UseSubMenuReturn = {
parentMenuItemProps: Partial<MenuItemProps>;
menuProps: Partial<MenuProps>;
menuButtonProps: Partial<MenuButtonProps>;
menuListProps: Partial<MenuListProps> & { ref: RefObject<HTMLDivElement> };
};
/**
* A hook that provides the necessary props to create a sub-menu within a menu.
*
* The sub-menu should be wrapped inside a parent `MenuItem` component.
*
* Use SubMenuButtonContent to render a button with a label and a right caret icon.
*
* TODO(psyche): Add keyboard handling for sub-menu.
*
* @example
* ```tsx
* const SubMenuExample = () => {
* const subMenu = useSubMenu();
* return (
* <Menu>
* <MenuButton>Open Parent Menu</MenuButton>
* <MenuList>
* <MenuItem>Parent Item 1</MenuItem>
* <MenuItem>Parent Item 2</MenuItem>
* <MenuItem>Parent Item 3</MenuItem>
* <MenuItem {...subMenu.parentMenuItemProps} icon={<PiImageBold />}>
* <Menu {...subMenu.menuProps}>
* <MenuButton {...subMenu.menuButtonProps}>
* <SubMenuButtonContent label="Open Sub Menu" />
* </MenuButton>
* <MenuList {...subMenu.menuListProps}>
* <MenuItem>Sub Item 1</MenuItem>
* <MenuItem>Sub Item 2</MenuItem>
* <MenuItem>Sub Item 3</MenuItem>
* </MenuList>
* </Menu>
* </MenuItem>
* </MenuList>
* </Menu>
* );
* };
* ```
*/
export const useSubMenu = (): UseSubMenuReturn => {
const subMenu = useDisclosure(false);
const menuListRef = useRef<HTMLDivElement>(null);
const closeDebounced = useDebouncedCallback(subMenu.close, 300);
const openAndCancelPendingClose = useCallback(() => {
closeDebounced.cancel();
subMenu.open();
}, [closeDebounced, subMenu]);
const toggleAndCancelPendingClose = useCallback(() => {
if (subMenu.isOpen) {
subMenu.close();
return;
} else {
closeDebounced.cancel();
subMenu.toggle();
}
}, [closeDebounced, subMenu]);
const onBlurMenuList = useCallback<FocusEventHandler<HTMLDivElement>>(
(e) => {
// Don't trigger blur if focus is moving to a child element - e.g. from a sub-menu item to another sub-menu item
if (e.currentTarget.contains(e.relatedTarget)) {
closeDebounced.cancel();
return;
}
subMenu.close();
},
[closeDebounced, subMenu]
);
const onParentMenuItemPointerLeave = useCallback(
(e: PointerEvent<HTMLButtonElement>) => {
/**
* The pointerleave event is triggered when the pen or touch device is lifted, which would close the sub-menu.
* However, we want to keep the sub-menu open until the pen or touch device pressed some other element. This
* will be handled in the useEffect below - just ignore the pointerleave event for pen and touch devices.
*/
if (e.pointerType === 'pen' || e.pointerType === 'touch') {
return;
}
subMenu.close();
},
[subMenu]
);
/**
* When using a mouse, the pointerleave events close the menu. But when using a pen or touch device, we need to close
* the sub-menu when the user taps outside of the menu list. So we need to listen for clicks outside of the menu list
* and close the menu accordingly.
*/
useEffect(() => {
const el = menuListRef.current;
if (!el) {
return;
}
const controller = new AbortController();
window.addEventListener(
'click',
(e) => {
if (menuListRef.current?.contains(e.target as Node)) {
return;
}
subMenu.close();
},
{ signal: controller.signal }
);
return () => {
controller.abort();
};
}, [subMenu]);
return {
parentMenuItemProps: {
onClick: toggleAndCancelPendingClose,
onPointerEnter: openAndCancelPendingClose,
onPointerLeave: onParentMenuItemPointerLeave,
closeOnSelect: false,
},
menuProps: {
isOpen: subMenu.isOpen,
onClose: subMenu.close,
placement: 'right',
offset: offset,
closeOnBlur: false,
},
menuButtonProps: {
as: Box,
width: 'full',
height: 'full',
},
menuListProps: {
ref: menuListRef,
onPointerEnter: openAndCancelPendingClose,
onPointerLeave: closeDebounced,
onBlur: onBlurMenuList,
},
};
};
export const SubMenuButtonContent = ({ label }: { label: string }) => {
return (
<Flex w="full" h="full" flexDir="row" justifyContent="space-between" alignItems="center">
<Text>{label}</Text>
<Icon as={PiCaretRightBold} />
</Flex>
);
};

View File

@@ -1,5 +1,6 @@
import { Button, Flex, Heading } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import {
useAddControlLayer,
useAddGlobalReferenceImage,
@@ -28,69 +29,80 @@ export const CanvasAddEntityButtons = memo(() => {
<Flex position="relative" flexDir="column" gap={4} top="20%">
<Flex flexDir="column" justifyContent="flex-start" gap={2}>
<Heading size="xs">{t('controlLayers.global')}</Heading>
<Button
size="sm"
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addGlobalReferenceImage}
>
{t('controlLayers.globalReferenceImage')}
</Button>
<InformationalPopover feature="globalReferenceImage">
<Button
size="sm"
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addGlobalReferenceImage}
>
{t('controlLayers.globalReferenceImage')}
</Button>
</InformationalPopover>
</Flex>
<Flex flexDir="column" gap={2}>
<Heading size="xs">{t('controlLayers.regional')}</Heading>
<Button
size="sm"
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addInpaintMask}
>
{t('controlLayers.inpaintMask')}
</Button>
<Button
size="sm"
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addRegionalGuidance}
isDisabled={isFLUX}
>
{t('controlLayers.regionalGuidance')}
</Button>
<Button
size="sm"
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addRegionalReferenceImage}
isDisabled={isFLUX}
>
{t('controlLayers.regionalReferenceImage')}
</Button>
<InformationalPopover feature="inpainting">
<Button
size="sm"
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addInpaintMask}
>
{t('controlLayers.inpaintMask')}
</Button>
</InformationalPopover>
<InformationalPopover feature="regionalGuidance">
<Button
size="sm"
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addRegionalGuidance}
isDisabled={isFLUX}
>
{t('controlLayers.regionalGuidance')}
</Button>
</InformationalPopover>
<InformationalPopover feature="regionalReferenceImage">
<Button
size="sm"
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addRegionalReferenceImage}
isDisabled={isFLUX}
>
{t('controlLayers.regionalReferenceImage')}
</Button>
</InformationalPopover>
</Flex>
<Flex flexDir="column" justifyContent="flex-start" gap={2}>
<Heading size="xs">{t('controlLayers.layer_other')}</Heading>
<Button
size="sm"
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addControlLayer}
>
{t('controlLayers.controlLayer')}
</Button>
<Button
size="sm"
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addRasterLayer}
>
{t('controlLayers.rasterLayer')}
</Button>
<InformationalPopover feature="controlNet">
<Button
size="sm"
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addControlLayer}
>
{t('controlLayers.controlLayer')}
</Button>
</InformationalPopover>
<InformationalPopover feature="rasterLayer">
<Button
size="sm"
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addRasterLayer}
>
{t('controlLayers.rasterLayer')}
</Button>
</InformationalPopover>
</Flex>
</Flex>
</Flex>

View File

@@ -13,7 +13,7 @@ export const CanvasAlertsPreserveMask = memo(() => {
}
return (
<Alert status="warning" borderRadius="base" fontSize="sm" shadow="md" w="fit-content" alignSelf="flex-end">
<Alert status="warning" borderRadius="base" fontSize="sm" shadow="md" w="fit-content">
<AlertIcon />
<AlertTitle>{t('controlLayers.settings.preserveMask.alert')}</AlertTitle>
</Alert>

View File

@@ -98,7 +98,7 @@ const CanvasAlertsSelectedEntityStatusContent = memo(({ entityIdentifier, adapte
}
return (
<Alert status={alert.status} borderRadius="base" fontSize="sm" shadow="md" w="fit-content" alignSelf="flex-end">
<Alert status={alert.status} borderRadius="base" fontSize="sm" shadow="md" w="fit-content">
<AlertIcon />
<AlertTitle>{alert.title}</AlertTitle>
</Alert>

View File

@@ -132,7 +132,6 @@ const AlertWrapper = ({
fontSize="sm"
shadow="md"
w="fit-content"
alignSelf="flex-end"
>
<Flex w="full" alignItems="center">
<AlertIcon />

View File

@@ -1,4 +1,5 @@
import { MenuGroup, MenuItem } from '@invoke-ai/ui-library';
import { Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { CanvasContextMenuItemsCropCanvasToBbox } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuItemsCropCanvasToBbox';
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
import {
@@ -16,6 +17,8 @@ import { PiFloppyDiskBold } from 'react-icons/pi';
export const CanvasContextMenuGlobalMenuItems = memo(() => {
const { t } = useTranslation();
const saveSubMenu = useSubMenu();
const newSubMenu = useSubMenu();
const isBusy = useCanvasIsBusy();
const saveCanvasToGallery = useSaveCanvasToGallery();
const saveBboxToGallery = useSaveBboxToGallery();
@@ -28,27 +31,41 @@ export const CanvasContextMenuGlobalMenuItems = memo(() => {
<>
<MenuGroup title={t('controlLayers.canvasContextMenu.canvasGroup')}>
<CanvasContextMenuItemsCropCanvasToBbox />
</MenuGroup>
<MenuGroup title={t('controlLayers.canvasContextMenu.saveToGalleryGroup')}>
<MenuItem icon={<PiFloppyDiskBold />} isDisabled={isBusy} onClick={saveCanvasToGallery}>
{t('controlLayers.canvasContextMenu.saveCanvasToGallery')}
<MenuItem {...saveSubMenu.parentMenuItemProps} icon={<PiFloppyDiskBold />}>
<Menu {...saveSubMenu.menuProps}>
<MenuButton {...saveSubMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.canvasContextMenu.saveToGalleryGroup')} />
</MenuButton>
<MenuList {...saveSubMenu.menuListProps}>
<MenuItem icon={<PiFloppyDiskBold />} isDisabled={isBusy} onClick={saveCanvasToGallery}>
{t('controlLayers.canvasContextMenu.saveCanvasToGallery')}
</MenuItem>
<MenuItem icon={<PiFloppyDiskBold />} isDisabled={isBusy} onClick={saveBboxToGallery}>
{t('controlLayers.canvasContextMenu.saveBboxToGallery')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
<MenuItem icon={<PiFloppyDiskBold />} isDisabled={isBusy} onClick={saveBboxToGallery}>
{t('controlLayers.canvasContextMenu.saveBboxToGallery')}
</MenuItem>
</MenuGroup>
<MenuGroup title={t('controlLayers.canvasContextMenu.bboxGroup')}>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onClick={newGlobalReferenceImageFromBbox}>
{t('controlLayers.canvasContextMenu.newGlobalReferenceImage')}
</MenuItem>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onClick={newRegionalReferenceImageFromBbox}>
{t('controlLayers.canvasContextMenu.newRegionalReferenceImage')}
</MenuItem>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onClick={newControlLayerFromBbox}>
{t('controlLayers.canvasContextMenu.newControlLayer')}
</MenuItem>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onClick={newRasterLayerFromBbox}>
{t('controlLayers.canvasContextMenu.newRasterLayer')}
<MenuItem {...newSubMenu.parentMenuItemProps} icon={<NewLayerIcon />}>
<Menu {...newSubMenu.menuProps}>
<MenuButton {...newSubMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.canvasContextMenu.bboxGroup')} />
</MenuButton>
<MenuList {...newSubMenu.menuListProps}>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onClick={newGlobalReferenceImageFromBbox}>
{t('controlLayers.canvasContextMenu.newGlobalReferenceImage')}
</MenuItem>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onClick={newRegionalReferenceImageFromBbox}>
{t('controlLayers.canvasContextMenu.newRegionalReferenceImage')}
</MenuItem>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onClick={newControlLayerFromBbox}>
{t('controlLayers.canvasContextMenu.newControlLayer')}
</MenuItem>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onClick={newRasterLayerFromBbox}>
{t('controlLayers.canvasContextMenu.newRasterLayer')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
</MenuGroup>
</>

View File

@@ -1,42 +1,43 @@
import { MenuGroup } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
import { CanvasEntityMenuItemsSegment } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSegment';
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
import { ControlLayerMenuItems } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItems';
import { InpaintMaskMenuItems } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItems';
import { IPAdapterMenuItems } from 'features/controlLayers/components/IPAdapter/IPAdapterMenuItems';
import { RasterLayerMenuItems } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItems';
import { RegionalGuidanceMenuItems } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems';
import {
EntityIdentifierContext,
useEntityIdentifierContext,
} from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useEntityTitle } from 'features/controlLayers/hooks/useEntityTitle';
import { useEntityTypeString } from 'features/controlLayers/hooks/useEntityTypeString';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import {
isFilterableEntityIdentifier,
isSaveableEntityIdentifier,
isSegmentableEntityIdentifier,
isTransformableEntityIdentifier,
} from 'features/controlLayers/store/types';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
const CanvasContextMenuSelectedEntityMenuItemsContent = memo(() => {
const entityIdentifier = useEntityIdentifierContext();
const title = useEntityTitle(entityIdentifier);
return (
<MenuGroup title={title}>
{isFilterableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsFilter />}
{isTransformableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsTransform />}
{isSegmentableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsSegment />}
{isSaveableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsCopyToClipboard />}
{isSaveableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsSave />}
{isTransformableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsCropToBbox />}
<CanvasEntityMenuItemsDelete />
</MenuGroup>
);
if (entityIdentifier.type === 'raster_layer') {
return <RasterLayerMenuItems />;
}
if (entityIdentifier.type === 'control_layer') {
return <ControlLayerMenuItems />;
}
if (entityIdentifier.type === 'inpaint_mask') {
return <InpaintMaskMenuItems />;
}
if (entityIdentifier.type === 'regional_guidance') {
return <RegionalGuidanceMenuItems />;
}
if (entityIdentifier.type === 'reference_image') {
return <IPAdapterMenuItems />;
}
assert<Equals<typeof entityIdentifier.type, never>>(false);
});
CanvasContextMenuSelectedEntityMenuItemsContent.displayName = 'CanvasContextMenuSelectedEntityMenuItemsContent';
export const CanvasContextMenuSelectedEntityMenuItems = memo(() => {
@@ -48,9 +49,20 @@ export const CanvasContextMenuSelectedEntityMenuItems = memo(() => {
return (
<EntityIdentifierContext.Provider value={selectedEntityIdentifier}>
<CanvasContextMenuSelectedEntityMenuItemsContent />
<CanvasContextMenuSelectedEntityMenuGroup>
<CanvasContextMenuSelectedEntityMenuItemsContent />
</CanvasContextMenuSelectedEntityMenuGroup>
</EntityIdentifierContext.Provider>
);
});
CanvasContextMenuSelectedEntityMenuItems.displayName = 'CanvasContextMenuSelectedEntityMenuItems';
const CanvasContextMenuSelectedEntityMenuGroup = memo((props: PropsWithChildren) => {
const entityIdentifier = useEntityIdentifierContext();
const title = useEntityTypeString(entityIdentifier.type);
return <MenuGroup title={title}>{props.children}</MenuGroup>;
});
CanvasContextMenuSelectedEntityMenuGroup.displayName = 'CanvasContextMenuSelectedEntityMenuGroup';

View File

@@ -62,6 +62,7 @@ export const CanvasDropArea = memo(() => {
data={addControlLayerFromImageDropData}
/>
</GridItem>
<GridItem position="relative">
<IAIDroppable
dropLabel={t('controlLayers.canvasContextMenu.newRegionalReferenceImage')}

View File

@@ -29,7 +29,7 @@ export const EntityListGlobalActionBarAddLayerMenu = memo(() => {
<Menu>
<MenuButton
as={IconButton}
size="sm"
minW={8}
variant="link"
alignSelf="stretch"
tooltip={t('controlLayers.addLayer')}

View File

@@ -4,6 +4,7 @@ import { EntityListSelectedEntityActionBarDuplicateButton } from 'features/contr
import { EntityListSelectedEntityActionBarFill } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFill';
import { EntityListSelectedEntityActionBarFilterButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton';
import { EntityListSelectedEntityActionBarOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity';
import { EntityListSelectedEntityActionBarSelectObjectButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarSelectObjectButton';
import { EntityListSelectedEntityActionBarTransformButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton';
import { memo } from 'react';
@@ -16,6 +17,7 @@ export const EntityListSelectedEntityActionBar = memo(() => {
<Spacer />
<EntityListSelectedEntityActionBarFill />
<Flex h="full">
<EntityListSelectedEntityActionBarSelectObjectButton />
<EntityListSelectedEntityActionBarFilterButton />
<EntityListSelectedEntityActionBarTransformButton />
<EntityListSelectedEntityActionBarSaveToAssetsButton />

View File

@@ -23,7 +23,7 @@ export const EntityListSelectedEntityActionBarDuplicateButton = memo(() => {
<IconButton
onClick={onClick}
isDisabled={!selectedEntityIdentifier || isBusy}
size="sm"
minW={8}
variant="link"
alignSelf="stretch"
aria-label={t('controlLayers.duplicate')}

View File

@@ -5,7 +5,7 @@ import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/sel
import { isFilterableEntityIdentifier } from 'features/controlLayers/store/types';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiShootingStarBold } from 'react-icons/pi';
import { PiShootingStarFill } from 'react-icons/pi';
export const EntityListSelectedEntityActionBarFilterButton = memo(() => {
const { t } = useTranslation();
@@ -24,12 +24,12 @@ export const EntityListSelectedEntityActionBarFilterButton = memo(() => {
<IconButton
onClick={filter.start}
isDisabled={filter.isDisabled}
size="sm"
minW={8}
variant="link"
alignSelf="stretch"
aria-label={t('controlLayers.filter.filter')}
tooltip={t('controlLayers.filter.filter')}
icon={<PiShootingStarBold />}
icon={<PiShootingStarFill />}
/>
);
});

View File

@@ -31,7 +31,7 @@ export const EntityListSelectedEntityActionBarSaveToAssetsButton = memo(() => {
<IconButton
onClick={onClick}
isDisabled={!selectedEntityIdentifier || isBusy}
size="sm"
minW={8}
variant="link"
alignSelf="stretch"
aria-label={t('controlLayers.saveLayerToAssets')}

View File

@@ -0,0 +1,37 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useEntitySegmentAnything } from 'features/controlLayers/hooks/useEntitySegmentAnything';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { isSegmentableEntityIdentifier } from 'features/controlLayers/store/types';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiShapesFill } from 'react-icons/pi';
export const EntityListSelectedEntityActionBarSelectObjectButton = memo(() => {
const { t } = useTranslation();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const segment = useEntitySegmentAnything(selectedEntityIdentifier);
if (!selectedEntityIdentifier) {
return null;
}
if (!isSegmentableEntityIdentifier(selectedEntityIdentifier)) {
return null;
}
return (
<IconButton
onClick={segment.start}
isDisabled={segment.isDisabled}
minW={8}
variant="link"
alignSelf="stretch"
aria-label={t('controlLayers.selectObject.selectObject')}
tooltip={t('controlLayers.selectObject.selectObject')}
icon={<PiShapesFill />}
/>
);
});
EntityListSelectedEntityActionBarSelectObjectButton.displayName = 'EntityListSelectedEntityActionBarSelectObjectButton';

View File

@@ -24,7 +24,7 @@ export const EntityListSelectedEntityActionBarTransformButton = memo(() => {
<IconButton
onClick={transform.start}
isDisabled={transform.isDisabled}
size="sm"
minW={8}
variant="link"
alignSelf="stretch"
aria-label={t('controlLayers.transform.transform')}

View File

@@ -10,7 +10,7 @@ import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea
import { Filter } from 'features/controlLayers/components/Filters/Filter';
import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
import { SegmentAnything } from 'features/controlLayers/components/SegmentAnything/SegmentAnything';
import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
import { StagingAreaIsStagingGate } from 'features/controlLayers/components/StagingArea/StagingAreaIsStagingGate';
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
@@ -25,8 +25,8 @@ const MenuContent = () => {
return (
<CanvasManagerProviderGate>
<MenuList>
<CanvasContextMenuGlobalMenuItems />
<CanvasContextMenuSelectedEntityMenuItems />
<CanvasContextMenuGlobalMenuItems />
</MenuList>
</CanvasManagerProviderGate>
);
@@ -71,12 +71,16 @@ export const CanvasMainPanelContent = memo(() => {
>
<InvokeCanvasComponent />
<CanvasManagerProviderGate>
{showHUD && (
<Flex position="absolute" top={1} insetInlineStart={1} pointerEvents="none">
<CanvasHUD />
</Flex>
)}
<Flex flexDir="column" position="absolute" top={1} insetInlineEnd={1} pointerEvents="none" gap={2}>
<Flex
position="absolute"
flexDir="column"
top={1}
insetInlineStart={1}
pointerEvents="none"
gap={2}
alignItems="flex-start"
>
{showHUD && <CanvasHUD />}
<CanvasAlertsSelectedEntityStatus />
<CanvasAlertsPreserveMask />
<CanvasAlertsSendingToGallery />
@@ -102,7 +106,7 @@ export const CanvasMainPanelContent = memo(() => {
<CanvasManagerProviderGate>
<Filter />
<Transform />
<SegmentAnything />
<SelectObject />
</CanvasManagerProviderGate>
</Flex>
<CanvasDropArea />

View File

@@ -21,7 +21,7 @@ import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/s
import type { CanvasEntityIdentifier, ControlModeV2 } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiBoundingBoxBold, PiShootingStarBold, PiUploadBold } from 'react-icons/pi';
import { PiBoundingBoxBold, PiShootingStarFill, PiUploadBold } from 'react-icons/pi';
import type { ControlNetModelConfig, PostUploadAction, T2IAdapterModelConfig } from 'services/api/types';
const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) => {
@@ -93,7 +93,7 @@ export const ControlLayerControlAdapter = memo(() => {
variant="link"
aria-label={t('controlLayers.filter.filter')}
tooltip={t('controlLayers.filter.filter')}
icon={<PiShootingStarBold />}
icon={<PiShootingStarFill />}
/>
<IconButton
onClick={pullBboxIntoLayer}

View File

@@ -1,15 +1,16 @@
import { MenuDivider } from '@invoke-ai/ui-library';
import { IconMenuItemGroup } from 'common/components/IconMenuItem';
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown';
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
import { CanvasEntityMenuItemsSegment } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSegment';
import { CanvasEntityMenuItemsSelectObject } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSelectObject';
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
import { ControlLayerMenuItemsConvertControlToRaster } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItemsConvertControlToRaster';
import { ControlLayerMenuItemsConvertToSubMenu } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItemsConvertToSubMenu';
import { ControlLayerMenuItemsCopyToSubMenu } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItemsCopyToSubMenu';
import { ControlLayerMenuItemsTransparencyEffect } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect';
import { memo } from 'react';
@@ -24,12 +25,13 @@ export const ControlLayerMenuItems = memo(() => {
<MenuDivider />
<CanvasEntityMenuItemsTransform />
<CanvasEntityMenuItemsFilter />
<CanvasEntityMenuItemsSegment />
<ControlLayerMenuItemsConvertControlToRaster />
<CanvasEntityMenuItemsSelectObject />
<ControlLayerMenuItemsTransparencyEffect />
<MenuDivider />
<CanvasEntityMenuItemsMergeDown />
<ControlLayerMenuItemsCopyToSubMenu />
<ControlLayerMenuItemsConvertToSubMenu />
<CanvasEntityMenuItemsCropToBbox />
<CanvasEntityMenuItemsCopyToClipboard />
<CanvasEntityMenuItemsSave />
</>
);

View File

@@ -1,27 +0,0 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { controlLayerConvertedToRasterLayer } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiLightningBold } from 'react-icons/pi';
export const ControlLayerMenuItemsConvertControlToRaster = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('control_layer');
const isInteractable = useIsEntityInteractable(entityIdentifier);
const convertControlLayerToRasterLayer = useCallback(() => {
dispatch(controlLayerConvertedToRasterLayer({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem onClick={convertControlLayerToRasterLayer} icon={<PiLightningBold />} isDisabled={!isInteractable}>
{t('controlLayers.convertToRasterLayer')}
</MenuItem>
);
});
ControlLayerMenuItemsConvertControlToRaster.displayName = 'ControlLayerMenuItemsConvertControlToRaster';

View File

@@ -0,0 +1,58 @@
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import {
controlLayerConvertedToInpaintMask,
controlLayerConvertedToRasterLayer,
controlLayerConvertedToRegionalGuidance,
} from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiSwapBold } from 'react-icons/pi';
export const ControlLayerMenuItemsConvertToSubMenu = memo(() => {
const { t } = useTranslation();
const subMenu = useSubMenu();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('control_layer');
const isBusy = useCanvasIsBusy();
const isLocked = useEntityIsLocked(entityIdentifier);
const convertToInpaintMask = useCallback(() => {
dispatch(controlLayerConvertedToInpaintMask({ entityIdentifier, replace: true }));
}, [dispatch, entityIdentifier]);
const convertToRegionalGuidance = useCallback(() => {
dispatch(controlLayerConvertedToRegionalGuidance({ entityIdentifier, replace: true }));
}, [dispatch, entityIdentifier]);
const convertToRasterLayer = useCallback(() => {
dispatch(controlLayerConvertedToRasterLayer({ entityIdentifier, replace: true }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.convertControlLayerTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
{t('controlLayers.inpaintMask')}
</MenuItem>
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
{t('controlLayers.regionalGuidance')}
</MenuItem>
<MenuItem onClick={convertToRasterLayer} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
{t('controlLayers.rasterLayer')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
);
});
ControlLayerMenuItemsConvertToSubMenu.displayName = 'ControlLayerMenuItemsConvertToSubMenu';

View File

@@ -0,0 +1,58 @@
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import {
controlLayerConvertedToInpaintMask,
controlLayerConvertedToRasterLayer,
controlLayerConvertedToRegionalGuidance,
} from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCopyBold } from 'react-icons/pi';
export const ControlLayerMenuItemsCopyToSubMenu = memo(() => {
const { t } = useTranslation();
const subMenu = useSubMenu();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('control_layer');
const isBusy = useCanvasIsBusy();
const copyToInpaintMask = useCallback(() => {
dispatch(controlLayerConvertedToInpaintMask({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const copyToRegionalGuidance = useCallback(() => {
dispatch(controlLayerConvertedToRegionalGuidance({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const copyToRasterLayer = useCallback(() => {
dispatch(controlLayerConvertedToRasterLayer({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />} isDisabled={isBusy}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.copyControlLayerTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<CanvasEntityMenuItemsCopyToClipboard />
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={isBusy}>
{t('controlLayers.newInpaintMask')}
</MenuItem>
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={isBusy}>
{t('controlLayers.newRegionalGuidance')}
</MenuItem>
<MenuItem onClick={copyToRasterLayer} icon={<PiCopyBold />} isDisabled={isBusy}>
{t('controlLayers.newRasterLayer')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
);
});
ControlLayerMenuItemsCopyToSubMenu.displayName = 'ControlLayerMenuItemsCopyToSubMenu';

View File

@@ -2,7 +2,7 @@ import { MenuItem } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import { controlLayerWithTransparencyEffectToggled } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { memo, useCallback, useMemo } from 'react';
@@ -13,7 +13,7 @@ export const ControlLayerMenuItemsTransparencyEffect = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('control_layer');
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isLocked = useEntityIsLocked(entityIdentifier);
const selectWithTransparencyEffect = useMemo(
() =>
createSelector(selectCanvasSlice, (canvas) => {
@@ -28,7 +28,7 @@ export const ControlLayerMenuItemsTransparencyEffect = memo(() => {
}, [dispatch, entityIdentifier]);
return (
<MenuItem onClick={onToggle} icon={<PiDropHalfBold />} isDisabled={!isInteractable}>
<MenuItem onClick={onToggle} icon={<PiDropHalfBold />} isDisabled={isLocked}>
{withTransparencyEffect
? t('controlLayers.disableTransparencyEffect')
: t('controlLayers.enableTransparencyEffect')}

View File

@@ -1,4 +1,15 @@
import { Button, ButtonGroup, Flex, Heading, Spacer } from '@invoke-ai/ui-library';
import {
Button,
ButtonGroup,
Flex,
Heading,
Menu,
MenuButton,
MenuItem,
MenuList,
Spacer,
Spinner,
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
@@ -15,7 +26,7 @@ import { IMAGE_FILTERS } from 'features/controlLayers/store/filters';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowsCounterClockwiseBold, PiCheckBold, PiShootingStarBold, PiXBold } from 'react-icons/pi';
import { PiCaretDownBold } from 'react-icons/pi';
const FilterContent = memo(
({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
@@ -25,7 +36,7 @@ const FilterContent = memo(
const config = useStore(adapter.filterer.$filterConfig);
const isCanvasFocused = useIsRegionFocused('canvas');
const isProcessing = useStore(adapter.filterer.$isProcessing);
const hasProcessed = useStore(adapter.filterer.$hasProcessed);
const hasImageState = useStore(adapter.filterer.$hasImageState);
const autoProcess = useAppSelector(selectAutoProcess);
const onChangeFilterConfig = useCallback(
@@ -46,6 +57,22 @@ const FilterContent = memo(
return IMAGE_FILTERS[config.type].validateConfig?.(config as never) ?? true;
}, [config]);
const saveAsInpaintMask = useCallback(() => {
adapter.filterer.saveAs('inpaint_mask');
}, [adapter.filterer]);
const saveAsRegionalGuidance = useCallback(() => {
adapter.filterer.saveAs('regional_guidance');
}, [adapter.filterer]);
const saveAsRasterLayer = useCallback(() => {
adapter.filterer.saveAs('raster_layer');
}, [adapter.filterer]);
const saveAsControlLayer = useCallback(() => {
adapter.filterer.saveAs('control_layer');
}, [adapter.filterer]);
useRegisteredHotkeys({
id: 'applyFilter',
category: 'canvas',
@@ -89,40 +116,56 @@ const FilterContent = memo(
<ButtonGroup isAttached={false} size="sm" w="full">
<Button
variant="ghost"
leftIcon={<PiShootingStarBold />}
onClick={adapter.filterer.processImmediate}
isLoading={isProcessing}
loadingText={t('controlLayers.filter.process')}
isDisabled={!isValid || autoProcess}
isDisabled={isProcessing || !isValid || (autoProcess && hasImageState)}
>
{t('controlLayers.filter.process')}
{isProcessing && <Spinner ms={3} boxSize={5} color="base.600" />}
</Button>
<Spacer />
<Button
leftIcon={<PiArrowsCounterClockwiseBold />}
onClick={adapter.filterer.reset}
isLoading={isProcessing}
isDisabled={isProcessing}
loadingText={t('controlLayers.filter.reset')}
variant="ghost"
>
{t('controlLayers.filter.reset')}
</Button>
<Button
variant="ghost"
leftIcon={<PiCheckBold />}
onClick={adapter.filterer.apply}
isLoading={isProcessing}
loadingText={t('controlLayers.filter.apply')}
isDisabled={!isValid || !hasProcessed}
variant="ghost"
isDisabled={isProcessing || !isValid || !hasImageState}
>
{t('controlLayers.filter.apply')}
</Button>
<Button
variant="ghost"
leftIcon={<PiXBold />}
onClick={adapter.filterer.cancel}
loadingText={t('controlLayers.filter.cancel')}
>
<Menu>
<MenuButton
as={Button}
loadingText={t('controlLayers.selectObject.saveAs')}
variant="ghost"
isDisabled={isProcessing || !isValid || !hasImageState}
rightIcon={<PiCaretDownBold />}
>
{t('controlLayers.selectObject.saveAs')}
</MenuButton>
<MenuList>
<MenuItem isDisabled={isProcessing || !isValid || !hasImageState} onClick={saveAsInpaintMask}>
{t('controlLayers.newInpaintMask')}
</MenuItem>
<MenuItem isDisabled={isProcessing || !isValid || !hasImageState} onClick={saveAsRegionalGuidance}>
{t('controlLayers.newRegionalGuidance')}
</MenuItem>
<MenuItem isDisabled={isProcessing || !isValid || !hasImageState} onClick={saveAsControlLayer}>
{t('controlLayers.newControlLayer')}
</MenuItem>
<MenuItem isDisabled={isProcessing || !isValid || !hasImageState} onClick={saveAsRasterLayer}>
{t('controlLayers.newRasterLayer')}
</MenuItem>
</MenuList>
</Menu>
<Button variant="ghost" onClick={adapter.filterer.cancel} loadingText={t('controlLayers.filter.cancel')}>
{t('controlLayers.filter.cancel')}
</Button>
</ButtonGroup>

View File

@@ -0,0 +1,22 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { usePullBboxIntoGlobalReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiBoundingBoxBold } from 'react-icons/pi';
export const IPAdapterMenuItemPullBbox = memo(() => {
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext('reference_image');
const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(entityIdentifier);
const isBusy = useCanvasIsBusy();
return (
<MenuItem onClick={pullBboxIntoIPAdapter} icon={<PiBoundingBoxBold />} isDisabled={isBusy}>
{t('controlLayers.pullBboxIntoReferenceImage')}
</MenuItem>
);
});
IPAdapterMenuItemPullBbox.displayName = 'IPAdapterMenuItemPullBbox';

View File

@@ -1,16 +1,22 @@
import { MenuDivider } from '@invoke-ai/ui-library';
import { IconMenuItemGroup } from 'common/components/IconMenuItem';
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
import { IPAdapterMenuItemPullBbox } from 'features/controlLayers/components/IPAdapter/IPAdapterMenuItemPullBbox';
import { memo } from 'react';
export const IPAdapterMenuItems = memo(() => {
return (
<IconMenuItemGroup>
<CanvasEntityMenuItemsArrange />
<CanvasEntityMenuItemsDuplicate />
<CanvasEntityMenuItemsDelete asIcon />
</IconMenuItemGroup>
<>
<IconMenuItemGroup>
<CanvasEntityMenuItemsArrange />
<CanvasEntityMenuItemsDuplicate />
<CanvasEntityMenuItemsDelete asIcon />
</IconMenuItemGroup>
<MenuDivider />
<IPAdapterMenuItemPullBbox />
</>
);
});

View File

@@ -14,7 +14,7 @@ type Props = {
};
export const InpaintMask = memo(({ id }: Props) => {
const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'inpaint_mask' }), [id]);
const entityIdentifier = useMemo<CanvasEntityIdentifier<'inpaint_mask'>>(() => ({ id, type: 'inpaint_mask' }), [id]);
return (
<EntityIdentifierContext.Provider value={entityIdentifier}>

View File

@@ -4,7 +4,11 @@ import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown';
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
import { InpaintMaskMenuItemsConvertToSubMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsConvertToSubMenu';
import { InpaintMaskMenuItemsCopyToSubMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsCopyToSubMenu';
import { memo } from 'react';
export const InpaintMaskMenuItems = memo(() => {
@@ -18,7 +22,11 @@ export const InpaintMaskMenuItems = memo(() => {
<MenuDivider />
<CanvasEntityMenuItemsTransform />
<MenuDivider />
<CanvasEntityMenuItemsMergeDown />
<InpaintMaskMenuItemsCopyToSubMenu />
<InpaintMaskMenuItemsConvertToSubMenu />
<CanvasEntityMenuItemsCropToBbox />
<CanvasEntityMenuItemsSave />
</>
);
});

View File

@@ -0,0 +1,40 @@
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import { inpaintMaskConvertedToRegionalGuidance } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiSwapBold } from 'react-icons/pi';
export const InpaintMaskMenuItemsConvertToSubMenu = memo(() => {
const { t } = useTranslation();
const subMenu = useSubMenu();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('inpaint_mask');
const isBusy = useCanvasIsBusy();
const isLocked = useEntityIsLocked(entityIdentifier);
const convertToRegionalGuidance = useCallback(() => {
dispatch(inpaintMaskConvertedToRegionalGuidance({ entityIdentifier, replace: true }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.convertInpaintMaskTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
{t('controlLayers.regionalGuidance')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
);
});
InpaintMaskMenuItemsConvertToSubMenu.displayName = 'InpaintMaskMenuItemsConvertToSubMenu';

View File

@@ -0,0 +1,40 @@
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { inpaintMaskConvertedToRegionalGuidance } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCopyBold } from 'react-icons/pi';
export const InpaintMaskMenuItemsCopyToSubMenu = memo(() => {
const { t } = useTranslation();
const subMenu = useSubMenu();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('inpaint_mask');
const isBusy = useCanvasIsBusy();
const copyToRegionalGuidance = useCallback(() => {
dispatch(inpaintMaskConvertedToRegionalGuidance({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />} isDisabled={isBusy}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.copyInpaintMaskTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<CanvasEntityMenuItemsCopyToClipboard />
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={isBusy}>
{t('controlLayers.newRegionalGuidance')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
);
});
InpaintMaskMenuItemsCopyToSubMenu.displayName = 'InpaintMaskMenuItemsCopyToSubMenu';

View File

@@ -1,15 +1,16 @@
import { MenuDivider } from '@invoke-ai/ui-library';
import { IconMenuItemGroup } from 'common/components/IconMenuItem';
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown';
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
import { CanvasEntityMenuItemsSegment } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSegment';
import { CanvasEntityMenuItemsSelectObject } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSelectObject';
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
import { RasterLayerMenuItemsConvertRasterToControl } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsConvertRasterToControl';
import { RasterLayerMenuItemsConvertToSubMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsConvertToSubMenu';
import { RasterLayerMenuItemsCopyToSubMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCopyToSubMenu';
import { memo } from 'react';
export const RasterLayerMenuItems = memo(() => {
@@ -23,11 +24,12 @@ export const RasterLayerMenuItems = memo(() => {
<MenuDivider />
<CanvasEntityMenuItemsTransform />
<CanvasEntityMenuItemsFilter />
<CanvasEntityMenuItemsSegment />
<RasterLayerMenuItemsConvertRasterToControl />
<CanvasEntityMenuItemsSelectObject />
<MenuDivider />
<CanvasEntityMenuItemsMergeDown />
<RasterLayerMenuItemsCopyToSubMenu />
<RasterLayerMenuItemsConvertToSubMenu />
<CanvasEntityMenuItemsCropToBbox />
<CanvasEntityMenuItemsCopyToClipboard />
<CanvasEntityMenuItemsSave />
</>
);

View File

@@ -1,36 +0,0 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectDefaultControlAdapter } from 'features/controlLayers/hooks/addLayerHooks';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { rasterLayerConvertedToControlLayer } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiLightningBold } from 'react-icons/pi';
export const RasterLayerMenuItemsConvertRasterToControl = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('raster_layer');
const defaultControlAdapter = useAppSelector(selectDefaultControlAdapter);
const isInteractable = useIsEntityInteractable(entityIdentifier);
const onClick = useCallback(() => {
dispatch(
rasterLayerConvertedToControlLayer({
entityIdentifier,
overrides: {
controlAdapter: defaultControlAdapter,
},
})
);
}, [defaultControlAdapter, dispatch, entityIdentifier]);
return (
<MenuItem onClick={onClick} icon={<PiLightningBold />} isDisabled={!isInteractable}>
{t('controlLayers.convertToControlLayer')}
</MenuItem>
);
});
RasterLayerMenuItemsConvertRasterToControl.displayName = 'RasterLayerMenuItemsConvertRasterToControl';

View File

@@ -0,0 +1,67 @@
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectDefaultControlAdapter } from 'features/controlLayers/hooks/addLayerHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import {
rasterLayerConvertedToControlLayer,
rasterLayerConvertedToInpaintMask,
rasterLayerConvertedToRegionalGuidance,
} from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiSwapBold } from 'react-icons/pi';
export const RasterLayerMenuItemsConvertToSubMenu = memo(() => {
const { t } = useTranslation();
const subMenu = useSubMenu();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('raster_layer');
const defaultControlAdapter = useAppSelector(selectDefaultControlAdapter);
const isBusy = useCanvasIsBusy();
const isLocked = useEntityIsLocked(entityIdentifier);
const convertToInpaintMask = useCallback(() => {
dispatch(rasterLayerConvertedToInpaintMask({ entityIdentifier, replace: true }));
}, [dispatch, entityIdentifier]);
const convertToRegionalGuidance = useCallback(() => {
dispatch(rasterLayerConvertedToRegionalGuidance({ entityIdentifier, replace: true }));
}, [dispatch, entityIdentifier]);
const convertToControlLayer = useCallback(() => {
dispatch(
rasterLayerConvertedToControlLayer({
entityIdentifier,
replace: true,
overrides: { controlAdapter: defaultControlAdapter },
})
);
}, [defaultControlAdapter, dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.convertRasterLayerTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
{t('controlLayers.inpaintMask')}
</MenuItem>
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
{t('controlLayers.regionalGuidance')}
</MenuItem>
<MenuItem onClick={convertToControlLayer} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
{t('controlLayers.controlLayer')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
);
});
RasterLayerMenuItemsConvertToSubMenu.displayName = 'RasterLayerMenuItemsConvertToSubMenu';

View File

@@ -0,0 +1,66 @@
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectDefaultControlAdapter } from 'features/controlLayers/hooks/addLayerHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import {
rasterLayerConvertedToControlLayer,
rasterLayerConvertedToInpaintMask,
rasterLayerConvertedToRegionalGuidance,
} from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCopyBold } from 'react-icons/pi';
export const RasterLayerMenuItemsCopyToSubMenu = memo(() => {
const { t } = useTranslation();
const subMenu = useSubMenu();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('raster_layer');
const defaultControlAdapter = useAppSelector(selectDefaultControlAdapter);
const isBusy = useCanvasIsBusy();
const copyToInpaintMask = useCallback(() => {
dispatch(rasterLayerConvertedToInpaintMask({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const copyToRegionalGuidance = useCallback(() => {
dispatch(rasterLayerConvertedToRegionalGuidance({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const copyToControlLayer = useCallback(() => {
dispatch(
rasterLayerConvertedToControlLayer({
entityIdentifier,
overrides: { controlAdapter: defaultControlAdapter },
})
);
}, [defaultControlAdapter, dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />} isDisabled={isBusy}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.copyRasterLayerTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<CanvasEntityMenuItemsCopyToClipboard />
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={isBusy}>
{t('controlLayers.newInpaintMask')}
</MenuItem>
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={isBusy}>
{t('controlLayers.newRegionalGuidance')}
</MenuItem>
<MenuItem onClick={copyToControlLayer} icon={<PiCopyBold />} isDisabled={isBusy}>
{t('controlLayers.newControlLayer')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
);
});
RasterLayerMenuItemsCopyToSubMenu.displayName = 'RasterLayerMenuItemsCopyToSubMenu';

View File

@@ -16,7 +16,10 @@ type Props = {
};
export const RegionalGuidance = memo(({ id }: Props) => {
const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'regional_guidance' }), [id]);
const entityIdentifier = useMemo<CanvasEntityIdentifier<'regional_guidance'>>(
() => ({ id, type: 'regional_guidance' }),
[id]
);
return (
<EntityIdentifierContext.Provider value={entityIdentifier}>

View File

@@ -1,28 +1,37 @@
import { Flex, MenuDivider } from '@invoke-ai/ui-library';
import { MenuDivider } from '@invoke-ai/ui-library';
import { IconMenuItemGroup } from 'common/components/IconMenuItem';
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown';
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
import { RegionalGuidanceMenuItemsAddPromptsAndIPAdapter } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter';
import { RegionalGuidanceMenuItemsAutoNegative } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative';
import { RegionalGuidanceMenuItemsConvertToSubMenu } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsConvertToSubMenu';
import { RegionalGuidanceMenuItemsCopyToSubMenu } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsCopyToSubMenu';
import { memo } from 'react';
export const RegionalGuidanceMenuItems = memo(() => {
return (
<>
<Flex gap={2}>
<IconMenuItemGroup>
<CanvasEntityMenuItemsArrange />
<CanvasEntityMenuItemsDuplicate />
<CanvasEntityMenuItemsDelete asIcon />
</Flex>
</IconMenuItemGroup>
<MenuDivider />
<RegionalGuidanceMenuItemsAddPromptsAndIPAdapter />
<MenuDivider />
<CanvasEntityMenuItemsTransform />
<RegionalGuidanceMenuItemsAutoNegative />
<MenuDivider />
<CanvasEntityMenuItemsMergeDown />
<RegionalGuidanceMenuItemsCopyToSubMenu />
<RegionalGuidanceMenuItemsConvertToSubMenu />
<CanvasEntityMenuItemsCropToBbox />
<CanvasEntityMenuItemsSave />
</>
);
});

View File

@@ -0,0 +1,40 @@
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import { rgConvertedToInpaintMask } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiSwapBold } from 'react-icons/pi';
export const RegionalGuidanceMenuItemsConvertToSubMenu = memo(() => {
const { t } = useTranslation();
const subMenu = useSubMenu();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const isBusy = useCanvasIsBusy();
const isLocked = useEntityIsLocked(entityIdentifier);
const convertToInpaintMask = useCallback(() => {
dispatch(rgConvertedToInpaintMask({ entityIdentifier, replace: true }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.convertRegionalGuidanceTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
{t('controlLayers.inpaintMask')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
);
});
RegionalGuidanceMenuItemsConvertToSubMenu.displayName = 'RegionalGuidanceMenuItemsConvertToSubMenu';

View File

@@ -0,0 +1,40 @@
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { rgConvertedToInpaintMask } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCopyBold } from 'react-icons/pi';
export const RegionalGuidanceMenuItemsCopyToSubMenu = memo(() => {
const { t } = useTranslation();
const subMenu = useSubMenu();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const isBusy = useCanvasIsBusy();
const copyToInpaintMask = useCallback(() => {
dispatch(rgConvertedToInpaintMask({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />} isDisabled={isBusy}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.copyRegionalGuidanceTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<CanvasEntityMenuItemsCopyToClipboard />
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={isBusy}>
{t('controlLayers.newInpaintMask')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
);
});
RegionalGuidanceMenuItemsCopyToSubMenu.displayName = 'RegionalGuidanceMenuItemsCopyToSubMenu';

View File

@@ -1,124 +0,0 @@
import { Button, ButtonGroup, Flex, Heading, Spacer } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
import { CanvasAutoProcessSwitch } from 'features/controlLayers/components/CanvasAutoProcessSwitch';
import { CanvasOperationIsolatedLayerPreviewSwitch } from 'features/controlLayers/components/CanvasOperationIsolatedLayerPreviewSwitch';
import { SegmentAnythingPointType } from 'features/controlLayers/components/SegmentAnything/SegmentAnythingPointType';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer';
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
import { selectAutoProcess } from 'features/controlLayers/store/canvasSettingsSlice';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowsCounterClockwiseBold, PiCheckBold, PiStarBold, PiXBold } from 'react-icons/pi';
const SegmentAnythingContent = memo(
({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
const { t } = useTranslation();
const ref = useRef<HTMLDivElement>(null);
useFocusRegion('canvas', ref, { focusOnMount: true });
const isCanvasFocused = useIsRegionFocused('canvas');
const isProcessing = useStore(adapter.segmentAnything.$isProcessing);
const hasPoints = useStore(adapter.segmentAnything.$hasPoints);
const autoProcess = useAppSelector(selectAutoProcess);
useRegisteredHotkeys({
id: 'applySegmentAnything',
category: 'canvas',
callback: adapter.segmentAnything.apply,
options: { enabled: !isProcessing && isCanvasFocused },
dependencies: [adapter.segmentAnything, isProcessing, isCanvasFocused],
});
useRegisteredHotkeys({
id: 'cancelSegmentAnything',
category: 'canvas',
callback: adapter.segmentAnything.cancel,
options: { enabled: !isProcessing && isCanvasFocused },
dependencies: [adapter.segmentAnything, isProcessing, isCanvasFocused],
});
return (
<Flex
ref={ref}
bg="base.800"
borderRadius="base"
p={4}
flexDir="column"
gap={4}
minW={420}
h="auto"
shadow="dark-lg"
transitionProperty="height"
transitionDuration="normal"
>
<Flex w="full" gap={4}>
<Heading size="md" color="base.300" userSelect="none">
{t('controlLayers.segment.autoMask')}
</Heading>
<Spacer />
<CanvasAutoProcessSwitch />
<CanvasOperationIsolatedLayerPreviewSwitch />
</Flex>
<SegmentAnythingPointType adapter={adapter} />
<ButtonGroup isAttached={false} size="sm" w="full">
<Button
leftIcon={<PiStarBold />}
onClick={adapter.segmentAnything.processImmediate}
isLoading={isProcessing}
loadingText={t('controlLayers.segment.process')}
variant="ghost"
isDisabled={!hasPoints || autoProcess}
>
{t('controlLayers.segment.process')}
</Button>
<Spacer />
<Button
leftIcon={<PiArrowsCounterClockwiseBold />}
onClick={adapter.segmentAnything.reset}
isLoading={isProcessing}
loadingText={t('controlLayers.segment.reset')}
variant="ghost"
>
{t('controlLayers.segment.reset')}
</Button>
<Button
leftIcon={<PiCheckBold />}
onClick={adapter.segmentAnything.apply}
isLoading={isProcessing}
loadingText={t('controlLayers.segment.apply')}
variant="ghost"
>
{t('controlLayers.segment.apply')}
</Button>
<Button
leftIcon={<PiXBold />}
onClick={adapter.segmentAnything.cancel}
isLoading={isProcessing}
loadingText={t('common.cancel')}
variant="ghost"
>
{t('controlLayers.segment.cancel')}
</Button>
</ButtonGroup>
</Flex>
);
}
);
SegmentAnythingContent.displayName = 'SegmentAnythingContent';
export const SegmentAnything = () => {
const canvasManager = useCanvasManager();
const adapter = useStore(canvasManager.stateApi.$segmentingAdapter);
if (!adapter) {
return null;
}
return <SegmentAnythingContent adapter={adapter} />;
};

View File

@@ -0,0 +1,223 @@
import {
Button,
ButtonGroup,
Flex,
Heading,
Icon,
ListItem,
Menu,
MenuButton,
MenuItem,
MenuList,
Spacer,
Spinner,
Text,
Tooltip,
UnorderedList,
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
import { CanvasAutoProcessSwitch } from 'features/controlLayers/components/CanvasAutoProcessSwitch';
import { CanvasOperationIsolatedLayerPreviewSwitch } from 'features/controlLayers/components/CanvasOperationIsolatedLayerPreviewSwitch';
import { SelectObjectInvert } from 'features/controlLayers/components/SelectObject/SelectObjectInvert';
import { SelectObjectPointType } from 'features/controlLayers/components/SelectObject/SelectObjectPointType';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer';
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
import { selectAutoProcess } from 'features/controlLayers/store/canvasSettingsSlice';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import type { PropsWithChildren } from 'react';
import { memo, useCallback, useRef } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { PiCaretDownBold, PiInfoBold } from 'react-icons/pi';
const SelectObjectContent = memo(
({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
const { t } = useTranslation();
const ref = useRef<HTMLDivElement>(null);
useFocusRegion('canvas', ref, { focusOnMount: true });
const isCanvasFocused = useIsRegionFocused('canvas');
const isProcessing = useStore(adapter.segmentAnything.$isProcessing);
const hasPoints = useStore(adapter.segmentAnything.$hasPoints);
const hasImageState = useStore(adapter.segmentAnything.$hasImageState);
const autoProcess = useAppSelector(selectAutoProcess);
const saveAsInpaintMask = useCallback(() => {
adapter.segmentAnything.saveAs('inpaint_mask');
}, [adapter.segmentAnything]);
const saveAsRegionalGuidance = useCallback(() => {
adapter.segmentAnything.saveAs('regional_guidance');
}, [adapter.segmentAnything]);
const saveAsRasterLayer = useCallback(() => {
adapter.segmentAnything.saveAs('raster_layer');
}, [adapter.segmentAnything]);
const saveAsControlLayer = useCallback(() => {
adapter.segmentAnything.saveAs('control_layer');
}, [adapter.segmentAnything]);
useRegisteredHotkeys({
id: 'applySegmentAnything',
category: 'canvas',
callback: adapter.segmentAnything.apply,
options: { enabled: !isProcessing && isCanvasFocused },
dependencies: [adapter.segmentAnything, isProcessing, isCanvasFocused],
});
useRegisteredHotkeys({
id: 'cancelSegmentAnything',
category: 'canvas',
callback: adapter.segmentAnything.cancel,
options: { enabled: !isProcessing && isCanvasFocused },
dependencies: [adapter.segmentAnything, isProcessing, isCanvasFocused],
});
return (
<Flex
ref={ref}
bg="base.800"
borderRadius="base"
p={4}
flexDir="column"
gap={4}
minW={420}
h="auto"
shadow="dark-lg"
transitionProperty="height"
transitionDuration="normal"
>
<Flex w="full" gap={4} alignItems="center">
<Flex gap={2}>
<Heading size="md" color="base.300" userSelect="none">
{t('controlLayers.selectObject.selectObject')}
</Heading>
<Tooltip label={<SelectObjectHelpTooltipContent />}>
<Flex alignItems="center">
<Icon as={PiInfoBold} color="base.500" />
</Flex>
</Tooltip>
</Flex>
<Spacer />
<CanvasAutoProcessSwitch />
<CanvasOperationIsolatedLayerPreviewSwitch />
</Flex>
<Flex w="full" justifyContent="space-between" py={2}>
<SelectObjectPointType adapter={adapter} />
<SelectObjectInvert adapter={adapter} />
</Flex>
<ButtonGroup isAttached={false} size="sm" w="full">
<Button
onClick={adapter.segmentAnything.processImmediate}
loadingText={t('controlLayers.selectObject.process')}
variant="ghost"
isDisabled={isProcessing || !hasPoints || (autoProcess && hasImageState)}
>
{t('controlLayers.selectObject.process')}
{isProcessing && <Spinner ms={3} boxSize={5} color="base.600" />}
</Button>
<Spacer />
<Button
onClick={adapter.segmentAnything.reset}
isDisabled={isProcessing || !hasPoints}
loadingText={t('controlLayers.selectObject.reset')}
variant="ghost"
>
{t('controlLayers.selectObject.reset')}
</Button>
<Button
onClick={adapter.segmentAnything.apply}
loadingText={t('controlLayers.selectObject.apply')}
variant="ghost"
isDisabled={isProcessing || !hasImageState}
>
{t('controlLayers.selectObject.apply')}
</Button>
<Menu>
<MenuButton
as={Button}
loadingText={t('controlLayers.selectObject.saveAs')}
variant="ghost"
isDisabled={isProcessing || !hasImageState}
rightIcon={<PiCaretDownBold />}
>
{t('controlLayers.selectObject.saveAs')}
</MenuButton>
<MenuList>
<MenuItem isDisabled={isProcessing || !hasImageState} onClick={saveAsInpaintMask}>
{t('controlLayers.newInpaintMask')}
</MenuItem>
<MenuItem isDisabled={isProcessing || !hasImageState} onClick={saveAsRegionalGuidance}>
{t('controlLayers.newRegionalGuidance')}
</MenuItem>
<MenuItem isDisabled={isProcessing || !hasImageState} onClick={saveAsControlLayer}>
{t('controlLayers.newControlLayer')}
</MenuItem>
<MenuItem isDisabled={isProcessing || !hasImageState} onClick={saveAsRasterLayer}>
{t('controlLayers.newRasterLayer')}
</MenuItem>
</MenuList>
</Menu>
<Button
onClick={adapter.segmentAnything.cancel}
isDisabled={isProcessing}
loadingText={t('common.cancel')}
variant="ghost"
>
{t('controlLayers.selectObject.cancel')}
</Button>
</ButtonGroup>
</Flex>
);
}
);
SelectObjectContent.displayName = 'SegmentAnythingContent';
export const SelectObject = memo(() => {
const canvasManager = useCanvasManager();
const adapter = useStore(canvasManager.stateApi.$segmentingAdapter);
if (!adapter) {
return null;
}
return <SelectObjectContent adapter={adapter} />;
});
SelectObject.displayName = 'SelectObject';
const Bold = (props: PropsWithChildren) => (
<Text as="span" fontWeight="semibold">
{props.children}
</Text>
);
const SelectObjectHelpTooltipContent = memo(() => {
const { t } = useTranslation();
return (
<Flex gap={3} flexDir="column">
<Text>
<Trans i18nKey="controlLayers.selectObject.help1" components={{ Bold: <Bold /> }} />
</Text>
<Text>
<Trans i18nKey="controlLayers.selectObject.help2" components={{ Bold: <Bold /> }} />
</Text>
<Text>
<Trans i18nKey="controlLayers.selectObject.help3" />
</Text>
<UnorderedList>
<ListItem>{t('controlLayers.selectObject.clickToAdd')}</ListItem>
<ListItem>{t('controlLayers.selectObject.dragToMove')}</ListItem>
<ListItem>{t('controlLayers.selectObject.clickToRemove')}</ListItem>
</UnorderedList>
</Flex>
);
});
SelectObjectHelpTooltipContent.displayName = 'SelectObjectHelpTooltipContent';

View File

@@ -0,0 +1,26 @@
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer';
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const SelectObjectInvert = memo(
({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
const { t } = useTranslation();
const invert = useStore(adapter.segmentAnything.$invert);
const onChange = useCallback(() => {
adapter.segmentAnything.$invert.set(!adapter.segmentAnything.$invert.get());
}, [adapter.segmentAnything.$invert]);
return (
<FormControl w="min-content">
<FormLabel m={0}>{t('controlLayers.selectObject.invertSelection')}</FormLabel>
<Switch size="sm" isChecked={invert} onChange={onChange} />
</FormControl>
);
}
);
SelectObjectInvert.displayName = 'SelectObjectInvert';

View File

@@ -6,7 +6,7 @@ import { SAM_POINT_LABEL_STRING_TO_NUMBER, zSAMPointLabelString } from 'features
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const SegmentAnythingPointType = memo(
export const SelectObjectPointType = memo(
({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
const { t } = useTranslation();
const pointType = useStore(adapter.segmentAnything.$pointTypeString);
@@ -21,18 +21,15 @@ export const SegmentAnythingPointType = memo(
);
return (
<FormControl w="full">
<FormLabel>{t('controlLayers.segment.pointType')}</FormLabel>
<FormControl w="min-content">
<FormLabel m={0}>{t('controlLayers.selectObject.pointType')}</FormLabel>
<RadioGroup value={pointType} onChange={onChange} w="full" size="md">
<Flex alignItems="center" w="full" gap={4} fontWeight="semibold" color="base.300">
<Radio value="foreground">
<Text>{t('controlLayers.segment.foreground')}</Text>
<Text>{t('controlLayers.selectObject.include')}</Text>
</Radio>
<Radio value="background">
<Text>{t('controlLayers.segment.background')}</Text>
</Radio>
<Radio value="neutral">
<Text>{t('controlLayers.segment.neutral')}</Text>
<Text>{t('controlLayers.selectObject.exclude')}</Text>
</Radio>
</Flex>
</RadioGroup>
@@ -41,4 +38,4 @@ export const SegmentAnythingPointType = memo(
}
);
SegmentAnythingPointType.displayName = 'SegmentAnythingPointType';
SelectObjectPointType.displayName = 'SelectObject';

View File

@@ -1,4 +1,4 @@
import { Button, ButtonGroup, Flex, Heading, Spacer } from '@invoke-ai/ui-library';
import { Button, ButtonGroup, Flex, Heading, Spacer, Spinner } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
import { CanvasOperationIsolatedLayerPreviewSwitch } from 'features/controlLayers/components/CanvasOperationIsolatedLayerPreviewSwitch';
@@ -8,7 +8,6 @@ import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEnt
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowsCounterClockwiseBold, PiCheckBold, PiXBold } from 'react-icons/pi';
const TransformContent = memo(({ adapter }: { adapter: CanvasEntityAdapter }) => {
const { t } = useTranslation();
@@ -62,30 +61,28 @@ const TransformContent = memo(({ adapter }: { adapter: CanvasEntityAdapter }) =>
<TransformFitToBboxButtons adapter={adapter} />
<ButtonGroup isAttached={false} size="sm" w="full">
<ButtonGroup isAttached={false} size="sm" w="full" alignItems="center">
{isProcessing && <Spinner ms={3} boxSize={5} color="base.600" />}
<Spacer />
<Button
leftIcon={<PiArrowsCounterClockwiseBold />}
onClick={adapter.transformer.resetTransform}
isLoading={isProcessing}
isDisabled={isProcessing}
loadingText={t('controlLayers.transform.reset')}
variant="ghost"
>
{t('controlLayers.transform.reset')}
</Button>
<Button
leftIcon={<PiCheckBold />}
onClick={adapter.transformer.applyTransform}
isLoading={isProcessing}
isDisabled={isProcessing}
loadingText={t('controlLayers.transform.apply')}
variant="ghost"
>
{t('controlLayers.transform.apply')}
</Button>
<Button
leftIcon={<PiXBold />}
onClick={adapter.transformer.stopTransform}
isLoading={isProcessing}
isDisabled={isProcessing}
loadingText={t('common.cancel')}
variant="ghost"
>

View File

@@ -4,7 +4,6 @@ import { useStore } from '@nanostores/react';
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowsOutBold } from 'react-icons/pi';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
import { z } from 'zod';
@@ -60,10 +59,9 @@ export const TransformFitToBboxButtons = memo(({ adapter }: { adapter: CanvasEnt
<Combobox options={options} value={value} onChange={onChange} isSearchable={false} isClearable={false} />
</FormControl>
<Button
leftIcon={<PiArrowsOutBold />}
size="sm"
onClick={onClick}
isLoading={isProcessing}
isDisabled={isProcessing}
loadingText={t('controlLayers.transform.fitToBbox')}
variant="ghost"
>

View File

@@ -1,13 +1,15 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Button, Collapse, Flex, Icon, Spacer, Text } from '@invoke-ai/ui-library';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { useBoolean } from 'common/hooks/useBoolean';
import { CanvasEntityAddOfTypeButton } from 'features/controlLayers/components/common/CanvasEntityAddOfTypeButton';
import { CanvasEntityMergeVisibleButton } from 'features/controlLayers/components/common/CanvasEntityMergeVisibleButton';
import { CanvasEntityTypeIsHiddenToggle } from 'features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle';
import { useEntityTypeInformationalPopover } from 'features/controlLayers/hooks/useEntityTypeInformationalPopover';
import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { type CanvasEntityIdentifier, isRenderableEntityType } from 'features/controlLayers/store/types';
import type { PropsWithChildren } from 'react';
import { memo, useMemo } from 'react';
import { memo } from 'react';
import { PiCaretDownBold } from 'react-icons/pi';
type Props = PropsWithChildren<{
@@ -21,9 +23,8 @@ const _hover: SystemStyleObject = {
export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props) => {
const title = useEntityTypeTitle(type);
const informationalPopoverFeature = useEntityTypeInformationalPopover(type);
const collapse = useBoolean(true);
const canMergeVisible = useMemo(() => type === 'raster_layer' || type === 'inpaint_mask', [type]);
const canHideAll = useMemo(() => type !== 'reference_image', [type]);
return (
<Flex flexDir="column" w="full">
@@ -47,19 +48,34 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props
transitionProperty="common"
transitionDuration="fast"
/>
<Text
fontWeight="semibold"
color={isSelected ? 'base.200' : 'base.500'}
userSelect="none"
transitionProperty="common"
transitionDuration="fast"
>
{title}
</Text>
{informationalPopoverFeature ? (
<InformationalPopover feature={informationalPopoverFeature}>
<Text
fontWeight="semibold"
color={isSelected ? 'base.200' : 'base.500'}
userSelect="none"
transitionProperty="common"
transitionDuration="fast"
>
{title}
</Text>
</InformationalPopover>
) : (
<Text
fontWeight="semibold"
color={isSelected ? 'base.200' : 'base.500'}
userSelect="none"
transitionProperty="common"
transitionDuration="fast"
>
{title}
</Text>
)}
<Spacer />
</Flex>
{canMergeVisible && <CanvasEntityMergeVisibleButton type={type} />}
{canHideAll && <CanvasEntityTypeIsHiddenToggle type={type} />}
{isRenderableEntityType(type) && <CanvasEntityMergeVisibleButton type={type} />}
{isRenderableEntityType(type) && <CanvasEntityTypeIsHiddenToggle type={type} />}
<CanvasEntityAddOfTypeButton type={type} />
</Flex>
<Collapse in={collapse.isTrue}>

View File

@@ -2,7 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { IconMenuItem } from 'common/components/IconMenuItem';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import {
entityArrangedBackwardOne,
entityArrangedForwardOne,
@@ -56,7 +56,7 @@ export const CanvasEntityMenuItemsArrange = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectCanvasSlice, (canvas) => {
@@ -92,28 +92,28 @@ export const CanvasEntityMenuItemsArrange = memo(() => {
aria-label={t('controlLayers.moveToFront')}
tooltip={t('controlLayers.moveToFront')}
onClick={moveToFront}
isDisabled={!validActions.canMoveToFront || !isInteractable}
isDisabled={!validActions.canMoveToFront || isBusy}
icon={<PiArrowLineUpBold />}
/>
<IconMenuItem
aria-label={t('controlLayers.moveForward')}
tooltip={t('controlLayers.moveForward')}
onClick={moveForwardOne}
isDisabled={!validActions.canMoveForwardOne || !isInteractable}
isDisabled={!validActions.canMoveForwardOne || isBusy}
icon={<PiArrowUpBold />}
/>
<IconMenuItem
aria-label={t('controlLayers.moveBackward')}
tooltip={t('controlLayers.moveBackward')}
onClick={moveBackwardOne}
isDisabled={!validActions.canMoveBackwardOne || !isInteractable}
isDisabled={!validActions.canMoveBackwardOne || isBusy}
icon={<PiArrowDownBold />}
/>
<IconMenuItem
aria-label={t('controlLayers.moveToBack')}
tooltip={t('controlLayers.moveToBack')}
onClick={moveToBack}
isDisabled={!validActions.canMoveToBack || !isInteractable}
isDisabled={!validActions.canMoveToBack || isBusy}
icon={<PiArrowLineDownBold />}
/>
</>

View File

@@ -1,8 +1,9 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useCopyLayerToClipboard } from 'features/controlLayers/hooks/useCopyLayerToClipboard';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCopyBold } from 'react-icons/pi';
@@ -11,7 +12,8 @@ export const CanvasEntityMenuItemsCopyToClipboard = memo(() => {
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext();
const adapter = useEntityAdapterSafe(entityIdentifier);
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const isEmpty = useEntityIsEmpty(entityIdentifier);
const copyLayerToClipboard = useCopyLayerToClipboard();
const onClick = useCallback(() => {
@@ -19,8 +21,8 @@ export const CanvasEntityMenuItemsCopyToClipboard = memo(() => {
}, [copyLayerToClipboard, adapter]);
return (
<MenuItem onClick={onClick} icon={<PiCopyBold />} isDisabled={!isInteractable}>
{t('controlLayers.copyToClipboard')}
<MenuItem onClick={onClick} icon={<PiCopyBold />} isDisabled={isBusy || isEmpty}>
{t('common.clipboard')}
</MenuItem>
);
});

View File

@@ -1,7 +1,8 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCropBold } from 'react-icons/pi';
@@ -10,7 +11,8 @@ export const CanvasEntityMenuItemsCropToBbox = memo(() => {
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext();
const adapter = useEntityAdapterSafe(entityIdentifier);
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const isLocked = useEntityIsLocked(entityIdentifier);
const onClick = useCallback(() => {
if (!adapter) {
return;
@@ -19,7 +21,7 @@ export const CanvasEntityMenuItemsCropToBbox = memo(() => {
}, [adapter]);
return (
<MenuItem onClick={onClick} icon={<PiCropBold />} isDisabled={!isInteractable}>
<MenuItem onClick={onClick} icon={<PiCropBold />} isDisabled={isBusy || isLocked}>
{t('controlLayers.cropLayerToBbox')}
</MenuItem>
);

View File

@@ -2,7 +2,7 @@ import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { IconMenuItem } from 'common/components/IconMenuItem';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -16,7 +16,7 @@ export const CanvasEntityMenuItemsDelete = memo(({ asIcon = false }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const deleteEntity = useCallback(() => {
dispatch(entityDeleted({ entityIdentifier }));
@@ -30,13 +30,13 @@ export const CanvasEntityMenuItemsDelete = memo(({ asIcon = false }: Props) => {
onClick={deleteEntity}
icon={<PiTrashSimpleBold />}
isDestructive
isDisabled={!isInteractable}
isDisabled={isBusy}
/>
);
}
return (
<MenuItem onClick={deleteEntity} icon={<PiTrashSimpleBold />} isDestructive isDisabled={!isInteractable}>
<MenuItem onClick={deleteEntity} icon={<PiTrashSimpleBold />} isDestructive isDisabled={isBusy}>
{t('common.delete')}
</MenuItem>
);

View File

@@ -1,7 +1,7 @@
import { useAppDispatch } from 'app/store/storeHooks';
import { IconMenuItem } from 'common/components/IconMenuItem';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { entityDuplicated } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -11,7 +11,7 @@ export const CanvasEntityMenuItemsDuplicate = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const onClick = useCallback(() => {
dispatch(entityDuplicated({ entityIdentifier }));
@@ -23,7 +23,7 @@ export const CanvasEntityMenuItemsDuplicate = memo(() => {
tooltip={t('controlLayers.duplicate')}
onClick={onClick}
icon={<PiCopyFill />}
isDisabled={!isInteractable}
isDisabled={isBusy}
/>
);
});

View File

@@ -3,7 +3,7 @@ import { useEntityIdentifierContext } from 'features/controlLayers/contexts/Enti
import { useEntityFilter } from 'features/controlLayers/hooks/useEntityFilter';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiShootingStarBold } from 'react-icons/pi';
import { PiShootingStarFill } from 'react-icons/pi';
export const CanvasEntityMenuItemsFilter = memo(() => {
const { t } = useTranslation();
@@ -11,7 +11,7 @@ export const CanvasEntityMenuItemsFilter = memo(() => {
const filter = useEntityFilter(entityIdentifier);
return (
<MenuItem onClick={filter.start} icon={<PiShootingStarBold />} isDisabled={filter.isDisabled}>
<MenuItem onClick={filter.start} icon={<PiShootingStarFill />} isDisabled={filter.isDisabled}>
{t('controlLayers.filter.filter')}
</MenuItem>
);

View File

@@ -0,0 +1,35 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIdentifierBelowThisOne } from 'features/controlLayers/hooks/useNextRenderableEntityIdentifier';
import type { CanvasRenderableEntityType } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiStackSimpleBold } from 'react-icons/pi';
export const CanvasEntityMenuItemsMergeDown = memo(() => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const isBusy = useCanvasIsBusy();
const entityIdentifier = useEntityIdentifierContext<CanvasRenderableEntityType>();
const entityIdentifierBelowThisOne = useEntityIdentifierBelowThisOne(entityIdentifier);
const mergeDown = useCallback(() => {
if (entityIdentifierBelowThisOne === null) {
return;
}
canvasManager.compositor.mergeByEntityIdentifiers([entityIdentifierBelowThisOne, entityIdentifier], true);
}, [canvasManager.compositor, entityIdentifier, entityIdentifierBelowThisOne]);
return (
<MenuItem
onClick={mergeDown}
icon={<PiStackSimpleBold />}
isDisabled={isBusy || entityIdentifierBelowThisOne === null}
>
{t('controlLayers.mergeDown')}
</MenuItem>
);
});
CanvasEntityMenuItemsMergeDown.displayName = 'CanvasEntityMenuItemsMergeDown';

View File

@@ -1,7 +1,7 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useSaveLayerToAssets } from 'features/controlLayers/hooks/useSaveLayerToAssets';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -11,14 +11,14 @@ export const CanvasEntityMenuItemsSave = memo(() => {
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext();
const adapter = useEntityAdapterSafe(entityIdentifier);
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const saveLayerToAssets = useSaveLayerToAssets();
const onClick = useCallback(() => {
saveLayerToAssets(adapter);
}, [saveLayerToAssets, adapter]);
return (
<MenuItem onClick={onClick} icon={<PiFloppyDiskBold />} isDisabled={!isInteractable}>
<MenuItem onClick={onClick} icon={<PiFloppyDiskBold />} isDisabled={isBusy}>
{t('controlLayers.saveLayerToAssets')}
</MenuItem>
);

View File

@@ -3,18 +3,18 @@ import { useEntityIdentifierContext } from 'features/controlLayers/contexts/Enti
import { useEntitySegmentAnything } from 'features/controlLayers/hooks/useEntitySegmentAnything';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiMaskHappyBold } from 'react-icons/pi';
import { PiShapesFill } from 'react-icons/pi';
export const CanvasEntityMenuItemsSegment = memo(() => {
export const CanvasEntityMenuItemsSelectObject = memo(() => {
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext();
const segmentAnything = useEntitySegmentAnything(entityIdentifier);
return (
<MenuItem onClick={segmentAnything.start} icon={<PiMaskHappyBold />} isDisabled={segmentAnything.isDisabled}>
{t('controlLayers.segment.autoMask')}
<MenuItem onClick={segmentAnything.start} icon={<PiShapesFill />} isDisabled={segmentAnything.isDisabled}>
{t('controlLayers.selectObject.selectObject')}
</MenuItem>
);
});
CanvasEntityMenuItemsSegment.displayName = 'CanvasEntityMenuItemsSegment';
CanvasEntityMenuItemsSelectObject.displayName = 'CanvasEntityMenuItemsSelectObject';

View File

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

View File

@@ -24,7 +24,9 @@ import {
selectEntityOrThrow,
} from 'features/controlLayers/store/selectors';
import type {
CanvasControlLayerState,
CanvasEntityIdentifier,
CanvasInpaintMaskState,
CanvasRasterLayerState,
CanvasRegionalGuidanceState,
ControlNetConfig,
@@ -44,6 +46,8 @@ import { useCallback } from 'react';
import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models';
import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
import { isControlNetOrT2IAdapterModelConfig, isIPAdapterModelConfig } from 'services/api/types';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
export const selectDefaultControlAdapter = createSelector(
selectModelConfigsQuery,
@@ -124,6 +128,60 @@ export const useNewRasterLayerFromImage = () => {
return func;
};
export const useNewControlLayerFromImage = () => {
const dispatch = useAppDispatch();
const bboxRect = useAppSelector(selectBboxRect);
const func = useCallback(
(imageDTO: ImageDTO) => {
const imageObject = imageDTOToImageObject(imageDTO);
const overrides: Partial<CanvasControlLayerState> = {
position: { x: bboxRect.x, y: bboxRect.y },
objects: [imageObject],
};
dispatch(controlLayerAdded({ overrides, isSelected: true }));
},
[bboxRect.x, bboxRect.y, dispatch]
);
return func;
};
export const useNewInpaintMaskFromImage = () => {
const dispatch = useAppDispatch();
const bboxRect = useAppSelector(selectBboxRect);
const func = useCallback(
(imageDTO: ImageDTO) => {
const imageObject = imageDTOToImageObject(imageDTO);
const overrides: Partial<CanvasInpaintMaskState> = {
position: { x: bboxRect.x, y: bboxRect.y },
objects: [imageObject],
};
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
},
[bboxRect.x, bboxRect.y, dispatch]
);
return func;
};
export const useNewRegionalGuidanceFromImage = () => {
const dispatch = useAppDispatch();
const bboxRect = useAppSelector(selectBboxRect);
const func = useCallback(
(imageDTO: ImageDTO) => {
const imageObject = imageDTOToImageObject(imageDTO);
const overrides: Partial<CanvasRegionalGuidanceState> = {
position: { x: bboxRect.x, y: bboxRect.y },
objects: [imageObject],
};
dispatch(rgAdded({ overrides, isSelected: true }));
},
[bboxRect.x, bboxRect.y, dispatch]
);
return func;
};
/**
* Returns a function that adds a new canvas with the given image as the initial image, replicating the img2img flow:
* - Reset the canvas
@@ -138,18 +196,31 @@ export const useNewCanvasFromImage = () => {
const bboxRect = useAppSelector(selectBboxRect);
const base = useAppSelector(selectBboxModelBase);
const func = useCallback(
(imageDTO: ImageDTO) => {
(imageDTO: ImageDTO, type: CanvasRasterLayerState['type'] | CanvasControlLayerState['type']) => {
// Calculate the new bbox dimensions to fit the image's aspect ratio at the optimal size
const ratio = imageDTO.width / imageDTO.height;
const optimalDimension = getOptimalDimension(base);
const { width, height } = calculateNewSize(ratio, optimalDimension ** 2, base);
// The overrides need to include the layer's ID so we can transform the layer it is initialized
const overrides = {
id: getPrefixedId('raster_layer'),
position: { x: bboxRect.x, y: bboxRect.y },
objects: [imageDTOToImageObject(imageDTO)],
} satisfies Partial<CanvasRasterLayerState>;
let overrides: Partial<CanvasRasterLayerState> | Partial<CanvasControlLayerState>;
if (type === 'raster_layer') {
overrides = {
id: getPrefixedId('raster_layer'),
position: { x: bboxRect.x, y: bboxRect.y },
objects: [imageDTOToImageObject(imageDTO)],
} satisfies Partial<CanvasRasterLayerState>;
} else if (type === 'control_layer') {
overrides = {
id: getPrefixedId('control_layer'),
position: { x: bboxRect.x, y: bboxRect.y },
objects: [imageDTOToImageObject(imageDTO)],
} satisfies Partial<CanvasControlLayerState>;
} else {
// Catch unhandled types
assert<Equals<typeof type, never>>(false);
}
CanvasEntityAdapterBase.registerInitCallback(async (adapter) => {
// Skip the callback if the adapter is not the one we are creating
@@ -166,7 +237,16 @@ export const useNewCanvasFromImage = () => {
dispatch(canvasReset());
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
// The type casts are safe because the type is checked above
if (type === 'raster_layer') {
dispatch(rasterLayerAdded({ overrides: overrides as Partial<CanvasRasterLayerState>, isSelected: true }));
} else if (type === 'control_layer') {
dispatch(controlLayerAdded({ overrides: overrides as Partial<CanvasControlLayerState>, isSelected: true }));
} else {
// Catch unhandled types
assert<Equals<typeof type, never>>(false);
}
},
[base, bboxRect.x, bboxRect.y, dispatch]
);

View File

@@ -51,7 +51,9 @@ const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave, wit
const saveCanvas = useCallback(async () => {
const rect =
region === 'bbox' ? canvasManager.stateApi.getBbox().rect : canvasManager.stage.getVisibleRect('raster_layer');
region === 'bbox'
? canvasManager.stateApi.getBbox().rect
: canvasManager.compositor.getVisibleRectOfType('raster_layer');
if (rect.width === 0 || rect.height === 0) {
toast({
@@ -68,12 +70,13 @@ const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave, wit
metadata = selectCanvasMetadata(store.getState());
}
const result = await withResultAsync(() =>
canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, {
const result = await withResultAsync(() => {
const rasterAdapters = canvasManager.compositor.getVisibleAdaptersOfType('raster_layer');
return canvasManager.compositor.getCompositeImageDTO(rasterAdapters, rect, {
is_intermediate: !saveToGallery,
metadata,
})
);
});
});
if (result.isOk()) {
if (onSave) {
@@ -86,7 +89,6 @@ const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave, wit
}
}, [
canvasManager.compositor,
canvasManager.stage,
canvasManager.stateApi,
onSave,
region,

View File

@@ -1,9 +1,8 @@
import { useStore } from '@nanostores/react';
import { $false } from 'app/store/nanostores/util';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import { entityReset } from 'features/controlLayers/store/canvasSlice';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { isMaskEntityIdentifier } from 'features/controlLayers/store/types';
@@ -14,30 +13,30 @@ import { useCallback, useMemo } from 'react';
export function useCanvasResetLayerHotkey() {
useAssertSingleton(useCanvasResetLayerHotkey.name);
const dispatch = useAppDispatch();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const entityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const isBusy = useCanvasIsBusy();
const adapter = useEntityAdapterSafe(selectedEntityIdentifier);
const isInteractable = useStore(adapter?.$isInteractable ?? $false);
const adapter = useEntityAdapterSafe(entityIdentifier);
const isLocked = useEntityIsLocked(entityIdentifier);
const imageViewer = useImageViewer();
const resetSelectedLayer = useCallback(() => {
if (selectedEntityIdentifier === null || adapter === null) {
if (entityIdentifier === null || adapter === null) {
return;
}
adapter.bufferRenderer.clearBuffer();
dispatch(entityReset({ entityIdentifier: selectedEntityIdentifier }));
}, [adapter, dispatch, selectedEntityIdentifier]);
dispatch(entityReset({ entityIdentifier }));
}, [adapter, dispatch, entityIdentifier]);
const isResetEnabled = useMemo(
() => selectedEntityIdentifier !== null && isMaskEntityIdentifier(selectedEntityIdentifier),
[selectedEntityIdentifier]
const isResetAllowed = useMemo(
() => entityIdentifier !== null && isMaskEntityIdentifier(entityIdentifier),
[entityIdentifier]
);
useRegisteredHotkeys({
id: 'resetSelected',
category: 'canvas',
callback: resetSelectedLayer,
options: { enabled: isResetEnabled && !isBusy && isInteractable && !imageViewer.isOpen },
dependencies: [isResetEnabled, isBusy, isInteractable, resetSelectedLayer, imageViewer.isOpen],
options: { enabled: isResetAllowed && !isBusy && !isLocked && !imageViewer.isOpen },
dependencies: [isResetAllowed, isBusy, isLocked, resetSelectedLayer, imageViewer.isOpen],
});
}

View File

@@ -1,3 +1,4 @@
import { logger } from 'app/logging/logger';
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer';
import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask';
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
@@ -7,6 +8,9 @@ import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { serializeError } from 'serialize-error';
const log = logger('canvas');
export const useCopyLayerToClipboard = () => {
const { t } = useTranslation();
@@ -26,11 +30,13 @@ export const useCopyLayerToClipboard = () => {
const canvas = adapter.getCanvas();
const blob = await canvasToBlob(canvas);
copyBlobToClipboard(blob);
log.trace('Layer copied to clipboard');
toast({
status: 'info',
title: t('toast.layerCopiedToClipboard'),
});
} catch (error) {
log.error({ error: serializeError(error) }, 'Problem copying layer to clipboard');
toast({
status: 'error',
title: t('toast.problemCopyingLayer'),

View File

@@ -1,18 +1,20 @@
import { useStore } from '@nanostores/react';
import { $false } from 'app/store/nanostores/util';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { isFilterableEntityIdentifier } from 'features/controlLayers/store/types';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useCallback, useMemo } from 'react';
export const useEntityFilter = (entityIdentifier: CanvasEntityIdentifier | null) => {
const canvasManager = useCanvasManager();
const adapter = useEntityAdapterSafe(entityIdentifier);
const imageViewer = useImageViewer();
const isBusy = useCanvasIsBusy();
const isInteractable = useStore(adapter?.$isInteractable ?? $false);
const isEmpty = useStore(adapter?.$isEmpty ?? $false);
const isLocked = useEntityIsLocked(entityIdentifier);
const isEmpty = useEntityIsEmpty(entityIdentifier);
const isDisabled = useMemo(() => {
if (!entityIdentifier) {
@@ -27,14 +29,14 @@ export const useEntityFilter = (entityIdentifier: CanvasEntityIdentifier | null)
if (isBusy) {
return true;
}
if (!isInteractable) {
if (isLocked) {
return true;
}
if (isEmpty) {
return true;
}
return false;
}, [entityIdentifier, adapter, isBusy, isInteractable, isEmpty]);
}, [entityIdentifier, adapter, isBusy, isLocked, isEmpty]);
const start = useCallback(() => {
if (isDisabled) {
@@ -50,8 +52,9 @@ export const useEntityFilter = (entityIdentifier: CanvasEntityIdentifier | null)
if (!adapter) {
return;
}
imageViewer.close();
adapter.filterer.start();
}, [isDisabled, entityIdentifier, canvasManager]);
}, [isDisabled, entityIdentifier, canvasManager, imageViewer]);
return { isDisabled, start } as const;
};

View File

@@ -0,0 +1,14 @@
import { useAppSelector } from 'app/store/storeHooks';
import { buildSelectHasObjects } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
export const useEntityIsEmpty = (entityIdentifier: CanvasEntityIdentifier | null) => {
const selectHasObjects = useMemo(
() => (entityIdentifier ? buildSelectHasObjects(entityIdentifier) : () => false),
[entityIdentifier]
);
const hasObjects = useAppSelector(selectHasObjects);
return !hasObjects;
};

View File

@@ -1,13 +0,0 @@
import { useStore } from '@nanostores/react';
import { $true } from 'app/store/nanostores/util';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
export const useIsEntityInteractable = (entityIdentifier: CanvasEntityIdentifier) => {
const isBusy = useCanvasIsBusy();
const adapter = useEntityAdapterSafe(entityIdentifier);
const isInteractable = useStore(adapter?.$isInteractable ?? $true);
return !isBusy && isInteractable;
};

View File

@@ -4,10 +4,13 @@ import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/se
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
export const useEntityIsLocked = (entityIdentifier: CanvasEntityIdentifier) => {
export const useEntityIsLocked = (entityIdentifier: CanvasEntityIdentifier | null) => {
const selectIsLocked = useMemo(
() =>
createSelector(selectCanvasSlice, (canvas) => {
if (!entityIdentifier) {
return false;
}
const entity = selectEntity(canvas, entityIdentifier);
if (!entity) {
return false;

View File

@@ -1,18 +1,20 @@
import { useStore } from '@nanostores/react';
import { $false } from 'app/store/nanostores/util';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { isSegmentableEntityIdentifier } from 'features/controlLayers/store/types';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useCallback, useMemo } from 'react';
export const useEntitySegmentAnything = (entityIdentifier: CanvasEntityIdentifier | null) => {
const canvasManager = useCanvasManager();
const adapter = useEntityAdapterSafe(entityIdentifier);
const imageViewer = useImageViewer();
const isBusy = useCanvasIsBusy();
const isInteractable = useStore(adapter?.$isInteractable ?? $false);
const isEmpty = useStore(adapter?.$isEmpty ?? $false);
const isLocked = useEntityIsLocked(entityIdentifier);
const isEmpty = useEntityIsEmpty(entityIdentifier);
const isDisabled = useMemo(() => {
if (!entityIdentifier) {
@@ -27,14 +29,14 @@ export const useEntitySegmentAnything = (entityIdentifier: CanvasEntityIdentifie
if (isBusy) {
return true;
}
if (!isInteractable) {
if (isLocked) {
return true;
}
if (isEmpty) {
return true;
}
return false;
}, [entityIdentifier, adapter, isBusy, isInteractable, isEmpty]);
}, [entityIdentifier, adapter, isBusy, isLocked, isEmpty]);
const start = useCallback(() => {
if (isDisabled) {
@@ -50,8 +52,9 @@ export const useEntitySegmentAnything = (entityIdentifier: CanvasEntityIdentifie
if (!adapter) {
return;
}
imageViewer.close();
adapter.segmentAnything.start();
}, [isDisabled, entityIdentifier, canvasManager]);
}, [isDisabled, entityIdentifier, canvasManager, imageViewer]);
return { isDisabled, start } as const;
};

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