Compare commits

...

32 Commits

Author SHA1 Message Date
Eugene Brodsky
fe6ba3571a fix(package): pin python-multipart to v0.0.12 due to breakage in dependent packages with later versions 2024-10-24 16:08:18 +00: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
56 changed files with 1810 additions and 469 deletions

View File

@@ -808,7 +808,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

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

@@ -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

@@ -94,6 +94,7 @@
"close": "Close",
"copy": "Copy",
"copyError": "$t(gallery.copy) Error",
"clipboard": "Clipboard",
"on": "On",
"off": "Off",
"or": "or",
@@ -1251,6 +1252,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": [
@@ -1688,8 +1716,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",
@@ -1845,11 +1883,11 @@
"segment": {
"autoMask": "Auto Mask",
"pointType": "Point Type",
"foreground": "Foreground",
"background": "Background",
"include": "Include",
"exclude": "Exclude",
"neutral": "Neutral",
"reset": "Reset",
"apply": "Apply",
"saveAs": "Save As",
"cancel": "Cancel",
"process": "Process"
},

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

@@ -16,6 +16,7 @@ type UseGroupedModelComboboxArg<T extends AnyModelConfig> = {
getIsDisabled?: (model: T) => boolean;
isLoading?: boolean;
groupByType?: boolean;
showDescriptions?: boolean;
};
type UseGroupedModelComboboxReturn = {
@@ -37,7 +38,15 @@ export const useGroupedModelCombobox = <T extends AnyModelConfig>(
): UseGroupedModelComboboxReturn => {
const { t } = useTranslation();
const base = useAppSelector(selectBaseWithSDXLFallback);
const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, groupByType = false } = arg;
const {
modelConfigs,
selectedModel,
getIsDisabled,
onChange,
isLoading,
groupByType = false,
showDescriptions = false,
} = arg;
const options = useMemo<GroupBase<ComboboxOption>[]>(() => {
if (!modelConfigs) {
return [];
@@ -51,6 +60,7 @@ export const useGroupedModelCombobox = <T extends AnyModelConfig>(
options: val.map((model) => ({
label: model.name,
value: model.key,
description: (showDescriptions && model.description) || undefined,
isDisabled: getIsDisabled ? getIsDisabled(model) : false,
})),
});
@@ -60,7 +70,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, showDescriptions]);
const value = useMemo(
() =>

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

@@ -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,40 @@
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 { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import {
isFilterableEntityIdentifier,
isSaveableEntityIdentifier,
isSegmentableEntityIdentifier,
isTransformableEntityIdentifier,
} from 'features/controlLayers/store/types';
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(() => {

View File

@@ -1,5 +1,6 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { EntityListGlobalActionBarAddLayerMenu } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu';
import { EntityListSelectedEntityActionBarAutoMaskButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarAutoMaskButton';
import { EntityListSelectedEntityActionBarDuplicateButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton';
import { EntityListSelectedEntityActionBarFill } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFill';
import { EntityListSelectedEntityActionBarFilterButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton';
@@ -16,6 +17,7 @@ export const EntityListSelectedEntityActionBar = memo(() => {
<Spacer />
<EntityListSelectedEntityActionBarFill />
<Flex h="full">
<EntityListSelectedEntityActionBarAutoMaskButton />
<EntityListSelectedEntityActionBarFilterButton />
<EntityListSelectedEntityActionBarTransformButton />
<EntityListSelectedEntityActionBarSaveToAssetsButton />

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 { PiMaskHappyBold } from 'react-icons/pi';
export const EntityListSelectedEntityActionBarAutoMaskButton = 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}
size="sm"
variant="link"
alignSelf="stretch"
aria-label={t('controlLayers.segment.autoMask')}
tooltip={t('controlLayers.segment.autoMask')}
icon={<PiMaskHappyBold />}
/>
);
});
EntityListSelectedEntityActionBarAutoMaskButton.displayName = 'EntityListSelectedEntityActionBarAutoMaskButton';

View File

@@ -25,8 +25,8 @@ const MenuContent = () => {
return (
<CanvasManagerProviderGate>
<MenuList>
<CanvasContextMenuGlobalMenuItems />
<CanvasContextMenuSelectedEntityMenuItems />
<CanvasContextMenuGlobalMenuItems />
</MenuList>
</CanvasManagerProviderGate>
);

View File

@@ -1,7 +1,6 @@
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';
@@ -9,7 +8,8 @@ import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/c
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 { 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';
@@ -25,12 +25,13 @@ export const ControlLayerMenuItems = memo(() => {
<CanvasEntityMenuItemsTransform />
<CanvasEntityMenuItemsFilter />
<CanvasEntityMenuItemsSegment />
<ControlLayerMenuItemsConvertControlToRaster />
<ControlLayerMenuItemsTransparencyEffect />
<MenuDivider />
<CanvasEntityMenuItemsCropToBbox />
<CanvasEntityMenuItemsCopyToClipboard />
<CanvasEntityMenuItemsSave />
<MenuDivider />
<ControlLayerMenuItemsConvertToSubMenu />
<ControlLayerMenuItemsCopyToSubMenu />
</>
);
});

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,56 @@
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 { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
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 isInteractable = useIsEntityInteractable(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 />}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.convertControlLayerTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={!isInteractable}>
{t('controlLayers.inpaintMask')}
</MenuItem>
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={!isInteractable}>
{t('controlLayers.regionalGuidance')}
</MenuItem>
<MenuItem onClick={convertToRasterLayer} icon={<PiSwapBold />} isDisabled={!isInteractable}>
{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 { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
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 isInteractable = useIsEntityInteractable(entityIdentifier);
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 />}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.copyControlLayerTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<CanvasEntityMenuItemsCopyToClipboard />
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={!isInteractable}>
{t('controlLayers.newInpaintMask')}
</MenuItem>
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={!isInteractable}>
{t('controlLayers.newRegionalGuidance')}
</MenuItem>
<MenuItem onClick={copyToRasterLayer} icon={<PiCopyBold />} isDisabled={!isInteractable}>
{t('controlLayers.newRasterLayer')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
);
});
ControlLayerMenuItemsCopyToSubMenu.displayName = 'ControlLayerMenuItemsCopyToSubMenu';

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

@@ -5,6 +5,8 @@ import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/componen
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
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(() => {
@@ -19,6 +21,9 @@ export const InpaintMaskMenuItems = memo(() => {
<CanvasEntityMenuItemsTransform />
<MenuDivider />
<CanvasEntityMenuItemsCropToBbox />
<MenuDivider />
<InpaintMaskMenuItemsConvertToSubMenu />
<InpaintMaskMenuItemsCopyToSubMenu />
</>
);
});

View File

@@ -0,0 +1,38 @@
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 { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
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 isInteractable = useIsEntityInteractable(entityIdentifier);
const convertToRegionalGuidance = useCallback(() => {
dispatch(inpaintMaskConvertedToRegionalGuidance({ entityIdentifier, replace: true }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.convertInpaintMaskTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={!isInteractable}>
{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 { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
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 isInteractable = useIsEntityInteractable(entityIdentifier);
const copyToRegionalGuidance = useCallback(() => {
dispatch(inpaintMaskConvertedToRegionalGuidance({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.copyInpaintMaskTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<CanvasEntityMenuItemsCopyToClipboard />
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={!isInteractable}>
{t('controlLayers.newRegionalGuidance')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
);
});
InpaintMaskMenuItemsCopyToSubMenu.displayName = 'InpaintMaskMenuItemsCopyToSubMenu';

View File

@@ -1,7 +1,6 @@
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';
@@ -9,7 +8,8 @@ import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/c
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 { 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(() => {
@@ -24,11 +24,12 @@ export const RasterLayerMenuItems = memo(() => {
<CanvasEntityMenuItemsTransform />
<CanvasEntityMenuItemsFilter />
<CanvasEntityMenuItemsSegment />
<RasterLayerMenuItemsConvertRasterToControl />
<MenuDivider />
<CanvasEntityMenuItemsCropToBbox />
<CanvasEntityMenuItemsCopyToClipboard />
<CanvasEntityMenuItemsSave />
<MenuDivider />
<RasterLayerMenuItemsConvertToSubMenu />
<RasterLayerMenuItemsCopyToSubMenu />
</>
);
});

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,65 @@
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 { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
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 isInteractable = useIsEntityInteractable(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 />}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.convertRasterLayerTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={!isInteractable}>
{t('controlLayers.inpaintMask')}
</MenuItem>
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={!isInteractable}>
{t('controlLayers.regionalGuidance')}
</MenuItem>
<MenuItem onClick={convertToControlLayer} icon={<PiSwapBold />} isDisabled={!isInteractable}>
{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 { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
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 isInteractable = useIsEntityInteractable(entityIdentifier);
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 />}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.copyRasterLayerTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<CanvasEntityMenuItemsCopyToClipboard />
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={!isInteractable}>
{t('controlLayers.newInpaintMask')}
</MenuItem>
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={!isInteractable}>
{t('controlLayers.newRegionalGuidance')}
</MenuItem>
<MenuItem onClick={copyToControlLayer} icon={<PiCopyBold />} isDisabled={!isInteractable}>
{t('controlLayers.newControlLayer')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
);
});
RasterLayerMenuItemsCopyToSubMenu.displayName = 'RasterLayerMenuItemsCopyToSubMenu';

View File

@@ -1,4 +1,5 @@
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';
@@ -6,16 +7,18 @@ import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/component
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 />
@@ -23,6 +26,9 @@ export const RegionalGuidanceMenuItems = memo(() => {
<RegionalGuidanceMenuItemsAutoNegative />
<MenuDivider />
<CanvasEntityMenuItemsCropToBbox />
<MenuDivider />
<RegionalGuidanceMenuItemsConvertToSubMenu />
<RegionalGuidanceMenuItemsCopyToSubMenu />
</>
);
});

View File

@@ -0,0 +1,38 @@
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 { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
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 isInteractable = useIsEntityInteractable(entityIdentifier);
const convertToInpaintMask = useCallback(() => {
dispatch(rgConvertedToInpaintMask({ entityIdentifier, replace: true }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.convertRegionalGuidanceTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={!isInteractable}>
{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 { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
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 isInteractable = useIsEntityInteractable(entityIdentifier);
const copyToInpaintMask = useCallback(() => {
dispatch(rgConvertedToInpaintMask({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.copyRegionalGuidanceTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<CanvasEntityMenuItemsCopyToClipboard />
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={!isInteractable}>
{t('controlLayers.newInpaintMask')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
);
});
RegionalGuidanceMenuItemsCopyToSubMenu.displayName = 'RegionalGuidanceMenuItemsCopyToSubMenu';

View File

@@ -1,4 +1,14 @@
import { Button, ButtonGroup, Flex, Heading, Spacer } from '@invoke-ai/ui-library';
import {
Button,
ButtonGroup,
Flex,
Heading,
Menu,
MenuButton,
MenuItem,
MenuList,
Spacer,
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
@@ -10,9 +20,9 @@ import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/kon
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 { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowsCounterClockwiseBold, PiCheckBold, PiStarBold, PiXBold } from 'react-icons/pi';
import { PiArrowsCounterClockwiseBold, PiFloppyDiskBold, PiStarBold, PiXBold } from 'react-icons/pi';
const SegmentAnythingContent = memo(
({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
@@ -22,8 +32,25 @@ const SegmentAnythingContent = memo(
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',
@@ -86,15 +113,32 @@ const SegmentAnythingContent = memo(
>
{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>
<Menu>
<MenuButton
as={Button}
leftIcon={<PiFloppyDiskBold />}
isLoading={isProcessing}
loadingText={t('controlLayers.segment.saveAs')}
variant="ghost"
isDisabled={!hasImageState}
>
{t('controlLayers.segment.saveAs')}
</MenuButton>
<MenuList>
<MenuItem isDisabled={!hasImageState} onClick={saveAsInpaintMask}>
{t('controlLayers.inpaintMask')}
</MenuItem>
<MenuItem isDisabled={!hasImageState} onClick={saveAsRegionalGuidance}>
{t('controlLayers.regionalGuidance')}
</MenuItem>
<MenuItem isDisabled={!hasImageState} onClick={saveAsControlLayer}>
{t('controlLayers.controlLayer')}
</MenuItem>
<MenuItem isDisabled={!hasImageState} onClick={saveAsRasterLayer}>
{t('controlLayers.rasterLayer')}
</MenuItem>
</MenuList>
</Menu>
<Button
leftIcon={<PiXBold />}
onClick={adapter.segmentAnything.cancel}

View File

@@ -26,13 +26,10 @@ export const SegmentAnythingPointType = memo(
<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.segment.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.segment.exclude')}</Text>
</Radio>
</Flex>
</RadioGroup>

View File

@@ -1,9 +1,11 @@
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 { PropsWithChildren } from 'react';
@@ -21,6 +23,7 @@ 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]);
@@ -47,15 +50,30 @@ 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} />}

View File

@@ -20,7 +20,7 @@ export const CanvasEntityMenuItemsCopyToClipboard = memo(() => {
return (
<MenuItem onClick={onClick} icon={<PiCopyBold />} isDisabled={!isInteractable}>
{t('controlLayers.copyToClipboard')}
{t('common.clipboard')}
</MenuItem>
);
});

View File

@@ -5,11 +5,13 @@ import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdap
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
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);
@@ -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

@@ -5,11 +5,13 @@ import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdap
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
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);
@@ -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;
};

View File

@@ -5,11 +5,13 @@ import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdap
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { isTransformableEntityIdentifier } from 'features/controlLayers/store/types';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useCallback, useMemo } from 'react';
export const useEntityTransform = (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);
@@ -67,10 +69,11 @@ export const useEntityTransform = (entityIdentifier: CanvasEntityIdentifier | nu
if (!adapter) {
return;
}
imageViewer.close();
await adapter.transformer.startTransform({ silent: true });
adapter.transformer.fitToBboxContain();
await adapter.transformer.applyTransform();
}, [canvasManager, entityIdentifier, isDisabled]);
}, [canvasManager, entityIdentifier, imageViewer, isDisabled]);
return { isDisabled, start, fitToBbox } as const;
};

View File

@@ -0,0 +1,25 @@
import type { Feature } from 'common/components/InformationalPopover/constants';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
export const useEntityTypeInformationalPopover = (type: CanvasEntityIdentifier['type']): Feature | undefined => {
const feature = useMemo(() => {
switch (type) {
case 'control_layer':
return 'controlNet';
case 'inpaint_mask':
return 'inpainting';
case 'raster_layer':
return 'rasterLayer';
case 'regional_guidance':
return 'regionalGuidanceAndReferenceImage';
case 'reference_image':
return 'globalReferenceImage';
default:
return undefined;
}
}, [type]);
return feature;
};

View File

@@ -6,15 +6,22 @@ import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konv
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
import { addCoords, getKonvaNodeDebugAttrs, getPrefixedId, offsetCoord } from 'features/controlLayers/konva/util';
import {
addCoords,
getKonvaNodeDebugAttrs,
getPrefixedId,
offsetCoord,
roundCoord,
} from 'features/controlLayers/konva/util';
import { selectAutoProcess } from 'features/controlLayers/store/canvasSettingsSlice';
import type {
CanvasEntityType,
CanvasImageState,
Coordinate,
RgbaColor,
SAMPoint,
SAMPointLabel,
SAMPointLabelString,
SAMPointWithId,
} from 'features/controlLayers/store/types';
import { SAM_POINT_LABEL_NUMBER_TO_STRING } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
@@ -27,6 +34,9 @@ import { atom, computed } from 'nanostores';
import type { Logger } from 'roarr';
import { serializeError } from 'serialize-error';
import type { ImageDTO } from 'services/api/types';
import stableHash from 'stable-hash';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
type CanvasSegmentAnythingModuleConfig = {
/**
@@ -70,7 +80,7 @@ const DEFAULT_CONFIG: CanvasSegmentAnythingModuleConfig = {
SAM_POINT_FOREGROUND_COLOR: { r: 50, g: 255, b: 0, a: 1 }, // light green
SAM_POINT_BACKGROUND_COLOR: { r: 255, g: 0, b: 50, a: 1 }, // red-ish
SAM_POINT_NEUTRAL_COLOR: { r: 0, g: 225, b: 255, a: 1 }, // cyan
MASK_COLOR: { r: 0, g: 200, b: 200, a: 0.5 }, // cyan with 50% opacity
MASK_COLOR: { r: 0, g: 225, b: 255, a: 1 }, // cyan
PROCESS_DEBOUNCE_MS: 1000,
};
@@ -85,6 +95,7 @@ const DEFAULT_CONFIG: CanvasSegmentAnythingModuleConfig = {
type SAMPointState = {
id: string;
label: SAMPointLabel;
coord: Coordinate;
konva: {
circle: Konva.Circle;
};
@@ -113,9 +124,9 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
$isSegmenting = atom<boolean>(false);
/**
* Whether the current set of points has been processed.
* The hash of the last processed points. This is used to prevent re-processing the same points.
*/
$hasProcessed = atom<boolean>(false);
$lastProcessedHash = atom<string>('');
/**
* Whether the module is currently processing the points.
@@ -144,10 +155,15 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
/**
* The ephemeral image state of the processed image. Only used while segmenting.
*/
imageState: CanvasImageState | null = null;
$imageState = atom<CanvasImageState | null>(null);
/**
* The current input points.
* Whether the module has an image state. This is a computed value based on $imageState.
*/
$hasImageState = computed(this.$imageState, (imageState) => imageState !== null);
/**
* The current input points. A listener is added to this atom to process the points when they change.
*/
$points = atom<SAMPointState[]>([]);
@@ -187,6 +203,10 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
* It's rendered with a globalCompositeOperation of 'source-atop' to preview the mask as a semi-transparent overlay.
*/
compositingRect: Konva.Rect;
/**
* A tween for pulsing the mask group's opacity.
*/
maskTween: Konva.Tween | null;
};
KONVA_CIRCLE_NAME = `${this.type}:circle`;
@@ -209,7 +229,7 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
this.konva = {
group: new Konva.Group({ name: this.KONVA_GROUP_NAME }),
pointGroup: new Konva.Group({ name: this.KONVA_POINT_GROUP_NAME }),
maskGroup: new Konva.Group({ name: this.KONVA_MASK_GROUP_NAME }),
maskGroup: new Konva.Group({ name: this.KONVA_MASK_GROUP_NAME, opacity: 0.6 }),
compositingRect: new Konva.Rect({
name: this.KONVA_COMPOSITING_RECT_NAME,
fill: rgbaColorToString(this.config.MASK_COLOR),
@@ -219,6 +239,7 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
perfectDrawEnabled: false,
visible: false,
}),
maskTween: null,
};
// Points should always be rendered above the mask group
@@ -250,10 +271,12 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
createPoint(coord: Coordinate, label: SAMPointLabel): SAMPointState {
const id = getPrefixedId('sam_point');
const roundedCoord = roundCoord(coord);
const circle = new Konva.Circle({
name: this.KONVA_CIRCLE_NAME,
x: Math.round(coord.x),
y: Math.round(coord.y),
x: roundedCoord.x,
y: roundedCoord.y,
radius: this.manager.stage.unscale(this.config.SAM_POINT_RADIUS), // We will scale this as the stage scale changes
fill: rgbaColorToString(this.getSAMPointColor(label)),
stroke: rgbaColorToString(this.config.SAM_POINT_BORDER_COLOR),
@@ -273,11 +296,12 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
// This event should not bubble up to the parent, stage or any other nodes
e.cancelBubble = true;
circle.destroy();
this.$points.set(this.$points.get().filter((point) => point.id !== id));
if (this.$points.get().length === 0) {
const newPoints = this.$points.get().filter((point) => point.id !== id);
if (newPoints.length === 0) {
this.resetEphemeralState();
} else {
this.$hasProcessed.set(false);
this.$points.set(newPoints);
}
});
@@ -286,25 +310,28 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
});
circle.on('dragend', () => {
const roundedCoord = roundCoord(circle.position());
this.log.trace({ ...roundedCoord, label: SAM_POINT_LABEL_NUMBER_TO_STRING[label] }, 'Moved SAM point');
this.$isDraggingPoint.set(false);
// Point has changed!
this.$hasProcessed.set(false);
this.$points.notify();
this.log.trace(
{ x: Math.round(circle.x()), y: Math.round(circle.y()), label: SAM_POINT_LABEL_NUMBER_TO_STRING[label] },
'Moved SAM point'
);
const newPoints = this.$points.get().map((point) => {
if (point.id === id) {
return { ...point, coord: roundedCoord };
}
return point;
});
this.$points.set(newPoints);
});
this.konva.pointGroup.add(circle);
this.log.trace(
{ x: Math.round(circle.x()), y: Math.round(circle.y()), label: SAM_POINT_LABEL_NUMBER_TO_STRING[label] },
'Created SAM point'
);
this.log.trace({ ...roundedCoord, label: SAM_POINT_LABEL_NUMBER_TO_STRING[label] }, 'Created SAM point');
return {
id,
coord: roundedCoord,
label,
konva: { circle },
};
@@ -327,14 +354,14 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
/**
* Gets the SAM points in the format expected by the segment-anything API. The x and y values are rounded to integers.
*/
getSAMPoints = (): SAMPoint[] => {
const points: SAMPoint[] = [];
getSAMPoints = (): SAMPointWithId[] => {
const points: SAMPointWithId[] = [];
for (const { konva, label } of this.$points.get()) {
for (const { id, coord, label } of this.$points.get()) {
points.push({
// Pull out and round the x and y values from Konva
x: Math.round(konva.circle.x()),
y: Math.round(konva.circle.y()),
id,
x: coord.x,
y: coord.y,
label,
});
}
@@ -381,10 +408,8 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
// Create a SAM point at the normalized position
const point = this.createPoint(normalizedPoint, this.$pointType.get());
this.$points.set([...this.$points.get(), point]);
// Mark the module as having _not_ processed the points now that they have changed
this.$hasProcessed.set(false);
const newPoints = [...this.$points.get(), point];
this.$points.set(newPoints);
};
/**
@@ -421,6 +446,7 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
if (points.length === 0) {
return;
}
if (this.manager.stateApi.getSettings().autoProcess) {
this.process();
}
@@ -433,7 +459,7 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
if (this.$points.get().length === 0) {
return;
}
if (autoProcess && !this.$hasProcessed.get()) {
if (autoProcess) {
this.process();
}
})
@@ -500,6 +526,12 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
return;
}
const hash = stableHash(points);
if (hash === this.$lastProcessedHash.get()) {
this.log.trace('Already processed points');
return;
}
this.$isProcessing.set(true);
this.log.trace({ points }, 'Segmenting');
@@ -521,7 +553,7 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
this.abortController = controller;
// Build the graph for segmenting the image, using the rasterized image DTO
const { graph, outputNodeId } = this.buildGraph(rasterizeResult.value);
const { graph, outputNodeId } = this.buildGraph(rasterizeResult.value, points);
// Run the graph and get the segmented image output
const segmentResult = await withResultAsync(() =>
@@ -548,21 +580,27 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
this.log.trace({ imageDTO: segmentResult.value }, 'Segmented');
// Prepare the ephemeral image state
this.imageState = imageDTOToImageObject(segmentResult.value);
const imageState = imageDTOToImageObject(segmentResult.value);
this.$imageState.set(imageState);
// Destroy any existing masked image and create a new one
if (this.maskedImage) {
this.maskedImage.destroy();
}
this.maskedImage = new CanvasObjectImage(this.imageState, this);
if (this.konva.maskTween) {
this.konva.maskTween.destroy();
this.konva.maskTween = null;
}
this.maskedImage = new CanvasObjectImage(imageState, this);
// Force update the masked image - after awaiting, the image will be rendered (in memory)
await this.maskedImage.update(this.imageState, true);
await this.maskedImage.update(imageState, true);
// Update the compositing rect to match the image size
this.konva.compositingRect.setAttrs({
width: this.imageState.image.width,
height: this.imageState.image.height,
width: imageState.image.width,
height: imageState.image.height,
visible: true,
});
@@ -574,12 +612,24 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
// Cache the group to ensure the mask is rendered correctly w/ opacity
this.konva.maskGroup.cache();
// Create a pulsing tween
this.konva.maskTween = new Konva.Tween({
node: this.konva.maskGroup,
duration: 1,
opacity: 0.4, // oscillate between this value and pre-tween opacity
yoyo: true,
repeat: Infinity,
easing: Konva.Easings.EaseOut,
});
// Start the pulsing effect
this.konva.maskTween.play();
this.$lastProcessedHash.set(hash);
// We are done processing (still segmenting though!)
this.$isProcessing.set(false);
// The current points have been processed
this.$hasProcessed.set(true);
// Clean up the abort controller as needed
if (!this.abortController.signal.aborted) {
this.abortController.abort();
@@ -596,11 +646,7 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
* Applies the segmented image to the entity.
*/
apply = () => {
if (!this.$hasProcessed.get()) {
this.log.error('Cannot apply unprocessed points');
return;
}
const imageState = this.imageState;
const imageState = this.$imageState.get();
if (!imageState) {
this.log.error('No image state to apply');
return;
@@ -627,6 +673,55 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
this.teardown();
};
/**
* Applies the segmented image to the entity.
*/
saveAs = (type: Exclude<CanvasEntityType, 'reference_image'>) => {
const imageState = this.$imageState.get();
if (!imageState) {
this.log.error('No image state to save as');
return;
}
this.log.trace(`Saving as ${type}`);
// Clear the buffer - we are creating a new entity, so we don't want to keep the old one
this.parent.bufferRenderer.clearBuffer();
// Create the new entity with the masked image as its only object
const rect = this.parent.transformer.getRelativeRect();
const arg = {
overrides: {
objects: [imageState],
position: {
x: Math.round(rect.x),
y: Math.round(rect.y),
},
},
isSelected: true,
};
switch (type) {
case 'raster_layer':
this.manager.stateApi.addRasterLayer(arg);
break;
case 'control_layer':
this.manager.stateApi.addControlLayer(arg);
break;
case 'inpaint_mask':
this.manager.stateApi.addInpaintMask(arg);
break;
case 'regional_guidance':
this.manager.stateApi.addRegionalGuidance(arg);
break;
default:
assert<Equals<typeof type, never>>(false);
}
// Final cleanup and teardown, returning user to main canvas UI
this.resetEphemeralState();
this.teardown();
};
/**
* Resets the module (e.g. remove all points and the mask image).
*
@@ -686,12 +781,16 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
if (this.maskedImage) {
this.maskedImage.destroy();
}
if (this.konva.maskTween) {
this.konva.maskTween.destroy();
this.konva.maskTween = null;
}
// Empty internal module state
this.$points.set([]);
this.imageState = null;
this.$imageState.set(null);
this.$pointType.set(1);
this.$hasProcessed.set(false);
this.$lastProcessedHash.set('');
this.$isProcessing.set(false);
// Reset non-ephemeral konva nodes
@@ -706,7 +805,7 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
/**
* Builds a graph for segmenting an image with the given image DTO.
*/
buildGraph = ({ image_name }: ImageDTO): { graph: Graph; outputNodeId: string } => {
buildGraph = ({ image_name }: ImageDTO, points: SAMPointWithId[]): { graph: Graph; outputNodeId: string } => {
const graph = new Graph(getPrefixedId('canvas_segment_anything'));
// TODO(psyche): When SAM2 is available in transformers, use it here
@@ -716,7 +815,7 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
type: 'segment_anything',
model: 'segment-anything-huge',
image: { image_name },
point_lists: [{ points: this.getSAMPoints() }],
point_lists: [{ points: points.map(({ x, y, label }) => ({ x, y, label })) }],
mask_filter: 'largest',
});
@@ -759,11 +858,11 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
label,
circle: getKonvaNodeDebugAttrs(konva.circle),
})),
imageState: deepClone(this.imageState),
imageState: deepClone(this.$imageState.get()),
maskedImage: this.maskedImage?.repr(),
config: deepClone(this.config),
$isSegmenting: this.$isSegmenting.get(),
$hasProcessed: this.$hasProcessed.get(),
$lastProcessedHash: this.$lastProcessedHash.get(),
$isProcessing: this.$isProcessing.get(),
$pointType: this.$pointType.get(),
$pointTypeString: this.$pointTypeString.get(),

View File

@@ -17,12 +17,16 @@ import {
} from 'features/controlLayers/store/canvasSettingsSlice';
import {
bboxChangedFromCanvas,
controlLayerAdded,
entityBrushLineAdded,
entityEraserLineAdded,
entityMoved,
entityRasterized,
entityRectAdded,
entityReset,
inpaintMaskAdded,
rasterLayerAdded,
rgAdded,
} from 'features/controlLayers/store/canvasSlice';
import { selectCanvasStagingAreaSlice } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
@@ -51,6 +55,7 @@ import { getImageDTO } from 'services/api/endpoints/images';
import { queueApi } from 'services/api/endpoints/queue';
import type { BatchConfig, ImageDTO, S } from 'services/api/types';
import { QueueError } from 'services/events/errors';
import type { Param0 } from 'tsafe';
import { assert } from 'tsafe';
import type { CanvasEntityAdapter } from './CanvasEntity/types';
@@ -160,6 +165,34 @@ export class CanvasStateApiModule extends CanvasModuleBase {
this.store.dispatch(entityRectAdded(arg));
};
/**
* Adds a raster layer to the canvas, pushing state to redux.
*/
addRasterLayer = (arg: Param0<typeof rasterLayerAdded>) => {
this.store.dispatch(rasterLayerAdded(arg));
};
/**
* Adds a control layer to the canvas, pushing state to redux.
*/
addControlLayer = (arg: Param0<typeof controlLayerAdded>) => {
this.store.dispatch(controlLayerAdded(arg));
};
/**
* Adds an inpaint mask to the canvas, pushing state to redux.
*/
addInpaintMask = (arg: Param0<typeof inpaintMaskAdded>) => {
this.store.dispatch(inpaintMaskAdded(arg));
};
/**
* Adds regional guidance to the canvas, pushing state to redux.
*/
addRegionalGuidance = (arg: Param0<typeof rgAdded>) => {
this.store.dispatch(rgAdded(arg));
};
/**
* Rasterizes an entity, pushing state to redux.
*/

View File

@@ -126,6 +126,13 @@ export const floorCoord = (coord: Coordinate): Coordinate => {
};
};
export const roundCoord = (coord: Coordinate): Coordinate => {
return {
x: Math.round(coord.x),
y: Math.round(coord.y),
};
};
/**
* Snaps a position to the edge of the given rect if within a threshold of the edge
* @param pos The position to snap

View File

@@ -29,7 +29,7 @@ import { isMainModelBase, zModelIdentifierField } from 'features/nodes/types/com
import { ASPECT_RATIO_MAP } from 'features/parameters/components/Bbox/constants';
import { getGridSize, getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
import type { IRect } from 'konva/lib/types';
import { merge, omit } from 'lodash-es';
import { merge } from 'lodash-es';
import type { UndoableOptions } from 'redux-undo';
import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
@@ -57,13 +57,13 @@ import type {
} from './types';
import { getEntityIdentifier, isRenderableEntity } from './types';
import {
converters,
getControlLayerState,
getInpaintMaskState,
getRasterLayerState,
getReferenceImageState,
getRegionalGuidanceState,
imageDTOToImageWithDims,
initialControlNet,
initialIPAdapter,
} from './util';
@@ -157,28 +157,25 @@ export const canvasSlice = createSlice({
reducer: (
state,
action: PayloadAction<
EntityIdentifierPayload<{ newId: string; overrides?: Partial<CanvasControlLayerState> }, 'raster_layer'>
EntityIdentifierPayload<
{ newId: string; overrides?: Partial<CanvasControlLayerState>; replace?: boolean },
'raster_layer'
>
>
) => {
const { entityIdentifier, newId, overrides } = action.payload;
const { entityIdentifier, newId, overrides, replace } = action.payload;
const layer = selectEntity(state, entityIdentifier);
if (!layer) {
return;
}
// Convert the raster layer to control layer
const controlLayerState: CanvasControlLayerState = {
...deepClone(layer),
id: newId,
type: 'control_layer',
controlAdapter: deepClone(initialControlNet),
withTransparencyEffect: true,
};
const controlLayerState = converters.rasterLayer.toControlLayer(newId, layer, overrides);
merge(controlLayerState, overrides);
// Remove the raster layer
state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id);
if (replace) {
// Remove the raster layer
state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id);
}
// Add the converted control layer
state.controlLayers.entities.push(controlLayerState);
@@ -186,11 +183,90 @@ export const canvasSlice = createSlice({
state.selectedEntityIdentifier = { type: controlLayerState.type, id: controlLayerState.id };
},
prepare: (
payload: EntityIdentifierPayload<{ overrides?: Partial<CanvasControlLayerState> } | undefined, 'raster_layer'>
payload: EntityIdentifierPayload<
{ overrides?: Partial<CanvasControlLayerState>; replace?: boolean } | undefined,
'raster_layer'
>
) => ({
payload: { ...payload, newId: getPrefixedId('control_layer') },
}),
},
rasterLayerConvertedToInpaintMask: {
reducer: (
state,
action: PayloadAction<
EntityIdentifierPayload<
{ newId: string; overrides?: Partial<CanvasInpaintMaskState>; replace?: boolean },
'raster_layer'
>
>
) => {
const { entityIdentifier, newId, overrides, replace } = action.payload;
const layer = selectEntity(state, entityIdentifier);
if (!layer) {
return;
}
// Convert the raster layer to inpaint mask
const inpaintMaskState = converters.rasterLayer.toInpaintMask(newId, layer, overrides);
if (replace) {
// Remove the raster layer
state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id);
}
// Add the converted inpaint mask
state.inpaintMasks.entities.push(inpaintMaskState);
state.selectedEntityIdentifier = { type: inpaintMaskState.type, id: inpaintMaskState.id };
},
prepare: (
payload: EntityIdentifierPayload<
{ overrides?: Partial<CanvasInpaintMaskState>; replace?: boolean } | undefined,
'raster_layer'
>
) => ({
payload: { ...payload, newId: getPrefixedId('inpaint_mask') },
}),
},
rasterLayerConvertedToRegionalGuidance: {
reducer: (
state,
action: PayloadAction<
EntityIdentifierPayload<
{ newId: string; overrides?: Partial<CanvasRegionalGuidanceState>; replace?: boolean },
'raster_layer'
>
>
) => {
const { entityIdentifier, newId, overrides, replace } = action.payload;
const layer = selectEntity(state, entityIdentifier);
if (!layer) {
return;
}
// Convert the raster layer to inpaint mask
const regionalGuidanceState = converters.rasterLayer.toRegionalGuidance(newId, layer, overrides);
if (replace) {
// Remove the raster layer
state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id);
}
// Add the converted inpaint mask
state.regionalGuidance.entities.push(regionalGuidanceState);
state.selectedEntityIdentifier = { type: regionalGuidanceState.type, id: regionalGuidanceState.id };
},
prepare: (
payload: EntityIdentifierPayload<
{ overrides?: Partial<CanvasRegionalGuidanceState>; replace?: boolean } | undefined,
'raster_layer'
>
) => ({
payload: { ...payload, newId: getPrefixedId('regional_guidance') },
}),
},
//#region Control layers
controlLayerAdded: {
reducer: (
@@ -217,32 +293,125 @@ export const canvasSlice = createSlice({
state.selectedEntityIdentifier = { type: 'control_layer', id: data.id };
},
controlLayerConvertedToRasterLayer: {
reducer: (state, action: PayloadAction<EntityIdentifierPayload<{ newId: string }, 'control_layer'>>) => {
const { entityIdentifier, newId } = action.payload;
reducer: (
state,
action: PayloadAction<
EntityIdentifierPayload<
{ newId: string; overrides?: Partial<CanvasRasterLayerState>; replace?: boolean },
'control_layer'
>
>
) => {
const { entityIdentifier, newId, overrides, replace } = action.payload;
const layer = selectEntity(state, entityIdentifier);
if (!layer) {
return;
}
// Convert the raster layer to control layer
const rasterLayerState: CanvasRasterLayerState = {
...omit(deepClone(layer), ['type', 'controlAdapter', 'withTransparencyEffect']),
id: newId,
type: 'raster_layer',
};
const rasterLayerState = converters.controlLayer.toRasterLayer(newId, layer, overrides);
// Remove the control layer
state.controlLayers.entities = state.controlLayers.entities.filter((layer) => layer.id !== entityIdentifier.id);
if (replace) {
// Remove the control layer
state.controlLayers.entities = state.controlLayers.entities.filter(
(layer) => layer.id !== entityIdentifier.id
);
}
// Add the new raster layer
state.rasterLayers.entities.push(rasterLayerState);
state.selectedEntityIdentifier = { type: rasterLayerState.type, id: rasterLayerState.id };
},
prepare: (payload: EntityIdentifierPayload<void, 'control_layer'>) => ({
prepare: (
payload: EntityIdentifierPayload<
{ overrides?: Partial<CanvasRasterLayerState>; replace?: boolean } | undefined,
'control_layer'
>
) => ({
payload: { ...payload, newId: getPrefixedId('raster_layer') },
}),
},
controlLayerConvertedToInpaintMask: {
reducer: (
state,
action: PayloadAction<
EntityIdentifierPayload<
{ newId: string; overrides?: Partial<CanvasInpaintMaskState>; replace?: boolean },
'control_layer'
>
>
) => {
const { entityIdentifier, newId, overrides, replace } = action.payload;
const layer = selectEntity(state, entityIdentifier);
if (!layer) {
return;
}
// Convert the control layer to inpaint mask
const inpaintMaskState = converters.controlLayer.toInpaintMask(newId, layer, overrides);
if (replace) {
// Remove the control layer
state.controlLayers.entities = state.controlLayers.entities.filter(
(layer) => layer.id !== entityIdentifier.id
);
}
// Add the new inpaint mask
state.inpaintMasks.entities.push(inpaintMaskState);
state.selectedEntityIdentifier = { type: inpaintMaskState.type, id: inpaintMaskState.id };
},
prepare: (
payload: EntityIdentifierPayload<
{ overrides?: Partial<CanvasInpaintMaskState>; replace?: boolean } | undefined,
'control_layer'
>
) => ({
payload: { ...payload, newId: getPrefixedId('inpaint_mask') },
}),
},
controlLayerConvertedToRegionalGuidance: {
reducer: (
state,
action: PayloadAction<
EntityIdentifierPayload<
{ newId: string; overrides?: Partial<CanvasRegionalGuidanceState>; replace?: boolean },
'control_layer'
>
>
) => {
const { entityIdentifier, newId, overrides, replace } = action.payload;
const layer = selectEntity(state, entityIdentifier);
if (!layer) {
return;
}
// Convert the control layer to regional guidance
const regionalGuidanceState = converters.controlLayer.toRegionalGuidance(newId, layer, overrides);
if (replace) {
// Remove the control layer
state.controlLayers.entities = state.controlLayers.entities.filter(
(layer) => layer.id !== entityIdentifier.id
);
}
// Add the new regional guidance
state.regionalGuidance.entities.push(regionalGuidanceState);
state.selectedEntityIdentifier = { type: regionalGuidanceState.type, id: regionalGuidanceState.id };
},
prepare: (
payload: EntityIdentifierPayload<
{ overrides?: Partial<CanvasRegionalGuidanceState>; replace?: boolean } | undefined,
'control_layer'
>
) => ({
payload: { ...payload, newId: getPrefixedId('regional_guidance') },
}),
},
controlLayerModelChanged: (
state,
action: PayloadAction<
@@ -447,6 +616,46 @@ export const canvasSlice = createSlice({
state.regionalGuidance.entities.push(data);
state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id };
},
rgConvertedToInpaintMask: {
reducer: (
state,
action: PayloadAction<
EntityIdentifierPayload<
{ newId: string; overrides?: Partial<CanvasInpaintMaskState>; replace?: boolean },
'regional_guidance'
>
>
) => {
const { entityIdentifier, newId, overrides, replace } = action.payload;
const layer = selectEntity(state, entityIdentifier);
if (!layer) {
return;
}
// Convert the regional guidance to inpaint mask
const inpaintMaskState = converters.regionalGuidance.toInpaintMask(newId, layer, overrides);
if (replace) {
// Remove the regional guidance
state.regionalGuidance.entities = state.regionalGuidance.entities.filter(
(layer) => layer.id !== entityIdentifier.id
);
}
// Add the new inpaint mask
state.inpaintMasks.entities.push(inpaintMaskState);
state.selectedEntityIdentifier = { type: inpaintMaskState.type, id: inpaintMaskState.id };
},
prepare: (
payload: EntityIdentifierPayload<
{ overrides?: Partial<CanvasInpaintMaskState>; replace?: boolean } | undefined,
'regional_guidance'
>
) => ({
payload: { ...payload, newId: getPrefixedId('inpaint_mask') },
}),
},
rgPositivePromptChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ prompt: string | null }, 'regional_guidance'>>
@@ -644,6 +853,44 @@ export const canvasSlice = createSlice({
state.inpaintMasks.entities = [data];
state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id };
},
inpaintMaskConvertedToRegionalGuidance: {
reducer: (
state,
action: PayloadAction<
EntityIdentifierPayload<
{ newId: string; overrides?: Partial<CanvasRegionalGuidanceState>; replace?: boolean },
'inpaint_mask'
>
>
) => {
const { entityIdentifier, newId, overrides, replace } = action.payload;
const layer = selectEntity(state, entityIdentifier);
if (!layer) {
return;
}
// Convert the inpaint mask to regional guidance
const regionalGuidanceState = converters.inpaintMask.toRegionalGuidance(newId, layer, overrides);
if (replace) {
// Remove the inpaint mask
state.inpaintMasks.entities = state.inpaintMasks.entities.filter((layer) => layer.id !== entityIdentifier.id);
}
// Add the new regional guidance
state.regionalGuidance.entities.push(regionalGuidanceState);
state.selectedEntityIdentifier = { type: regionalGuidanceState.type, id: regionalGuidanceState.id };
},
prepare: (
payload: EntityIdentifierPayload<
{ overrides?: Partial<CanvasRegionalGuidanceState>; replace?: boolean } | undefined,
'inpaint_mask'
>
) => ({
payload: { ...payload, newId: getPrefixedId('regional_guidance') },
}),
},
//#region BBox
bboxScaledWidthChanged: (state, action: PayloadAction<number>) => {
const gridSize = getGridSize(state.bbox.modelBase);
@@ -1210,10 +1457,14 @@ export const {
rasterLayerAdded,
// rasterLayerRecalled,
rasterLayerConvertedToControlLayer,
rasterLayerConvertedToInpaintMask,
rasterLayerConvertedToRegionalGuidance,
// Control layers
controlLayerAdded,
// controlLayerRecalled,
controlLayerConvertedToRasterLayer,
controlLayerConvertedToInpaintMask,
controlLayerConvertedToRegionalGuidance,
controlLayerModelChanged,
controlLayerControlModeChanged,
controlLayerWeightChanged,
@@ -1231,6 +1482,7 @@ export const {
// Regions
rgAdded,
// rgRecalled,
rgConvertedToInpaintMask,
rgPositivePromptChanged,
rgNegativePromptChanged,
rgAutoNegativeToggled,
@@ -1244,6 +1496,7 @@ export const {
rgIPAdapterCLIPVisionModelChanged,
// Inpaint mask
inpaintMaskAdded,
inpaintMaskConvertedToRegionalGuidance,
// inpaintMaskRecalled,
} = canvasSlice.actions;

View File

@@ -131,7 +131,8 @@ const zSAMPoint = z.object({
y: z.number().int().gte(0),
label: zSAMPointLabel,
});
export type SAMPoint = z.infer<typeof zSAMPoint>;
type SAMPoint = z.infer<typeof zSAMPoint>;
export type SAMPointWithId = SAMPoint & { id: string };
const zRect = z.object({
x: z.number(),

View File

@@ -184,3 +184,153 @@ export const getInpaintMaskState = (
merge(entityState, overrides);
return entityState;
};
const convertRasterLayerToControlLayer = (
newId: string,
rasterLayerState: CanvasRasterLayerState,
overrides?: Partial<CanvasControlLayerState>
): CanvasControlLayerState => {
const { name, objects, position } = rasterLayerState;
const controlLayerState = getControlLayerState(newId, {
name,
objects,
position,
});
merge(controlLayerState, overrides);
return controlLayerState;
};
const convertRasterLayerToInpaintMask = (
newId: string,
rasterLayerState: CanvasRasterLayerState,
overrides?: Partial<CanvasInpaintMaskState>
): CanvasInpaintMaskState => {
const { name, objects, position } = rasterLayerState;
const inpaintMaskState = getInpaintMaskState(newId, {
name,
objects,
position,
});
merge(inpaintMaskState, overrides);
return inpaintMaskState;
};
const convertRasterLayerToRegionalGuidance = (
newId: string,
rasterLayerState: CanvasRasterLayerState,
overrides?: Partial<CanvasRegionalGuidanceState>
): CanvasRegionalGuidanceState => {
const { name, objects, position } = rasterLayerState;
const regionalGuidanceState = getRegionalGuidanceState(newId, {
name,
objects,
position,
});
merge(regionalGuidanceState, overrides);
return regionalGuidanceState;
};
const convertControlLayerToRasterLayer = (
newId: string,
controlLayerState: CanvasControlLayerState,
overrides?: Partial<CanvasRasterLayerState>
): CanvasRasterLayerState => {
const { name, objects, position } = controlLayerState;
const rasterLayerState = getRasterLayerState(newId, {
name,
objects,
position,
});
merge(rasterLayerState, overrides);
return rasterLayerState;
};
const convertControlLayerToInpaintMask = (
newId: string,
rasterLayerState: CanvasControlLayerState,
overrides?: Partial<CanvasInpaintMaskState>
): CanvasInpaintMaskState => {
const { name, objects, position } = rasterLayerState;
const inpaintMaskState = getInpaintMaskState(newId, {
name,
objects,
position,
});
merge(inpaintMaskState, overrides);
return inpaintMaskState;
};
const convertControlLayerToRegionalGuidance = (
newId: string,
rasterLayerState: CanvasControlLayerState,
overrides?: Partial<CanvasRegionalGuidanceState>
): CanvasRegionalGuidanceState => {
const { name, objects, position } = rasterLayerState;
const regionalGuidanceState = getRegionalGuidanceState(newId, {
name,
objects,
position,
});
merge(regionalGuidanceState, overrides);
return regionalGuidanceState;
};
const convertInpaintMaskToRegionalGuidance = (
newId: string,
inpaintMaskState: CanvasInpaintMaskState,
overrides?: Partial<CanvasRegionalGuidanceState>
): CanvasRegionalGuidanceState => {
const { name, objects, position } = inpaintMaskState;
const regionalGuidanceState = getRegionalGuidanceState(newId, {
name,
objects,
position,
});
merge(regionalGuidanceState, overrides);
return regionalGuidanceState;
};
const convertRegionalGuidanceToInpaintMask = (
newId: string,
regionalGuidanceState: CanvasRegionalGuidanceState,
overrides?: Partial<CanvasInpaintMaskState>
): CanvasInpaintMaskState => {
const { name, objects, position } = regionalGuidanceState;
const inpaintMaskState = getInpaintMaskState(newId, {
name,
objects,
position,
});
merge(inpaintMaskState, overrides);
return inpaintMaskState;
};
/**
* Supported conversions:
* - Raster Layer -> Control Layer
* - Raster Layer -> Inpaint Mask
* - Raster Layer -> Regional Guidance
* - Control Layer -> Control Layer
* - Control Layer -> Inpaint Mask
* - Control Layer -> Regional Guidance
* - Inpaint Mask -> Regional Guidance
* - Regional Guidance -> Inpaint Mask
*/
export const converters = {
rasterLayer: {
toControlLayer: convertRasterLayerToControlLayer,
toInpaintMask: convertRasterLayerToInpaintMask,
toRegionalGuidance: convertRasterLayerToRegionalGuidance,
},
controlLayer: {
toRasterLayer: convertControlLayerToRasterLayer,
toInpaintMask: convertControlLayerToInpaintMask,
toRegionalGuidance: convertControlLayerToRegionalGuidance,
},
inpaintMask: {
toRegionalGuidance: convertInpaintMaskToRegionalGuidance,
},
regionalGuidance: {
toInpaintMask: convertRegionalGuidanceToInpaintMask,
},
};

View File

@@ -1,4 +1,4 @@
import { Flex, Link, Spacer, Text } from '@invoke-ai/ui-library';
import { Link } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $projectName, $projectUrl } from 'app/store/nanostores/projectId';
import { memo } from 'react';
@@ -9,15 +9,13 @@ export const GalleryHeader = memo(() => {
if (projectName && projectUrl) {
return (
<Flex gap={2} alignItems="center" justifyContent="space-evenly" pe={2} w="50%">
<Text fontSize="md" fontWeight="semibold" noOfLines={1} wordBreak="break-all" w="full" textAlign="center">
<Link href={projectUrl}>{projectName}</Link>
</Text>
</Flex>
<Link fontSize="md" fontWeight="semibold" noOfLines={1} wordBreak="break-all" href={projectUrl}>
{projectName}
</Link>
);
}
return <Spacer />;
return null;
});
GalleryHeader.displayName = 'GalleryHeader';

View File

@@ -51,8 +51,8 @@ const GalleryPanelContent = () => {
return (
<Flex ref={galleryPanelFocusRef} position="relative" flexDirection="column" h="full" w="full" tabIndex={-1}>
<Flex alignItems="center" w="full">
<Flex w="25%">
<Flex alignItems="center" justifyContent="space-between" w="full">
<Flex flexGrow={1} flexBasis={0}>
<Button
size="sm"
variant="ghost"
@@ -62,8 +62,10 @@ const GalleryPanelContent = () => {
{boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
</Button>
</Flex>
<GalleryHeader />
<Flex h="full" w="25%" justifyContent="flex-end">
<Flex>
<GalleryHeader />
</Flex>
<Flex flexGrow={1} flexBasis={0} justifyContent="flex-end">
<BoardsSettingsPopover />
<IconButton
size="sm"

View File

@@ -1,9 +1,11 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowBendUpLeftBold,
PiArrowsCounterClockwiseBold,
PiAsteriskBold,
PiPaintBrushBold,
@@ -14,28 +16,36 @@ import {
export const ImageMenuItemMetadataRecallActions = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
const subMenu = useSubMenu();
const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, createAsPreset } =
useImageActions(imageDTO);
return (
<>
<MenuItem icon={<PiArrowsCounterClockwiseBold />} onClickCapture={remix} isDisabled={!hasMetadata}>
{t('parameters.remixImage')}
</MenuItem>
<MenuItem icon={<PiQuotesBold />} onClickCapture={recallPrompts} isDisabled={!hasPrompts}>
{t('parameters.usePrompt')}
</MenuItem>
<MenuItem icon={<PiPlantBold />} onClickCapture={recallSeed} isDisabled={!hasSeed}>
{t('parameters.useSeed')}
</MenuItem>
<MenuItem icon={<PiAsteriskBold />} onClickCapture={recallAll} isDisabled={!hasMetadata}>
{t('parameters.useAll')}
</MenuItem>
<MenuItem icon={<PiPaintBrushBold />} onClickCapture={createAsPreset} isDisabled={!hasPrompts}>
{t('stylePresets.useForTemplate')}
</MenuItem>
</>
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiArrowBendUpLeftBold />}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label="Recall Metadata" />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem icon={<PiArrowsCounterClockwiseBold />} onClick={remix} isDisabled={!hasMetadata}>
{t('parameters.remixImage')}
</MenuItem>
<MenuItem icon={<PiQuotesBold />} onClick={recallPrompts} isDisabled={!hasPrompts}>
{t('parameters.usePrompt')}
</MenuItem>
<MenuItem icon={<PiPlantBold />} onClick={recallSeed} isDisabled={!hasSeed}>
{t('parameters.useSeed')}
</MenuItem>
<MenuItem icon={<PiAsteriskBold />} onClick={recallAll} isDisabled={!hasMetadata}>
{t('parameters.useAll')}
</MenuItem>
<MenuItem icon={<PiPaintBrushBold />} onClick={createAsPreset} isDisabled={!hasPrompts}>
{t('stylePresets.useForTemplate')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
);
});

View File

@@ -21,9 +21,12 @@ export const useBuildModelInstallArg = () => {
});
const getIsInstalled = useCallback(
({ source, name, base, type, is_installed }: StarterModel): boolean =>
({ source, name, base, type, is_installed, previous_names }: StarterModel): boolean =>
modelList.some(
(mc) => is_installed || source === mc.source || (base === mc.base && name === mc.name && type === mc.type)
(mc) =>
is_installed ||
source === mc.source ||
(base === mc.base && (name === mc.name || previous_names?.includes(name)) && type === mc.type)
),
[modelList]
);

View File

@@ -1,4 +1,4 @@
import { Button, Flex, Text, Tooltip } from '@invoke-ai/ui-library';
import { Button, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-ai/ui-library';
import { flattenStarterModel, useBuildModelInstallArg } from 'features/modelManagerV2/hooks/useBuildModelsToInstall';
import { isMainModelBase } from 'features/nodes/types/common';
import { MODEL_TYPE_SHORT_MAP } from 'features/parameters/types/constants';
@@ -44,8 +44,15 @@ export const StarterBundle = ({ bundleName, bundle }: { bundleName: string; bund
return (
<Tooltip
label={
<Flex flexDir="column">
<Text>{t('modelManager.includesNModels', { n: bundle.length })}</Text>
<Flex flexDir="column" p={1}>
<Text>{t('modelManager.includesNModels', { n: bundle.length })}:</Text>
<UnorderedList>
{bundle.map((model, index) => (
<ListItem key={index} wordBreak="break-all">
{model.name}
</ListItem>
))}
</UnorderedList>
</Flex>
}
>

View File

@@ -1,14 +1,4 @@
import {
Box,
Flex,
Icon,
IconButton,
Input,
InputGroup,
InputRightElement,
Text,
Tooltip,
} from '@invoke-ai/ui-library';
import { Flex, Icon, IconButton, Input, InputGroup, InputRightElement, Text, Tooltip } from '@invoke-ai/ui-library';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { map, size } from 'lodash-es';
import type { ChangeEventHandler } from 'react';
@@ -59,14 +49,14 @@ export const StarterModelsResults = memo(({ results }: StarterModelsResultsProps
<Flex justifyContent="space-between" alignItems="center">
{size(results.starter_bundles) > 0 && (
<Flex gap={4} alignItems="center">
<Flex gap={1} alignItems="center">
<Flex gap={2} alignItems="center">
<Text color="base.200" fontWeight="semibold">
{t('modelManager.starterBundles')}
</Text>
<Tooltip label={t('modelManager.starterBundleHelpText')}>
<Box>
<Flex alignItems="center">
<Icon as={PiInfoBold} color="base.200" />
</Box>
</Flex>
</Tooltip>
</Flex>
<Flex gap={2}>

View File

@@ -106,10 +106,12 @@ export const getInfill = (
}
if (infillMethod === 'color') {
const { a, ...rgb } = infillColorValue;
const color = { ...rgb, a: Math.round(a * 255) };
return g.addNode({
id: 'infill_rgba',
type: 'infill_rgba',
color: infillColorValue,
color,
});
}

View File

@@ -14731,7 +14731,7 @@ export type components = {
bounding_boxes?: components["schemas"]["BoundingBoxField"][] | null;
/**
* Point Lists
* @description The points to prompt the SAM model with.
* @description The list of point lists to prompt the SAM model with. Each list of points represents a single object.
* @default null
*/
point_lists?: components["schemas"]["SAMPointsField"][] | null;
@@ -15347,6 +15347,11 @@ export type components = {
* @default false
*/
is_installed?: boolean;
/**
* Previous Names
* @default []
*/
previous_names?: string[];
/** Dependencies */
dependencies?: components["schemas"]["StarterModelWithoutDependencies"][] | null;
};
@@ -15375,6 +15380,11 @@ export type components = {
* @default false
*/
is_installed?: boolean;
/**
* Previous Names
* @default []
*/
previous_names?: string[];
};
/**
* Step Param Easing

View File

@@ -89,7 +89,7 @@ dependencies = [
"pypatchmatch",
'pyperclip',
"pyreadline3",
"python-multipart",
"python-multipart==0.0.12",
"requests~=2.28.2",
"rich~=13.3",
"scikit-image~=0.21.0",