mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Rebases against development
This commit is contained in:
parent
248068fe5d
commit
6c7191712f
@ -7,10 +7,13 @@ import traceback
|
|||||||
import math
|
import math
|
||||||
import io
|
import io
|
||||||
import base64
|
import base64
|
||||||
|
import os
|
||||||
|
|
||||||
from flask import Flask, redirect, send_from_directory
|
from werkzeug.utils import secure_filename
|
||||||
|
from flask import Flask, redirect, send_from_directory, flash, request, url_for, jsonify
|
||||||
from flask_socketio import SocketIO
|
from flask_socketio import SocketIO
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from PIL.Image import Image as ImageType
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from threading import Event
|
from threading import Event
|
||||||
|
|
||||||
@ -19,6 +22,9 @@ from ldm.invoke.pngwriter import PngWriter, retrieve_metadata
|
|||||||
from ldm.invoke.prompt_parser import split_weighted_subprompts
|
from ldm.invoke.prompt_parser import split_weighted_subprompts
|
||||||
|
|
||||||
from backend.modules.parameters import parameters_to_command
|
from backend.modules.parameters import parameters_to_command
|
||||||
|
from backend.modules.get_outpainting_generation_mode import (
|
||||||
|
get_outpainting_generation_mode,
|
||||||
|
)
|
||||||
|
|
||||||
# Loading Arguments
|
# Loading Arguments
|
||||||
opt = Args()
|
opt = Args()
|
||||||
@ -91,6 +97,43 @@ class InvokeAIWebServer:
|
|||||||
else:
|
else:
|
||||||
return send_from_directory(self.app.static_folder, "index.html")
|
return send_from_directory(self.app.static_folder, "index.html")
|
||||||
|
|
||||||
|
@self.app.route("/upload", methods=["POST"])
|
||||||
|
def upload_base64_file():
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
dataURL = data["dataURL"]
|
||||||
|
name = data["name"]
|
||||||
|
|
||||||
|
print(f'>> Image upload requested "{name}"')
|
||||||
|
|
||||||
|
if dataURL is not None:
|
||||||
|
bytes = dataURL_to_bytes(dataURL)
|
||||||
|
|
||||||
|
file_path = self.save_file_unique_uuid_name(
|
||||||
|
bytes=bytes, name=name, path=self.result_path
|
||||||
|
)
|
||||||
|
|
||||||
|
mtime = os.path.getmtime(file_path)
|
||||||
|
(width, height) = Image.open(file_path).size
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"url": self.get_url_from_image_path(file_path),
|
||||||
|
"mtime": mtime,
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
"category": "result",
|
||||||
|
"destination": "outpainting_merge",
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
return "No dataURL provided"
|
||||||
|
except Exception as e:
|
||||||
|
self.socketio.emit("error", {"message": (str(e))})
|
||||||
|
print("\n")
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
print("\n")
|
||||||
|
|
||||||
self.load_socketio_listeners(self.socketio)
|
self.load_socketio_listeners(self.socketio)
|
||||||
|
|
||||||
if args.gui:
|
if args.gui:
|
||||||
@ -308,19 +351,24 @@ class InvokeAIWebServer:
|
|||||||
generation_parameters, esrgan_parameters, facetool_parameters
|
generation_parameters, esrgan_parameters, facetool_parameters
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
# truncate long init_mask base64 if needed
|
# truncate long init_mask/init_img base64 if needed
|
||||||
if "init_mask" in generation_parameters:
|
|
||||||
printable_parameters = {
|
printable_parameters = {
|
||||||
**generation_parameters,
|
**generation_parameters,
|
||||||
"init_mask": generation_parameters["init_mask"][:20] + "...",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if "init_img" in generation_parameters:
|
||||||
|
printable_parameters["init_img"] = (
|
||||||
|
printable_parameters["init_img"][:64] + "..."
|
||||||
|
)
|
||||||
|
|
||||||
|
if "init_mask" in generation_parameters:
|
||||||
|
printable_parameters["init_mask"] = (
|
||||||
|
printable_parameters["init_mask"][:64] + "..."
|
||||||
|
)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f">> Image generation requested: {printable_parameters}\nESRGAN parameters: {esrgan_parameters}\nFacetool parameters: {facetool_parameters}"
|
f">> Image generation requested: {printable_parameters}\nESRGAN parameters: {esrgan_parameters}\nFacetool parameters: {facetool_parameters}"
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f">> Image generation requested: {generation_parameters}\nESRGAN parameters: {esrgan_parameters}\nFacetool parameters: {facetool_parameters}"
|
|
||||||
)
|
|
||||||
self.generate_images(
|
self.generate_images(
|
||||||
generation_parameters,
|
generation_parameters,
|
||||||
esrgan_parameters,
|
esrgan_parameters,
|
||||||
@ -456,7 +504,7 @@ class InvokeAIWebServer:
|
|||||||
from send2trash import send2trash
|
from send2trash import send2trash
|
||||||
|
|
||||||
path = self.get_image_path_from_url(url)
|
path = self.get_image_path_from_url(url)
|
||||||
print(path)
|
|
||||||
send2trash(path)
|
send2trash(path)
|
||||||
socketio.emit(
|
socketio.emit(
|
||||||
"imageDeleted",
|
"imageDeleted",
|
||||||
@ -479,7 +527,7 @@ class InvokeAIWebServer:
|
|||||||
)
|
)
|
||||||
mtime = os.path.getmtime(file_path)
|
mtime = os.path.getmtime(file_path)
|
||||||
(width, height) = Image.open(file_path).size
|
(width, height) = Image.open(file_path).size
|
||||||
print(file_path)
|
|
||||||
socketio.emit(
|
socketio.emit(
|
||||||
"imageUploaded",
|
"imageUploaded",
|
||||||
{
|
{
|
||||||
@ -499,17 +547,18 @@ class InvokeAIWebServer:
|
|||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
# TODO: I think this needs a safety mechanism.
|
# TODO: I think this needs a safety mechanism.
|
||||||
@socketio.on("uploadMaskImage")
|
@socketio.on("uploadOutpaintingMergeImage")
|
||||||
def handle_upload_mask_image(bytes, name):
|
def handle_upload_outpainting_merge_image(dataURL, name):
|
||||||
try:
|
try:
|
||||||
print(f'>> Mask image upload requested "{name}"')
|
print(f'>> Outpainting merge image upload requested "{name}"')
|
||||||
|
|
||||||
file_path = self.save_file_unique_uuid_name(
|
image = dataURL_to_image(dataURL)
|
||||||
bytes=bytes, name=name, path=self.mask_image_path
|
file_name = self.make_unique_init_image_filename(name)
|
||||||
)
|
file_path = os.path.join(self.result_path, file_name)
|
||||||
|
image.save(file_path)
|
||||||
|
|
||||||
socketio.emit(
|
socketio.emit(
|
||||||
"maskImageUploaded",
|
"outpaintingMergeImageUploaded",
|
||||||
{
|
{
|
||||||
"url": self.get_url_from_image_path(file_path),
|
"url": self.get_url_from_image_path(file_path),
|
||||||
},
|
},
|
||||||
@ -546,59 +595,146 @@ class InvokeAIWebServer:
|
|||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
|
|
||||||
|
actual_generation_mode = generation_parameters["generation_mode"]
|
||||||
|
original_bounding_box = None
|
||||||
"""
|
"""
|
||||||
TODO:
|
TODO:
|
||||||
If a result image is used as an init image, and then deleted, we will want to be
|
If a result image is used as an init image, and then deleted, we will want to be
|
||||||
able to use it as an init image in the future. Need to handle this case.
|
able to use it as an init image in the future. Need to handle this case.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# We need to give absolute paths to the generator, stash the URLs for later
|
"""
|
||||||
init_img_url = None
|
Prepare for generation based on generation_mode
|
||||||
mask_img_url = None
|
"""
|
||||||
|
if generation_parameters["generation_mode"] == "outpainting":
|
||||||
|
"""
|
||||||
|
generation_parameters["init_img"] is a base64 image
|
||||||
|
generation_parameters["init_mask"] is a base64 image
|
||||||
|
|
||||||
if "init_img" in generation_parameters:
|
So we need to convert each into a PIL Image.
|
||||||
|
"""
|
||||||
|
|
||||||
|
truncated_outpaint_image_b64 = generation_parameters["init_img"][:64]
|
||||||
|
truncated_outpaint_mask_b64 = generation_parameters["init_mask"][:64]
|
||||||
|
|
||||||
|
outpaint_image = dataURL_to_image(
|
||||||
|
generation_parameters["init_img"]
|
||||||
|
).convert("RGBA")
|
||||||
|
|
||||||
|
# Convert mask dataURL to an image and convert to greyscale
|
||||||
|
outpaint_mask = dataURL_to_image(
|
||||||
|
generation_parameters["init_mask"]
|
||||||
|
).convert("L")
|
||||||
|
|
||||||
|
actual_generation_mode = get_outpainting_generation_mode(
|
||||||
|
outpaint_image, outpaint_mask
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
The outpaint image and mask are pre-cropped by the UI, so the bounding box we pass
|
||||||
|
to the generator should be:
|
||||||
|
{
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"width": original_bounding_box["width"],
|
||||||
|
"height": original_bounding_box["height"]
|
||||||
|
}
|
||||||
|
|
||||||
|
Save the original bounding box, we need to give it back to the UI when finished,
|
||||||
|
because the UI needs to know where to put the inpainted image on the canvas.
|
||||||
|
"""
|
||||||
|
original_bounding_box = generation_parameters["bounding_box"].copy()
|
||||||
|
|
||||||
|
generation_parameters["bounding_box"]["x"] = 0
|
||||||
|
generation_parameters["bounding_box"]["y"] = 0
|
||||||
|
|
||||||
|
"""
|
||||||
|
Apply the mask to the init image, creating a "mask" image with
|
||||||
|
transparency where inpainting should occur. This is the kind of
|
||||||
|
mask that prompt2image() needs.
|
||||||
|
"""
|
||||||
|
alpha_mask = outpaint_image.copy()
|
||||||
|
alpha_mask.putalpha(outpaint_mask)
|
||||||
|
|
||||||
|
generation_parameters["init_img"] = outpaint_image
|
||||||
|
generation_parameters["init_mask"] = alpha_mask
|
||||||
|
|
||||||
|
# Remove the unneeded parameters for whichever mode we are doing
|
||||||
|
if actual_generation_mode == "inpainting":
|
||||||
|
generation_parameters.pop("seam_size", None)
|
||||||
|
generation_parameters.pop("seam_blur", None)
|
||||||
|
generation_parameters.pop("seam_strength", None)
|
||||||
|
generation_parameters.pop("seam_steps", None)
|
||||||
|
generation_parameters.pop("tile_size", None)
|
||||||
|
generation_parameters.pop("force_outpaint", None)
|
||||||
|
elif actual_generation_mode == "img2img":
|
||||||
|
generation_parameters["height"] = original_bounding_box["height"]
|
||||||
|
generation_parameters["width"] = original_bounding_box["width"]
|
||||||
|
generation_parameters.pop("init_mask", None)
|
||||||
|
generation_parameters.pop("seam_size", None)
|
||||||
|
generation_parameters.pop("seam_blur", None)
|
||||||
|
generation_parameters.pop("seam_strength", None)
|
||||||
|
generation_parameters.pop("seam_steps", None)
|
||||||
|
generation_parameters.pop("tile_size", None)
|
||||||
|
generation_parameters.pop("force_outpaint", None)
|
||||||
|
elif actual_generation_mode == "txt2img":
|
||||||
|
generation_parameters["height"] = original_bounding_box["height"]
|
||||||
|
generation_parameters["width"] = original_bounding_box["width"]
|
||||||
|
generation_parameters.pop("strength", None)
|
||||||
|
generation_parameters.pop("fit", None)
|
||||||
|
generation_parameters.pop("init_img", None)
|
||||||
|
generation_parameters.pop("init_mask", None)
|
||||||
|
generation_parameters.pop("seam_size", None)
|
||||||
|
generation_parameters.pop("seam_blur", None)
|
||||||
|
generation_parameters.pop("seam_strength", None)
|
||||||
|
generation_parameters.pop("seam_steps", None)
|
||||||
|
generation_parameters.pop("tile_size", None)
|
||||||
|
generation_parameters.pop("force_outpaint", None)
|
||||||
|
|
||||||
|
elif generation_parameters["generation_mode"] == "inpainting":
|
||||||
|
"""
|
||||||
|
generation_parameters["init_img"] is a url
|
||||||
|
generation_parameters["init_mask"] is a base64 image
|
||||||
|
|
||||||
|
So we need to convert each into a PIL Image.
|
||||||
|
"""
|
||||||
init_img_url = generation_parameters["init_img"]
|
init_img_url = generation_parameters["init_img"]
|
||||||
|
truncated_outpaint_mask_b64 = generation_parameters["init_mask"][:64]
|
||||||
|
|
||||||
|
init_img_url = generation_parameters["init_img"]
|
||||||
|
|
||||||
init_img_path = self.get_image_path_from_url(init_img_url)
|
init_img_path = self.get_image_path_from_url(init_img_url)
|
||||||
generation_parameters["init_img"] = init_img_path
|
|
||||||
|
|
||||||
# if 'init_mask' in generation_parameters:
|
|
||||||
# mask_img_url = generation_parameters['init_mask']
|
|
||||||
# generation_parameters[
|
|
||||||
# 'init_mask'
|
|
||||||
# ] = self.get_image_path_from_url(
|
|
||||||
# generation_parameters['init_mask']
|
|
||||||
# )
|
|
||||||
|
|
||||||
if "init_mask" in generation_parameters:
|
|
||||||
# grab an Image of the init image
|
|
||||||
original_image = Image.open(init_img_path)
|
original_image = Image.open(init_img_path)
|
||||||
|
|
||||||
|
rgba_image = original_image.convert("RGBA")
|
||||||
|
|
||||||
# copy a region from it which we will inpaint
|
# copy a region from it which we will inpaint
|
||||||
cropped_init_image = copy_image_from_bounding_box(
|
cropped_init_image = copy_image_from_bounding_box(
|
||||||
original_image, **generation_parameters["bounding_box"]
|
rgba_image, **generation_parameters["bounding_box"]
|
||||||
)
|
)
|
||||||
|
|
||||||
generation_parameters["init_img"] = cropped_init_image
|
generation_parameters["init_img"] = cropped_init_image
|
||||||
|
|
||||||
if generation_parameters["is_mask_empty"]:
|
# Convert mask dataURL to an image and convert to greyscale
|
||||||
generation_parameters["init_mask"] = None
|
mask_image = dataURL_to_image(
|
||||||
else:
|
generation_parameters["init_mask"]
|
||||||
# grab an Image of the mask
|
).convert("L")
|
||||||
mask_image = Image.open(
|
|
||||||
io.BytesIO(
|
|
||||||
base64.decodebytes(
|
|
||||||
bytes(generation_parameters["init_mask"], "utf-8")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
generation_parameters["init_mask"] = mask_image
|
|
||||||
|
|
||||||
totalSteps = self.calculate_real_steps(
|
"""
|
||||||
steps=generation_parameters["steps"],
|
Apply the mask to the init image, creating a "mask" image with
|
||||||
strength=generation_parameters["strength"]
|
transparency where inpainting should occur. This is the kind of
|
||||||
if "strength" in generation_parameters
|
mask that prompt2image() needs.
|
||||||
else None,
|
"""
|
||||||
has_init_image="init_img" in generation_parameters,
|
alpha_mask = cropped_init_image.copy()
|
||||||
)
|
alpha_mask.putalpha(mask_image)
|
||||||
|
|
||||||
|
generation_parameters["init_mask"] = alpha_mask
|
||||||
|
|
||||||
|
elif generation_parameters["generation_mode"] == "img2img":
|
||||||
|
init_img_url = generation_parameters["init_img"]
|
||||||
|
init_img_path = self.get_image_path_from_url(init_img_url)
|
||||||
|
generation_parameters["init_img"] = init_img_path
|
||||||
|
|
||||||
progress = Progress(generation_parameters=generation_parameters)
|
progress = Progress(generation_parameters=generation_parameters)
|
||||||
|
|
||||||
@ -613,13 +749,22 @@ class InvokeAIWebServer:
|
|||||||
nonlocal generation_parameters
|
nonlocal generation_parameters
|
||||||
nonlocal progress
|
nonlocal progress
|
||||||
|
|
||||||
|
generation_messages = {
|
||||||
|
"txt2img": "Text to Image",
|
||||||
|
"img2img": "Image to Image",
|
||||||
|
"inpainting": "Inpainting",
|
||||||
|
"outpainting": "Outpainting",
|
||||||
|
}
|
||||||
|
|
||||||
progress.set_current_step(step + 1)
|
progress.set_current_step(step + 1)
|
||||||
progress.set_current_status("Generating")
|
progress.set_current_status(
|
||||||
|
f"Generating ({generation_messages[actual_generation_mode]})"
|
||||||
|
)
|
||||||
progress.set_current_status_has_steps(True)
|
progress.set_current_status_has_steps(True)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
generation_parameters["progress_images"]
|
generation_parameters["progress_images"]
|
||||||
and step % generation_parameters['save_intermediates'] == 0
|
and step % generation_parameters["save_intermediates"] == 0
|
||||||
and step < generation_parameters["steps"] - 1
|
and step < generation_parameters["steps"] - 1
|
||||||
):
|
):
|
||||||
image = self.generate.sample_to_image(sample)
|
image = self.generate.sample_to_image(sample)
|
||||||
@ -648,6 +793,8 @@ class InvokeAIWebServer:
|
|||||||
"metadata": metadata,
|
"metadata": metadata,
|
||||||
"width": width,
|
"width": width,
|
||||||
"height": height,
|
"height": height,
|
||||||
|
"generationMode": generation_parameters["generation_mode"],
|
||||||
|
"boundingBox": original_bounding_box,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -670,6 +817,8 @@ class InvokeAIWebServer:
|
|||||||
"metadata": {},
|
"metadata": {},
|
||||||
"width": width,
|
"width": width,
|
||||||
"height": height,
|
"height": height,
|
||||||
|
"generationMode": generation_parameters["generation_mode"],
|
||||||
|
"boundingBox": original_bounding_box,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -688,8 +837,11 @@ class InvokeAIWebServer:
|
|||||||
step_index = 1
|
step_index = 1
|
||||||
nonlocal prior_variations
|
nonlocal prior_variations
|
||||||
|
|
||||||
|
"""
|
||||||
|
Tidy up after generation based on generation_mode
|
||||||
|
"""
|
||||||
# paste the inpainting image back onto the original
|
# paste the inpainting image back onto the original
|
||||||
if "init_mask" in generation_parameters:
|
if generation_parameters["generation_mode"] == "inpainting":
|
||||||
image = paste_image_into_bounding_box(
|
image = paste_image_into_bounding_box(
|
||||||
Image.open(init_img_path),
|
Image.open(init_img_path),
|
||||||
image,
|
image,
|
||||||
@ -786,11 +938,14 @@ class InvokeAIWebServer:
|
|||||||
|
|
||||||
# restore the stashed URLS and discard the paths, we are about to send the result to client
|
# restore the stashed URLS and discard the paths, we are about to send the result to client
|
||||||
if "init_img" in all_parameters:
|
if "init_img" in all_parameters:
|
||||||
all_parameters["init_img"] = init_img_url
|
all_parameters["init_img"] = ""
|
||||||
|
|
||||||
if "init_mask" in all_parameters:
|
if "init_mask" in all_parameters:
|
||||||
all_parameters["init_mask"] = "" # TODO: store the mask in metadata
|
all_parameters["init_mask"] = "" # TODO: store the mask in metadata
|
||||||
|
|
||||||
|
if generation_parameters["generation_mode"] == "outpainting":
|
||||||
|
all_parameters["bounding_box"] = original_bounding_box
|
||||||
|
|
||||||
metadata = self.parameters_to_generated_image_metadata(all_parameters)
|
metadata = self.parameters_to_generated_image_metadata(all_parameters)
|
||||||
|
|
||||||
command = parameters_to_command(all_parameters)
|
command = parameters_to_command(all_parameters)
|
||||||
@ -826,6 +981,8 @@ class InvokeAIWebServer:
|
|||||||
"metadata": metadata,
|
"metadata": metadata,
|
||||||
"width": width,
|
"width": width,
|
||||||
"height": height,
|
"height": height,
|
||||||
|
"boundingBox": original_bounding_box,
|
||||||
|
"generationMode": generation_parameters["generation_mode"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
eventlet.sleep(0)
|
eventlet.sleep(0)
|
||||||
@ -933,25 +1090,25 @@ class InvokeAIWebServer:
|
|||||||
|
|
||||||
rfc_dict["variations"] = variations
|
rfc_dict["variations"] = variations
|
||||||
|
|
||||||
if "init_img" in parameters:
|
# if "init_img" in parameters:
|
||||||
rfc_dict["type"] = "img2img"
|
# rfc_dict["type"] = "img2img"
|
||||||
rfc_dict["strength"] = parameters["strength"]
|
# rfc_dict["strength"] = parameters["strength"]
|
||||||
rfc_dict["fit"] = parameters["fit"] # TODO: Noncompliant
|
# rfc_dict["fit"] = parameters["fit"] # TODO: Noncompliant
|
||||||
rfc_dict["orig_hash"] = calculate_init_img_hash(
|
# rfc_dict["orig_hash"] = calculate_init_img_hash(
|
||||||
self.get_image_path_from_url(parameters["init_img"])
|
# self.get_image_path_from_url(parameters["init_img"])
|
||||||
)
|
# )
|
||||||
rfc_dict["init_image_path"] = parameters[
|
# rfc_dict["init_image_path"] = parameters[
|
||||||
"init_img"
|
# "init_img"
|
||||||
] # TODO: Noncompliant
|
|
||||||
# if 'init_mask' in parameters:
|
|
||||||
# rfc_dict['mask_hash'] = calculate_init_img_hash(
|
|
||||||
# self.get_image_path_from_url(parameters['init_mask'])
|
|
||||||
# ) # TODO: Noncompliant
|
|
||||||
# rfc_dict['mask_image_path'] = parameters[
|
|
||||||
# 'init_mask'
|
|
||||||
# ] # TODO: Noncompliant
|
# ] # TODO: Noncompliant
|
||||||
else:
|
# # if 'init_mask' in parameters:
|
||||||
rfc_dict["type"] = "txt2img"
|
# # rfc_dict['mask_hash'] = calculate_init_img_hash(
|
||||||
|
# # self.get_image_path_from_url(parameters['init_mask'])
|
||||||
|
# # ) # TODO: Noncompliant
|
||||||
|
# # rfc_dict['mask_image_path'] = parameters[
|
||||||
|
# # 'init_mask'
|
||||||
|
# # ] # TODO: Noncompliant
|
||||||
|
# else:
|
||||||
|
# rfc_dict["type"] = "txt2img"
|
||||||
|
|
||||||
metadata["image"] = rfc_dict
|
metadata["image"] = rfc_dict
|
||||||
|
|
||||||
@ -1244,23 +1401,67 @@ class CanceledException(Exception):
|
|||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Crops an image to a bounding box.
|
Returns a copy an image, cropped to a bounding box.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def copy_image_from_bounding_box(image, x, y, width, height):
|
def copy_image_from_bounding_box(
|
||||||
|
image: ImageType, x: int, y: int, width: int, height: int
|
||||||
|
) -> ImageType:
|
||||||
with image as im:
|
with image as im:
|
||||||
bounds = (x, y, x + width, y + height)
|
bounds = (x, y, x + width, y + height)
|
||||||
im_cropped = im.crop(bounds)
|
im_cropped = im.crop(bounds)
|
||||||
return im_cropped
|
return im_cropped
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Converts a base64 image dataURL into an image.
|
||||||
|
The dataURL is split on the first commma.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def dataURL_to_image(dataURL: str) -> ImageType:
|
||||||
|
image = Image.open(
|
||||||
|
io.BytesIO(
|
||||||
|
base64.decodebytes(
|
||||||
|
bytes(
|
||||||
|
dataURL.split(",", 1)[1],
|
||||||
|
"utf-8",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Converts a base64 image dataURL into bytes.
|
||||||
|
The dataURL is split on the first commma.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def dataURL_to_bytes(dataURL: str) -> bytes:
|
||||||
|
return base64.decodebytes(
|
||||||
|
bytes(
|
||||||
|
dataURL.split(",", 1)[1],
|
||||||
|
"utf-8",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Pastes an image onto another with a bounding box.
|
Pastes an image onto another with a bounding box.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def paste_image_into_bounding_box(recipient_image, donor_image, x, y, width, height):
|
def paste_image_into_bounding_box(
|
||||||
|
recipient_image: ImageType,
|
||||||
|
donor_image: ImageType,
|
||||||
|
x: int,
|
||||||
|
y: int,
|
||||||
|
width: int,
|
||||||
|
height: int,
|
||||||
|
) -> ImageType:
|
||||||
with recipient_image as im:
|
with recipient_image as im:
|
||||||
bounds = (x, y, x + width, y + height)
|
bounds = (x, y, x + width, y + height)
|
||||||
im.paste(donor_image, bounds)
|
im.paste(donor_image, bounds)
|
||||||
|
117
backend/modules/get_outpainting_generation_mode.py
Normal file
117
backend/modules/get_outpainting_generation_mode.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
from PIL import Image, ImageChops
|
||||||
|
from PIL.Image import Image as ImageType
|
||||||
|
from typing import Union, Literal
|
||||||
|
|
||||||
|
# https://stackoverflow.com/questions/43864101/python-pil-check-if-image-is-transparent
|
||||||
|
def check_for_any_transparency(img: Union[ImageType, str]) -> bool:
|
||||||
|
if type(img) is str:
|
||||||
|
img = Image.open(str)
|
||||||
|
|
||||||
|
if img.info.get("transparency", None) is not None:
|
||||||
|
return True
|
||||||
|
if img.mode == "P":
|
||||||
|
transparent = img.info.get("transparency", -1)
|
||||||
|
for _, index in img.getcolors():
|
||||||
|
if index == transparent:
|
||||||
|
return True
|
||||||
|
elif img.mode == "RGBA":
|
||||||
|
extrema = img.getextrema()
|
||||||
|
if extrema[3][0] < 255:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_outpainting_generation_mode(
|
||||||
|
init_img: Union[ImageType, str], init_mask: Union[ImageType, str]
|
||||||
|
) -> Literal["txt2img", "outpainting", "inpainting", "img2img",]:
|
||||||
|
if type(init_img) is str:
|
||||||
|
init_img = Image.open(init_img)
|
||||||
|
|
||||||
|
if type(init_mask) is str:
|
||||||
|
init_mask = Image.open(init_mask)
|
||||||
|
|
||||||
|
init_img = init_img.convert("RGBA")
|
||||||
|
|
||||||
|
# Get alpha from init_img
|
||||||
|
init_img_alpha = init_img.split()[-1]
|
||||||
|
init_img_alpha_mask = init_img_alpha.convert("L")
|
||||||
|
init_img_has_transparency = check_for_any_transparency(init_img)
|
||||||
|
|
||||||
|
if init_img_has_transparency:
|
||||||
|
init_img_is_fully_transparent = (
|
||||||
|
True if init_img_alpha_mask.getbbox() is None else False
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Mask images are white in areas where no change should be made, black where changes
|
||||||
|
should be made.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Fit the mask to init_img's size and convert it to greyscale
|
||||||
|
init_mask = init_mask.resize(init_img.size).convert("L")
|
||||||
|
|
||||||
|
"""
|
||||||
|
PIL.Image.getbbox() returns the bounding box of non-zero areas of the image, so we first
|
||||||
|
invert the mask image so that masked areas are white and other areas black == zero.
|
||||||
|
getbbox() now tells us if the are any masked areas.
|
||||||
|
"""
|
||||||
|
init_mask_bbox = ImageChops.invert(init_mask).getbbox()
|
||||||
|
init_mask_exists = False if init_mask_bbox is None else True
|
||||||
|
|
||||||
|
if init_img_has_transparency:
|
||||||
|
if init_img_is_fully_transparent:
|
||||||
|
return "txt2img"
|
||||||
|
else:
|
||||||
|
return "outpainting"
|
||||||
|
else:
|
||||||
|
if init_mask_exists:
|
||||||
|
return "inpainting"
|
||||||
|
else:
|
||||||
|
return "img2img"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Testing
|
||||||
|
init_img_opaque = "test_images/init-img_opaque.png"
|
||||||
|
init_img_partial_transparency = "test_images/init-img_partial_transparency.png"
|
||||||
|
init_img_full_transparency = "test_images/init-img_full_transparency.png"
|
||||||
|
init_mask_no_mask = "test_images/init-mask_no_mask.png"
|
||||||
|
init_mask_has_mask = "test_images/init-mask_has_mask.png"
|
||||||
|
|
||||||
|
print(
|
||||||
|
"OPAQUE IMAGE, NO MASK, expect img2img, got ",
|
||||||
|
get_outpainting_generation_mode(init_img_opaque, init_mask_no_mask),
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"IMAGE WITH TRANSPARENCY, NO MASK, expect outpainting, got ",
|
||||||
|
get_outpainting_generation_mode(
|
||||||
|
init_img_partial_transparency, init_mask_no_mask
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"FULLY TRANSPARENT IMAGE NO MASK, expect txt2img, got ",
|
||||||
|
get_outpainting_generation_mode(init_img_full_transparency, init_mask_no_mask),
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"OPAQUE IMAGE, WITH MASK, expect inpainting, got ",
|
||||||
|
get_outpainting_generation_mode(init_img_opaque, init_mask_has_mask),
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"IMAGE WITH TRANSPARENCY, WITH MASK, expect outpainting, got ",
|
||||||
|
get_outpainting_generation_mode(
|
||||||
|
init_img_partial_transparency, init_mask_has_mask
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"FULLY TRANSPARENT IMAGE WITH MASK, expect txt2img, got ",
|
||||||
|
get_outpainting_generation_mode(init_img_full_transparency, init_mask_has_mask),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
BIN
backend/modules/test_images/init-img_full_transparency.png
Normal file
BIN
backend/modules/test_images/init-img_full_transparency.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
BIN
backend/modules/test_images/init-img_opaque.png
Normal file
BIN
backend/modules/test_images/init-img_opaque.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 292 KiB |
BIN
backend/modules/test_images/init-img_partial_transparency.png
Normal file
BIN
backend/modules/test_images/init-img_partial_transparency.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 164 KiB |
BIN
backend/modules/test_images/init-mask_has_mask.png
Normal file
BIN
backend/modules/test_images/init-mask_has_mask.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
BIN
backend/modules/test_images/init-mask_no_mask.png
Normal file
BIN
backend/modules/test_images/init-mask_no_mask.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
@ -5,7 +5,7 @@
|
|||||||
- `python scripts/dream.py --web` serves both frontend and backend at
|
- `python scripts/dream.py --web` serves both frontend and backend at
|
||||||
http://localhost:9090
|
http://localhost:9090
|
||||||
|
|
||||||
## Environment
|
## Evironment
|
||||||
|
|
||||||
Install [node](https://nodejs.org/en/download/) (includes npm) and optionally
|
Install [node](https://nodejs.org/en/download/) (includes npm) and optionally
|
||||||
[yarn](https://yarnpkg.com/getting-started/install).
|
[yarn](https://yarnpkg.com/getting-started/install).
|
||||||
@ -15,7 +15,7 @@ packages.
|
|||||||
|
|
||||||
## Dev
|
## Dev
|
||||||
|
|
||||||
1. From `frontend/`, run `npm run dev` / `yarn dev` to start the dev server.
|
1. From `frontend/`, run `npm dev` / `yarn dev` to start the dev server.
|
||||||
2. Run `python scripts/dream.py --web`.
|
2. Run `python scripts/dream.py --web`.
|
||||||
3. Navigate to the dev server address e.g. `http://localhost:5173/`.
|
3. Navigate to the dev server address e.g. `http://localhost:5173/`.
|
||||||
|
|
||||||
|
23
frontend/eslintconfig.json
Normal file
23
frontend/eslintconfig.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:react-hooks/recommended"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": ["@typescript-eslint", "eslint-plugin-react-hooks"],
|
||||||
|
"root": true,
|
||||||
|
"settings": {
|
||||||
|
"import/resolver": {
|
||||||
|
"node": {
|
||||||
|
"paths": ["src"],
|
||||||
|
"extensions": [".js", ".jsx", ".ts", ".tsx"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"react/jsx-filename-extension": [1, { "extensions": [".tsx", ".ts"] }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/icons": "^2.0.10",
|
"@chakra-ui/icons": "^2.0.10",
|
||||||
"@chakra-ui/react": "^2.3.1",
|
"@chakra-ui/react": "^2.3.1",
|
||||||
|
"@emotion/cache": "^11.10.5",
|
||||||
"@emotion/react": "^11.10.4",
|
"@emotion/react": "^11.10.4",
|
||||||
"@emotion/styled": "^11.10.4",
|
"@emotion/styled": "^11.10.4",
|
||||||
"@radix-ui/react-context-menu": "^2.0.1",
|
"@radix-ui/react-context-menu": "^2.0.1",
|
||||||
@ -29,14 +30,17 @@
|
|||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "^14.2.2",
|
"react-dropzone": "^14.2.2",
|
||||||
"react-hotkeys-hook": "^3.4.7",
|
"react-hotkeys-hook": "4",
|
||||||
"react-icons": "^4.4.0",
|
"react-icons": "^4.4.0",
|
||||||
|
"react-image-pan-zoom-rotate": "^1.6.0",
|
||||||
"react-konva": "^18.2.3",
|
"react-konva": "^18.2.3",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
|
"redux-deep-persist": "^1.0.6",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"socket.io": "^4.5.2",
|
"socket.io": "^4.5.2",
|
||||||
"socket.io-client": "^4.5.2",
|
"socket.io-client": "^4.5.2",
|
||||||
|
"use-image": "^1.1.0",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"yarn": "^1.22.19"
|
"yarn": "^1.22.19"
|
||||||
},
|
},
|
||||||
@ -55,6 +59,7 @@
|
|||||||
"tsc-watch": "^5.0.3",
|
"tsc-watch": "^5.0.3",
|
||||||
"typescript": "^4.6.4",
|
"typescript": "^4.6.4",
|
||||||
"vite": "^3.0.7",
|
"vite": "^3.0.7",
|
||||||
"vite-plugin-eslint": "^1.8.1"
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
|
"vite-tsconfig-paths": "^3.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
@use '../styles/Mixins/' as *;
|
@use '../styles/Mixins/' as *;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--svg-color);
|
||||||
|
}
|
||||||
|
|
||||||
.App {
|
.App {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import ProgressBar from '../features/system/ProgressBar';
|
import ProgressBar from 'features/system/ProgressBar';
|
||||||
import SiteHeader from '../features/system/SiteHeader';
|
import SiteHeader from 'features/system/SiteHeader';
|
||||||
import Console from '../features/system/Console';
|
import Console from 'features/system/Console';
|
||||||
import { useAppDispatch } from './store';
|
import { useAppDispatch } from './store';
|
||||||
import { requestSystemConfig } from './socketio/actions';
|
import { requestSystemConfig } from './socketio/actions';
|
||||||
import { keepGUIAlive } from './utils';
|
import { keepGUIAlive } from './utils';
|
||||||
import InvokeTabs from '../features/tabs/InvokeTabs';
|
import InvokeTabs from 'features/tabs/InvokeTabs';
|
||||||
import ImageUploader from '../common/components/ImageUploader';
|
import ImageUploader from 'common/components/ImageUploader';
|
||||||
import { RootState, useAppSelector } from '../app/store';
|
import { RootState, useAppSelector } from 'app/store';
|
||||||
|
|
||||||
import FloatingGalleryButton from '../features/tabs/FloatingGalleryButton';
|
import FloatingGalleryButton from 'features/tabs/FloatingGalleryButton';
|
||||||
import FloatingOptionsPanelButtons from '../features/tabs/FloatingOptionsPanelButtons';
|
import FloatingOptionsPanelButtons from 'features/tabs/FloatingOptionsPanelButtons';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { GalleryState } from '../features/gallery/gallerySlice';
|
import { GalleryState } from 'features/gallery/gallerySlice';
|
||||||
import { OptionsState } from '../features/options/optionsSlice';
|
import { OptionsState } from 'features/options/optionsSlice';
|
||||||
import { activeTabNameSelector } from '../features/options/optionsSelectors';
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
import { SystemState } from '../features/system/systemSlice';
|
import { SystemState } from 'features/system/systemSlice';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Model } from './invokeai';
|
import { Model } from './invokeai';
|
||||||
|
|
||||||
@ -51,16 +51,20 @@ const appSelector = createSelector(
|
|||||||
''
|
''
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldShowGalleryButton = !(
|
const shouldShowGalleryButton =
|
||||||
shouldShowGallery ||
|
!(shouldShowGallery || (shouldHoldGalleryOpen && !shouldPinGallery)) &&
|
||||||
(shouldHoldGalleryOpen && !shouldPinGallery)
|
['txt2img', 'img2img', 'inpainting', 'outpainting'].includes(
|
||||||
|
activeTabName
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldShowOptionsPanelButton =
|
const shouldShowOptionsPanelButton =
|
||||||
!(
|
!(
|
||||||
shouldShowOptionsPanel ||
|
shouldShowOptionsPanel ||
|
||||||
(shouldHoldOptionsPanelOpen && !shouldPinOptionsPanel)
|
(shouldHoldOptionsPanelOpen && !shouldPinOptionsPanel)
|
||||||
) && ['txt2img', 'img2img', 'inpainting'].includes(activeTabName);
|
) &&
|
||||||
|
['txt2img', 'img2img', 'inpainting', 'outpainting'].includes(
|
||||||
|
activeTabName
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
modelStatusText,
|
modelStatusText,
|
||||||
@ -81,10 +85,6 @@ const App = () => {
|
|||||||
const { shouldShowGalleryButton, shouldShowOptionsPanelButton } =
|
const { shouldShowGalleryButton, shouldShowOptionsPanelButton } =
|
||||||
useAppSelector(appSelector);
|
useAppSelector(appSelector);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(requestSystemConfig());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<ImageUploader>
|
<ImageUploader>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// TODO: use Enums?
|
// TODO: use Enums?
|
||||||
|
|
||||||
import { InProgressImageType } from '../features/system/systemSlice';
|
import { InProgressImageType } from 'features/system/systemSlice';
|
||||||
|
|
||||||
// Valid samplers
|
// Valid samplers
|
||||||
export const SAMPLERS: Array<string> = [
|
export const SAMPLERS: Array<string> = [
|
||||||
|
21
frontend/src/app/invokeai.d.ts
vendored
21
frontend/src/app/invokeai.d.ts
vendored
@ -12,7 +12,9 @@
|
|||||||
* 'gfpgan'.
|
* 'gfpgan'.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Category as GalleryCategory } from '../features/gallery/gallerySlice';
|
import { Category as GalleryCategory } from 'features/gallery/gallerySlice';
|
||||||
|
import { InvokeTabName } from 'features/tabs/InvokeTabs';
|
||||||
|
import { IRect } from 'konva/lib/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO:
|
* TODO:
|
||||||
@ -171,10 +173,13 @@ export declare type SystemStatusResponse = SystemStatus;
|
|||||||
|
|
||||||
export declare type SystemConfigResponse = SystemConfig;
|
export declare type SystemConfigResponse = SystemConfig;
|
||||||
|
|
||||||
export declare type ImageResultResponse = Omit<Image, 'uuid'>;
|
export declare type ImageResultResponse = Omit<Image, 'uuid'> & {
|
||||||
|
boundingBox?: IRect;
|
||||||
|
generationMode: InvokeTabName;
|
||||||
|
};
|
||||||
|
|
||||||
export declare type ImageUploadResponse = Omit<Image, 'uuid' | 'metadata'> & {
|
export declare type ImageUploadResponse = Omit<Image, 'uuid' | 'metadata'> & {
|
||||||
destination: 'img2img' | 'inpainting';
|
destination: 'img2img' | 'inpainting' | 'outpainting' | 'outpainting_merge';
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare type ErrorResponse = {
|
export declare type ErrorResponse = {
|
||||||
@ -198,9 +203,17 @@ export declare type ImageUrlResponse = {
|
|||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare type ImageUploadDestination = 'img2img' | 'inpainting';
|
export declare type ImageUploadDestination =
|
||||||
|
| 'img2img'
|
||||||
|
| 'inpainting'
|
||||||
|
| 'outpainting_merge';
|
||||||
|
|
||||||
export declare type UploadImagePayload = {
|
export declare type UploadImagePayload = {
|
||||||
file: File;
|
file: File;
|
||||||
destination?: ImageUploadDestination;
|
destination?: ImageUploadDestination;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export declare type UploadOutpaintingMergeImagePayload = {
|
||||||
|
dataURL: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
@ -1,39 +1,35 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { RootState } from '../store';
|
import { RootState } from 'app/store';
|
||||||
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
import { OptionsState } from '../../features/options/optionsSlice';
|
import { OptionsState } from 'features/options/optionsSlice';
|
||||||
|
import { SystemState } from 'features/system/systemSlice';
|
||||||
import { SystemState } from '../../features/system/systemSlice';
|
import { baseCanvasImageSelector } from 'features/canvas/canvasSlice';
|
||||||
import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice';
|
import { validateSeedWeights } from 'common/util/seedWeightPairs';
|
||||||
import { validateSeedWeights } from '../../common/util/seedWeightPairs';
|
|
||||||
|
|
||||||
export const readinessSelector = createSelector(
|
export const readinessSelector = createSelector(
|
||||||
[
|
[
|
||||||
(state: RootState) => state.options,
|
(state: RootState) => state.options,
|
||||||
(state: RootState) => state.system,
|
(state: RootState) => state.system,
|
||||||
(state: RootState) => state.inpainting,
|
baseCanvasImageSelector,
|
||||||
activeTabNameSelector,
|
activeTabNameSelector,
|
||||||
],
|
],
|
||||||
(
|
(
|
||||||
options: OptionsState,
|
options: OptionsState,
|
||||||
system: SystemState,
|
system: SystemState,
|
||||||
inpainting: InpaintingState,
|
baseCanvasImage,
|
||||||
activeTabName
|
activeTabName
|
||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
prompt,
|
prompt,
|
||||||
shouldGenerateVariations,
|
shouldGenerateVariations,
|
||||||
seedWeights,
|
seedWeights,
|
||||||
// maskPath,
|
|
||||||
initialImage,
|
initialImage,
|
||||||
seed,
|
seed,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const { isProcessing, isConnected } = system;
|
const { isProcessing, isConnected } = system;
|
||||||
|
|
||||||
const { imageToInpaint } = inpainting;
|
|
||||||
|
|
||||||
let isReady = true;
|
let isReady = true;
|
||||||
const reasonsWhyNotReady: string[] = [];
|
const reasonsWhyNotReady: string[] = [];
|
||||||
|
|
||||||
@ -48,20 +44,11 @@ export const readinessSelector = createSelector(
|
|||||||
reasonsWhyNotReady.push('No initial image selected');
|
reasonsWhyNotReady.push('No initial image selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeTabName === 'inpainting' && !imageToInpaint) {
|
if (activeTabName === 'inpainting' && !baseCanvasImage) {
|
||||||
isReady = false;
|
isReady = false;
|
||||||
reasonsWhyNotReady.push('No inpainting image selected');
|
reasonsWhyNotReady.push('No inpainting image selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
// // We don't use mask paths now.
|
|
||||||
// // Cannot generate with a mask without img2img
|
|
||||||
// if (maskPath && !initialImage) {
|
|
||||||
// isReady = false;
|
|
||||||
// reasonsWhyNotReady.push(
|
|
||||||
// 'On ImageToImage tab, but no mask is provided.'
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO: job queue
|
// TODO: job queue
|
||||||
// Cannot generate if already processing an image
|
// Cannot generate if already processing an image
|
||||||
if (isProcessing) {
|
if (isProcessing) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
import { GalleryCategory } from '../../features/gallery/gallerySlice';
|
import { GalleryCategory } from 'features/gallery/gallerySlice';
|
||||||
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
import { InvokeTabName } from 'features/tabs/InvokeTabs';
|
||||||
import * as InvokeAI from '../invokeai';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4,23 +4,24 @@ import { Socket } from 'socket.io-client';
|
|||||||
import {
|
import {
|
||||||
frontendToBackendParameters,
|
frontendToBackendParameters,
|
||||||
FrontendToBackendParametersConfig,
|
FrontendToBackendParametersConfig,
|
||||||
} from '../../common/util/parameterTranslation';
|
} from 'common/util/parameterTranslation';
|
||||||
import {
|
import {
|
||||||
GalleryCategory,
|
GalleryCategory,
|
||||||
GalleryState,
|
GalleryState,
|
||||||
removeImage,
|
removeImage,
|
||||||
} from '../../features/gallery/gallerySlice';
|
} from 'features/gallery/gallerySlice';
|
||||||
import { OptionsState } from '../../features/options/optionsSlice';
|
import { OptionsState } from 'features/options/optionsSlice';
|
||||||
import {
|
import {
|
||||||
addLogEntry,
|
addLogEntry,
|
||||||
errorOccurred,
|
errorOccurred,
|
||||||
modelChangeRequested,
|
modelChangeRequested,
|
||||||
setIsProcessing,
|
setIsProcessing,
|
||||||
} from '../../features/system/systemSlice';
|
} from 'features/system/systemSlice';
|
||||||
import { inpaintingImageElementRef } from '../../features/tabs/Inpainting/InpaintingCanvas';
|
import { inpaintingImageElementRef } from 'features/canvas/IAICanvas';
|
||||||
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
import { InvokeTabName } from 'features/tabs/InvokeTabs';
|
||||||
import * as InvokeAI from '../invokeai';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
import { RootState } from '../store';
|
import { RootState } from 'app/store';
|
||||||
|
import { baseCanvasImageSelector } from 'features/canvas/canvasSlice';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an object containing all functions which use `socketio.emit()`.
|
* Returns an object containing all functions which use `socketio.emit()`.
|
||||||
@ -42,7 +43,7 @@ const makeSocketIOEmitters = (
|
|||||||
const {
|
const {
|
||||||
options: optionsState,
|
options: optionsState,
|
||||||
system: systemState,
|
system: systemState,
|
||||||
inpainting: inpaintingState,
|
canvas: canvasState,
|
||||||
gallery: galleryState,
|
gallery: galleryState,
|
||||||
} = state;
|
} = state;
|
||||||
|
|
||||||
@ -50,15 +51,15 @@ const makeSocketIOEmitters = (
|
|||||||
{
|
{
|
||||||
generationMode,
|
generationMode,
|
||||||
optionsState,
|
optionsState,
|
||||||
inpaintingState,
|
canvasState,
|
||||||
systemState,
|
systemState,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (generationMode === 'inpainting') {
|
if (['inpainting', 'outpainting'].includes(generationMode)) {
|
||||||
if (
|
const baseCanvasImage = baseCanvasImageSelector(getState());
|
||||||
!inpaintingImageElementRef.current ||
|
const imageUrl = baseCanvasImage?.url;
|
||||||
!inpaintingState.imageToInpaint?.url
|
|
||||||
) {
|
if (!inpaintingImageElementRef.current || !imageUrl) {
|
||||||
dispatch(
|
dispatch(
|
||||||
addLogEntry({
|
addLogEntry({
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||||
@ -70,11 +71,10 @@ const makeSocketIOEmitters = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
frontendToBackendParametersConfig.imageToProcessUrl =
|
frontendToBackendParametersConfig.imageToProcessUrl = imageUrl;
|
||||||
inpaintingState.imageToInpaint.url;
|
|
||||||
|
|
||||||
frontendToBackendParametersConfig.maskImageElement =
|
// frontendToBackendParametersConfig.maskImageElement =
|
||||||
inpaintingImageElementRef.current;
|
// inpaintingImageElementRef.current;
|
||||||
} else if (!['txt2img', 'img2img'].includes(generationMode)) {
|
} else if (!['txt2img', 'img2img'].includes(generationMode)) {
|
||||||
if (!galleryState.currentImage?.url) return;
|
if (!galleryState.currentImage?.url) return;
|
||||||
|
|
||||||
@ -96,7 +96,12 @@ const makeSocketIOEmitters = (
|
|||||||
// TODO: handle maintaining masks for reproducibility in future
|
// TODO: handle maintaining masks for reproducibility in future
|
||||||
if (generationParameters.init_mask) {
|
if (generationParameters.init_mask) {
|
||||||
generationParameters.init_mask = generationParameters.init_mask
|
generationParameters.init_mask = generationParameters.init_mask
|
||||||
.substr(0, 20)
|
.substr(0, 64)
|
||||||
|
.concat('...');
|
||||||
|
}
|
||||||
|
if (generationParameters.init_img) {
|
||||||
|
generationParameters.init_img = generationParameters.init_img
|
||||||
|
.substr(0, 64)
|
||||||
.concat('...');
|
.concat('...');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { AnyAction, MiddlewareAPI, Dispatch } from '@reduxjs/toolkit';
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import dateFormat from 'dateformat';
|
import dateFormat from 'dateformat';
|
||||||
|
|
||||||
import * as InvokeAI from '../invokeai';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addLogEntry,
|
addLogEntry,
|
||||||
@ -15,7 +15,7 @@ import {
|
|||||||
errorOccurred,
|
errorOccurred,
|
||||||
setModelList,
|
setModelList,
|
||||||
setIsCancelable,
|
setIsCancelable,
|
||||||
} from '../../features/system/systemSlice';
|
} from 'features/system/systemSlice';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addGalleryImages,
|
addGalleryImages,
|
||||||
@ -25,19 +25,21 @@ import {
|
|||||||
removeImage,
|
removeImage,
|
||||||
setCurrentImage,
|
setCurrentImage,
|
||||||
setIntermediateImage,
|
setIntermediateImage,
|
||||||
} from '../../features/gallery/gallerySlice';
|
} from 'features/gallery/gallerySlice';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
clearInitialImage,
|
clearInitialImage,
|
||||||
setInitialImage,
|
setInitialImage,
|
||||||
setMaskPath,
|
setMaskPath,
|
||||||
} from '../../features/options/optionsSlice';
|
} from 'features/options/optionsSlice';
|
||||||
import { requestImages, requestNewImages } from './actions';
|
import { requestImages, requestNewImages, requestSystemConfig } from './actions';
|
||||||
import {
|
import {
|
||||||
|
addImageToOutpaintingSesion,
|
||||||
clearImageToInpaint,
|
clearImageToInpaint,
|
||||||
setImageToInpaint,
|
setImageToInpaint,
|
||||||
} from '../../features/tabs/Inpainting/inpaintingSlice';
|
setImageToOutpaint,
|
||||||
import { tabMap } from '../../features/tabs/InvokeTabs';
|
} from 'features/canvas/canvasSlice';
|
||||||
|
import { tabMap } from 'features/tabs/InvokeTabs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an object containing listener callbacks for socketio events.
|
* Returns an object containing listener callbacks for socketio events.
|
||||||
@ -56,6 +58,7 @@ const makeSocketIOListeners = (
|
|||||||
try {
|
try {
|
||||||
dispatch(setIsConnected(true));
|
dispatch(setIsConnected(true));
|
||||||
dispatch(setCurrentStatus('Connected'));
|
dispatch(setCurrentStatus('Connected'));
|
||||||
|
dispatch(requestSystemConfig());
|
||||||
const gallery: GalleryState = getState().gallery;
|
const gallery: GalleryState = getState().gallery;
|
||||||
|
|
||||||
if (gallery.categories.user.latest_mtime) {
|
if (gallery.categories.user.latest_mtime) {
|
||||||
@ -111,6 +114,16 @@ const makeSocketIOListeners = (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (data.generationMode === 'outpainting' && data.boundingBox) {
|
||||||
|
const { boundingBox } = data;
|
||||||
|
dispatch(
|
||||||
|
addImageToOutpaintingSesion({
|
||||||
|
image: newImage,
|
||||||
|
boundingBox,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldLoopback) {
|
if (shouldLoopback) {
|
||||||
const activeTabName = tabMap[activeTab];
|
const activeTabName = tabMap[activeTab];
|
||||||
switch (activeTabName) {
|
switch (activeTabName) {
|
||||||
@ -299,15 +312,15 @@ const makeSocketIOListeners = (
|
|||||||
|
|
||||||
// remove references to image in options
|
// remove references to image in options
|
||||||
const { initialImage, maskPath } = getState().options;
|
const { initialImage, maskPath } = getState().options;
|
||||||
const { imageToInpaint } = getState().inpainting;
|
const { inpainting, outpainting } = getState().canvas;
|
||||||
|
|
||||||
if (initialImage?.url === url || initialImage === url) {
|
if (initialImage?.url === url || initialImage === url) {
|
||||||
dispatch(clearInitialImage());
|
dispatch(clearInitialImage());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageToInpaint?.url === url) {
|
// if (imageToInpaint?.url === url) {
|
||||||
dispatch(clearImageToInpaint());
|
// dispatch(clearImageToInpaint());
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (maskPath === url) {
|
if (maskPath === url) {
|
||||||
dispatch(setMaskPath(''));
|
dispatch(setMaskPath(''));
|
||||||
|
@ -4,7 +4,7 @@ import { io } from 'socket.io-client';
|
|||||||
import makeSocketIOListeners from './listeners';
|
import makeSocketIOListeners from './listeners';
|
||||||
import makeSocketIOEmitters from './emitters';
|
import makeSocketIOEmitters from './emitters';
|
||||||
|
|
||||||
import * as InvokeAI from '../invokeai';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a socketio middleware to handle communication with server.
|
* Creates a socketio middleware to handle communication with server.
|
||||||
@ -104,12 +104,9 @@ export const socketioMiddleware = () => {
|
|||||||
onImageDeleted(data);
|
onImageDeleted(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
socketio.on(
|
socketio.on('imageUploaded', (data: InvokeAI.ImageUploadResponse) => {
|
||||||
'imageUploaded',
|
|
||||||
(data: InvokeAI.ImageUploadResponse) => {
|
|
||||||
onImageUploaded(data);
|
onImageUploaded(data);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
socketio.on('maskImageUploaded', (data: InvokeAI.ImageUrlResponse) => {
|
socketio.on('maskImageUploaded', (data: InvokeAI.ImageUrlResponse) => {
|
||||||
onMaskImageUploaded(data);
|
onMaskImageUploaded(data);
|
||||||
|
@ -5,16 +5,14 @@ import type { TypedUseSelectorHook } from 'react-redux';
|
|||||||
import { persistReducer } from 'redux-persist';
|
import { persistReducer } from 'redux-persist';
|
||||||
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
|
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
|
||||||
|
|
||||||
import optionsReducer, { OptionsState } from '../features/options/optionsSlice';
|
import { getPersistConfig } from 'redux-deep-persist';
|
||||||
import galleryReducer, { GalleryState } from '../features/gallery/gallerySlice';
|
|
||||||
import inpaintingReducer, {
|
import optionsReducer from 'features/options/optionsSlice';
|
||||||
InpaintingState,
|
import galleryReducer from 'features/gallery/gallerySlice';
|
||||||
} from '../features/tabs/Inpainting/inpaintingSlice';
|
import systemReducer from 'features/system/systemSlice';
|
||||||
|
import canvasReducer from 'features/canvas/canvasSlice';
|
||||||
|
|
||||||
import systemReducer, { SystemState } from '../features/system/systemSlice';
|
|
||||||
import { socketioMiddleware } from './socketio/middleware';
|
import { socketioMiddleware } from './socketio/middleware';
|
||||||
import autoMergeLevel2 from 'redux-persist/es/stateReconciler/autoMergeLevel2';
|
|
||||||
import { PersistPartial } from 'redux-persist/es/persistReducer';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* redux-persist provides an easy and reliable way to persist state across reloads.
|
* redux-persist provides an easy and reliable way to persist state across reloads.
|
||||||
@ -28,87 +26,82 @@ import { PersistPartial } from 'redux-persist/es/persistReducer';
|
|||||||
* These can be blacklisted in redux-persist.
|
* These can be blacklisted in redux-persist.
|
||||||
*
|
*
|
||||||
* The necesssary nested persistors with blacklists are configured below.
|
* The necesssary nested persistors with blacklists are configured below.
|
||||||
*
|
|
||||||
* TODO: Do we blacklist initialImagePath? If the image is deleted from disk we get an
|
|
||||||
* ugly 404. But if we blacklist it, then this is a valuable parameter that is lost
|
|
||||||
* on reload. Need to figure out a good way to handle this.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const rootPersistConfig = {
|
const genericCanvasBlacklist = [
|
||||||
key: 'root',
|
'pastObjects',
|
||||||
storage,
|
'futureObjects',
|
||||||
stateReconciler: autoMergeLevel2,
|
'stageScale',
|
||||||
blacklist: ['gallery', 'system', 'inpainting'],
|
'stageDimensions',
|
||||||
};
|
'stageCoordinates',
|
||||||
|
'cursorPosition',
|
||||||
|
];
|
||||||
|
|
||||||
const systemPersistConfig = {
|
const inpaintingCanvasBlacklist = genericCanvasBlacklist.map(
|
||||||
key: 'system',
|
(blacklistItem) => `canvas.inpainting.${blacklistItem}`
|
||||||
storage,
|
);
|
||||||
stateReconciler: autoMergeLevel2,
|
|
||||||
blacklist: [
|
const outpaintingCanvasBlacklist = genericCanvasBlacklist.map(
|
||||||
|
(blacklistItem) => `canvas.outpainting.${blacklistItem}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const systemBlacklist = [
|
||||||
|
'currentIteration',
|
||||||
|
'currentStatus',
|
||||||
|
'currentStep',
|
||||||
'isCancelable',
|
'isCancelable',
|
||||||
'isConnected',
|
'isConnected',
|
||||||
'isProcessing',
|
|
||||||
'currentStep',
|
|
||||||
'socketId',
|
|
||||||
'isESRGANAvailable',
|
'isESRGANAvailable',
|
||||||
'isGFPGANAvailable',
|
'isGFPGANAvailable',
|
||||||
'currentStep',
|
'isProcessing',
|
||||||
'totalSteps',
|
'socketId',
|
||||||
'currentIteration',
|
|
||||||
'totalIterations',
|
'totalIterations',
|
||||||
'currentStatus',
|
'totalSteps',
|
||||||
],
|
].map((blacklistItem) => `system.${blacklistItem}`);
|
||||||
};
|
|
||||||
|
|
||||||
const galleryPersistConfig = {
|
const galleryBlacklist = [
|
||||||
key: 'gallery',
|
'categories',
|
||||||
storage,
|
'currentCategory',
|
||||||
stateReconciler: autoMergeLevel2,
|
'currentImage',
|
||||||
whitelist: [
|
'currentImageUuid',
|
||||||
'galleryWidth',
|
'shouldAutoSwitchToNewImages',
|
||||||
'shouldPinGallery',
|
'shouldHoldGalleryOpen',
|
||||||
'shouldShowGallery',
|
'intermediateImage',
|
||||||
'galleryScrollPosition',
|
].map((blacklistItem) => `gallery.${blacklistItem}`);
|
||||||
'galleryImageMinimumWidth',
|
|
||||||
'galleryImageObjectFit',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const inpaintingPersistConfig = {
|
const rootReducer = combineReducers({
|
||||||
key: 'inpainting',
|
|
||||||
storage,
|
|
||||||
stateReconciler: autoMergeLevel2,
|
|
||||||
blacklist: ['pastLines', 'futuresLines', 'cursorPosition'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const reducers = combineReducers({
|
|
||||||
options: optionsReducer,
|
options: optionsReducer,
|
||||||
gallery: persistReducer<GalleryState>(galleryPersistConfig, galleryReducer),
|
gallery: galleryReducer,
|
||||||
system: persistReducer<SystemState>(systemPersistConfig, systemReducer),
|
system: systemReducer,
|
||||||
inpainting: persistReducer<InpaintingState>(
|
canvas: canvasReducer,
|
||||||
inpaintingPersistConfig,
|
|
||||||
inpaintingReducer
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const persistedReducer = persistReducer<{
|
const rootPersistConfig = getPersistConfig({
|
||||||
options: OptionsState;
|
key: 'root',
|
||||||
gallery: GalleryState & PersistPartial;
|
storage,
|
||||||
system: SystemState & PersistPartial;
|
rootReducer,
|
||||||
inpainting: InpaintingState & PersistPartial;
|
blacklist: [
|
||||||
}>(rootPersistConfig, reducers);
|
...inpaintingCanvasBlacklist,
|
||||||
|
...outpaintingCanvasBlacklist,
|
||||||
|
...systemBlacklist,
|
||||||
|
...galleryBlacklist,
|
||||||
|
],
|
||||||
|
throttle: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
const persistedReducer = persistReducer(rootPersistConfig, rootReducer);
|
||||||
|
|
||||||
// Continue with store setup
|
// Continue with store setup
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: persistedReducer,
|
reducer: persistedReducer,
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) =>
|
||||||
getDefaultMiddleware({
|
getDefaultMiddleware({
|
||||||
// redux-persist sometimes needs to temporarily put a function in redux state, need to disable this check
|
immutableCheck: false,
|
||||||
serializableCheck: false,
|
serializableCheck: false,
|
||||||
}).concat(socketioMiddleware()),
|
}).concat(socketioMiddleware()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type AppGetState = typeof store.getState;
|
||||||
export type RootState = ReturnType<typeof store.getState>;
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
export type AppDispatch = typeof store.dispatch;
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Box, forwardRef, Icon } from '@chakra-ui/react';
|
import { Box, forwardRef, Icon } from '@chakra-ui/react';
|
||||||
import { IconType } from 'react-icons';
|
import { IconType } from 'react-icons';
|
||||||
import { MdHelp } from 'react-icons/md';
|
import { MdHelp } from 'react-icons/md';
|
||||||
import { Feature } from '../../app/features';
|
import { Feature } from 'app/features';
|
||||||
import GuidePopover from './GuidePopover';
|
import GuidePopover from './GuidePopover';
|
||||||
|
|
||||||
type GuideIconProps = {
|
type GuideIconProps = {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
.guide-popover-arrow {
|
.guide-popover-arrow {
|
||||||
background-color: var(--tab-panel-bg) !important;
|
background-color: var(--tab-panel-bg);
|
||||||
box-shadow: none !important;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-popover-content {
|
.guide-popover-content {
|
||||||
background-color: var(--background-color-secondary) !important;
|
background-color: var(--background-color-secondary);
|
||||||
border: none !important;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-popover-guide-content {
|
.guide-popover-guide-content {
|
||||||
|
@ -5,12 +5,12 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
Box,
|
Box,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { SystemState } from '../../features/system/systemSlice';
|
import { SystemState } from 'features/system/systemSlice';
|
||||||
import { useAppSelector } from '../../app/store';
|
import { useAppSelector } from 'app/store';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from 'app/store';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { ReactElement } from 'react';
|
import { ReactElement } from 'react';
|
||||||
import { Feature, FEATURES } from '../../app/features';
|
import { Feature, FEATURES } from 'app/features';
|
||||||
|
|
||||||
type GuideProps = {
|
type GuideProps = {
|
||||||
children: ReactElement;
|
children: ReactElement;
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
.invokeai__button {
|
.invokeai__button {
|
||||||
justify-content: space-between;
|
background-color: var(--btn-base-color);
|
||||||
|
place-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--btn-base-color-hover);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
svg {
|
svg {
|
||||||
width: 0.6rem;
|
width: 0.6rem;
|
||||||
height: 0.6rem;
|
height: 0.6rem;
|
||||||
stroke-width: 3px !important;
|
stroke-width: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-checked] {
|
&[data-checked] {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
@use '../../styles/Mixins/' as *;
|
@use '../../styles/Mixins/' as *;
|
||||||
|
|
||||||
.invokeai__icon-button {
|
.invokeai__icon-button {
|
||||||
background-color: var(--btn-grey);
|
background: var(--btn-base-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--btn-grey-hover);
|
background-color: var(--btn-base-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-selected='true'] {
|
&[data-selected='true'] {
|
||||||
@ -20,16 +20,39 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[data-variant='link'] {
|
&[data-variant='link'] {
|
||||||
background: none !important;
|
background: none;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: none !important;
|
background: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Box Style
|
||||||
|
&[data-as-checkbox='true'] {
|
||||||
|
background-color: var(--btn-base-color);
|
||||||
|
border: 3px solid var(--btn-base-color);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--btn-base-color);
|
||||||
|
border-color: var(--btn-checkbox-border-hover);
|
||||||
|
svg {
|
||||||
|
fill: var(--text-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-selected='true'] {
|
&[data-selected='true'] {
|
||||||
border-color: var(--accent-color);
|
border-color: var(--accent-color);
|
||||||
|
svg {
|
||||||
|
fill: var(--accent-color-hover);
|
||||||
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--accent-color-hover);
|
svg {
|
||||||
|
fill: var(--accent-color-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,28 +61,12 @@
|
|||||||
animation-duration: 1s;
|
animation-duration: 1s;
|
||||||
animation-timing-function: ease-in-out;
|
animation-timing-function: ease-in-out;
|
||||||
animation-iteration-count: infinite;
|
animation-iteration-count: infinite;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
animation: none;
|
animation: none;
|
||||||
background-color: var(--accent-color-hover);
|
background-color: var(--accent-color-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-as-checkbox='true'] {
|
|
||||||
background-color: var(--btn-grey);
|
|
||||||
border: 3px solid var(--btn-grey);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--btn-grey);
|
|
||||||
border-color: var(--btn-checkbox-border-hover);
|
|
||||||
svg {
|
|
||||||
fill: var(--text-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulseColor {
|
@keyframes pulseColor {
|
||||||
|
@ -25,13 +25,23 @@ const IAIIconButton = forwardRef((props: IAIIconButtonProps, forwardedRef) => {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={tooltip} hasArrow {...tooltipProps}>
|
<Tooltip
|
||||||
|
label={tooltip}
|
||||||
|
hasArrow
|
||||||
|
{...tooltipProps}
|
||||||
|
{...(tooltipProps?.placement
|
||||||
|
? { placement: tooltipProps.placement }
|
||||||
|
: { placement: 'top' })}
|
||||||
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
className={`invokeai__icon-button ${styleClass}`}
|
className={
|
||||||
|
styleClass
|
||||||
|
? `invokeai__icon-button ${styleClass}`
|
||||||
|
: `invokeai__icon-button`
|
||||||
|
}
|
||||||
data-as-checkbox={asCheckbox}
|
data-as-checkbox={asCheckbox}
|
||||||
data-selected={isChecked !== undefined ? isChecked : undefined}
|
data-selected={isChecked !== undefined ? isChecked : undefined}
|
||||||
style={props.onClick ? { cursor: 'pointer' } : {}}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
.invokeai__number-input-form-control {
|
.invokeai__number-input-form-control {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: max-content auto;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
column-gap: 1rem;
|
||||||
|
|
||||||
.invokeai__number-input-form-label {
|
.invokeai__number-input-form-label {
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
flex-grow: 2;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding-right: 1rem;
|
|
||||||
|
|
||||||
&[data-focus] + .invokeai__number-input-root {
|
&[data-focus] + .invokeai__number-input-root {
|
||||||
outline: none;
|
outline: none;
|
||||||
@ -33,7 +31,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--background-color-secondary);
|
background-color: var(--background-color-secondary);
|
||||||
border: 2px solid var(--border-color);
|
border: 2px solid var(--border-color);
|
||||||
border-radius: 0.2rem;
|
border-radius: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invokeai__number-input-field {
|
.invokeai__number-input-field {
|
||||||
@ -41,10 +39,8 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 0;
|
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding-left: 0.5rem;
|
padding: 0 0.5rem;
|
||||||
padding-right: 0.5rem;
|
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
@ -21,6 +21,7 @@ const numberStringRegex = /^-?(0\.)?\.?$/;
|
|||||||
interface Props extends Omit<NumberInputProps, 'onChange'> {
|
interface Props extends Omit<NumberInputProps, 'onChange'> {
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
labelFontSize?: string | number;
|
||||||
width?: string | number;
|
width?: string | number;
|
||||||
showStepper?: boolean;
|
showStepper?: boolean;
|
||||||
value: number;
|
value: number;
|
||||||
@ -43,6 +44,7 @@ interface Props extends Omit<NumberInputProps, 'onChange'> {
|
|||||||
const IAINumberInput = (props: Props) => {
|
const IAINumberInput = (props: Props) => {
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
|
labelFontSize = '1rem',
|
||||||
styleClass,
|
styleClass,
|
||||||
isDisabled = false,
|
isDisabled = false,
|
||||||
showStepper = true,
|
showStepper = true,
|
||||||
@ -127,6 +129,7 @@ const IAINumberInput = (props: Props) => {
|
|||||||
<FormLabel
|
<FormLabel
|
||||||
className="invokeai__number-input-form-label"
|
className="invokeai__number-input-form-label"
|
||||||
style={{ display: label ? 'block' : 'none' }}
|
style={{ display: label ? 'block' : 'none' }}
|
||||||
|
fontSize={labelFontSize}
|
||||||
{...formLabelProps}
|
{...formLabelProps}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
.invokeai__popover-content {
|
.invokeai__popover-content {
|
||||||
min-width: unset;
|
min-width: unset;
|
||||||
width: unset !important;
|
width: unset;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 0.5rem !important;
|
border-radius: 0.5rem;
|
||||||
background-color: var(--background-color) !important;
|
background-color: var(--background-color);
|
||||||
border: 2px solid var(--border-color) !important;
|
border: 2px solid var(--border-color);
|
||||||
|
|
||||||
.invokeai__popover-arrow {
|
.invokeai__popover-arrow {
|
||||||
background-color: var(--background-color) !important;
|
background-color: var(--background-color) !important;
|
||||||
|
@ -29,7 +29,7 @@ const IAIPopover = (props: IAIPopoverProps) => {
|
|||||||
<Popover {...rest}>
|
<Popover {...rest}>
|
||||||
<PopoverTrigger>{triggerComponent}</PopoverTrigger>
|
<PopoverTrigger>{triggerComponent}</PopoverTrigger>
|
||||||
<PopoverContent className={`invokeai__popover-content ${styleClass}`}>
|
<PopoverContent className={`invokeai__popover-content ${styleClass}`}>
|
||||||
{hasArrow && <PopoverArrow className={'invokeai__popover-arrow'} />}
|
{hasArrow && <PopoverArrow className="invokeai__popover-arrow" />}
|
||||||
{children}
|
{children}
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
@ -27,5 +27,6 @@
|
|||||||
|
|
||||||
.invokeai__select-option {
|
.invokeai__select-option {
|
||||||
background-color: var(--background-color-secondary);
|
background-color: var(--background-color-secondary);
|
||||||
|
color: var(--text-color-secondary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { FormControl, FormLabel, Select, SelectProps } from '@chakra-ui/react';
|
|||||||
import { MouseEvent } from 'react';
|
import { MouseEvent } from 'react';
|
||||||
|
|
||||||
type IAISelectProps = SelectProps & {
|
type IAISelectProps = SelectProps & {
|
||||||
label: string;
|
label?: string;
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
validValues:
|
validValues:
|
||||||
| Array<number | string>
|
| Array<number | string>
|
||||||
@ -32,6 +32,7 @@ const IAISelect = (props: IAISelectProps) => {
|
|||||||
e.nativeEvent.cancelBubble = true;
|
e.nativeEvent.cancelBubble = true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{label && (
|
||||||
<FormLabel
|
<FormLabel
|
||||||
className="invokeai__select-label"
|
className="invokeai__select-label"
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
@ -41,6 +42,8 @@ const IAISelect = (props: IAISelectProps) => {
|
|||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
)}
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
className="invokeai__select-picker"
|
className="invokeai__select-picker"
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
|
@ -1,40 +1,62 @@
|
|||||||
@use '../../styles/Mixins/' as *;
|
.invokeai__slider-component {
|
||||||
|
|
||||||
.invokeai__slider-form-control {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
column-gap: 1rem;
|
gap: 1rem;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: max-content;
|
|
||||||
padding-right: 0.25rem;
|
|
||||||
|
|
||||||
.invokeai__slider-inner-container {
|
.invokeai__slider-component-label {
|
||||||
display: flex;
|
min-width: max-content;
|
||||||
column-gap: 0.5rem;
|
|
||||||
|
|
||||||
.invokeai__slider-form-label {
|
|
||||||
color: var(--text-color-secondary);
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-right: 0.5rem;
|
font-weight: bold;
|
||||||
margin-bottom: 0.1rem;
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.invokeai__slider-root {
|
.invokeai__slider_track {
|
||||||
.invokeai__slider-filled-track {
|
background-color: var(--tab-color);
|
||||||
background-color: var(--accent-color-hover);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.invokeai__slider-track {
|
.invokeai__slider_track-filled {
|
||||||
background-color: var(--text-color-secondary);
|
background-color: var(--slider-color);
|
||||||
height: 5px;
|
|
||||||
border-radius: 9999px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.invokeai__slider-thumb {
|
.invokeai__slider-thumb {
|
||||||
|
width: 4px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.invokeai__slider-thumb-tooltip {
|
.invokeai__slider-mark {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--slider-color);
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invokeai__slider-number-input {
|
||||||
|
border: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: bold;
|
||||||
|
height: 2rem;
|
||||||
|
background-color: var(--background-color-secondary);
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border: 2px solid var(--input-border-color);
|
||||||
|
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.invokeai__slider-number-stepper {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-markers='true'] {
|
||||||
|
.invokeai__slider_container {
|
||||||
|
margin-top: -1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,87 +1,241 @@
|
|||||||
import {
|
import {
|
||||||
Slider,
|
|
||||||
SliderTrack,
|
|
||||||
SliderFilledTrack,
|
|
||||||
SliderThumb,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
|
||||||
Tooltip,
|
|
||||||
SliderProps,
|
|
||||||
FormControlProps,
|
FormControlProps,
|
||||||
|
FormLabel,
|
||||||
FormLabelProps,
|
FormLabelProps,
|
||||||
SliderTrackProps,
|
HStack,
|
||||||
|
NumberDecrementStepper,
|
||||||
|
NumberIncrementStepper,
|
||||||
|
NumberInput,
|
||||||
|
NumberInputField,
|
||||||
|
NumberInputFieldProps,
|
||||||
|
NumberInputProps,
|
||||||
|
NumberInputStepper,
|
||||||
|
NumberInputStepperProps,
|
||||||
|
Slider,
|
||||||
|
SliderFilledTrack,
|
||||||
|
SliderMark,
|
||||||
|
SliderMarkProps,
|
||||||
|
SliderThumb,
|
||||||
SliderThumbProps,
|
SliderThumbProps,
|
||||||
|
SliderTrack,
|
||||||
|
SliderTrackProps,
|
||||||
|
Tooltip,
|
||||||
TooltipProps,
|
TooltipProps,
|
||||||
SliderInnerTrackProps,
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
import React, { FocusEvent, useEffect, useState } from 'react';
|
||||||
|
import { BiReset } from 'react-icons/bi';
|
||||||
|
import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
type IAISliderProps = SliderProps & {
|
export type IAIFullSliderProps = {
|
||||||
label?: string;
|
label: string;
|
||||||
|
value: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
onChange: (v: number) => void;
|
||||||
|
withSliderMarks?: boolean;
|
||||||
|
sliderMarkLeftOffset?: number;
|
||||||
|
sliderMarkRightOffset?: number;
|
||||||
|
withInput?: boolean;
|
||||||
|
isInteger?: boolean;
|
||||||
|
inputWidth?: string | number;
|
||||||
|
inputReadOnly?: boolean;
|
||||||
|
withReset?: boolean;
|
||||||
|
handleReset?: () => void;
|
||||||
|
isResetDisabled?: boolean;
|
||||||
|
isSliderDisabled?: boolean;
|
||||||
|
isInputDisabled?: boolean;
|
||||||
|
tooltipSuffix?: string;
|
||||||
|
hideTooltip?: boolean;
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
formControlProps?: FormControlProps;
|
sliderFormControlProps?: FormControlProps;
|
||||||
formLabelProps?: FormLabelProps;
|
sliderFormLabelProps?: FormLabelProps;
|
||||||
|
sliderMarkProps?: Omit<SliderMarkProps, 'value'>;
|
||||||
sliderTrackProps?: SliderTrackProps;
|
sliderTrackProps?: SliderTrackProps;
|
||||||
sliderInnerTrackProps?: SliderInnerTrackProps;
|
|
||||||
sliderThumbProps?: SliderThumbProps;
|
sliderThumbProps?: SliderThumbProps;
|
||||||
sliderThumbTooltipProps?: Omit<TooltipProps, 'children'>;
|
sliderNumberInputProps?: NumberInputProps;
|
||||||
|
sliderNumberInputFieldProps?: NumberInputFieldProps;
|
||||||
|
sliderNumberInputStepperProps?: NumberInputStepperProps;
|
||||||
|
sliderTooltipProps?: Omit<TooltipProps, 'children'>;
|
||||||
|
sliderIAIIconButtonProps?: IAIIconButtonProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IAISlider = (props: IAISliderProps) => {
|
export default function IAISlider(props: IAIFullSliderProps) {
|
||||||
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
|
value,
|
||||||
|
min = 1,
|
||||||
|
max = 100,
|
||||||
|
step = 1,
|
||||||
|
onChange,
|
||||||
|
tooltipSuffix = '',
|
||||||
|
withSliderMarks = false,
|
||||||
|
sliderMarkLeftOffset = 0,
|
||||||
|
sliderMarkRightOffset = -7,
|
||||||
|
withInput = false,
|
||||||
|
isInteger = false,
|
||||||
|
inputWidth = '5rem',
|
||||||
|
inputReadOnly = true,
|
||||||
|
withReset = false,
|
||||||
|
hideTooltip = false,
|
||||||
|
handleReset,
|
||||||
|
isResetDisabled,
|
||||||
|
isSliderDisabled,
|
||||||
|
isInputDisabled,
|
||||||
styleClass,
|
styleClass,
|
||||||
formControlProps,
|
sliderFormControlProps,
|
||||||
formLabelProps,
|
sliderFormLabelProps,
|
||||||
|
sliderMarkProps,
|
||||||
sliderTrackProps,
|
sliderTrackProps,
|
||||||
sliderInnerTrackProps,
|
|
||||||
sliderThumbProps,
|
sliderThumbProps,
|
||||||
sliderThumbTooltipProps,
|
sliderNumberInputProps,
|
||||||
|
sliderNumberInputFieldProps,
|
||||||
|
sliderNumberInputStepperProps,
|
||||||
|
sliderTooltipProps,
|
||||||
|
sliderIAIIconButtonProps,
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const [localInputValue, setLocalInputValue] = useState<string>(String(value));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (String(value) !== localInputValue && localInputValue !== '') {
|
||||||
|
setLocalInputValue(String(value));
|
||||||
|
}
|
||||||
|
}, [value, localInputValue, setLocalInputValue]);
|
||||||
|
|
||||||
|
const handleInputBlur = (e: FocusEvent<HTMLInputElement>) => {
|
||||||
|
const clamped = _.clamp(
|
||||||
|
isInteger ? Math.floor(Number(e.target.value)) : Number(e.target.value),
|
||||||
|
min,
|
||||||
|
max
|
||||||
|
);
|
||||||
|
setLocalInputValue(String(clamped));
|
||||||
|
onChange(clamped);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (v: any) => {
|
||||||
|
setLocalInputValue(v);
|
||||||
|
onChange(Number(v));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetDisable = () => {
|
||||||
|
if (!handleReset) return;
|
||||||
|
handleReset();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl
|
<FormControl
|
||||||
className={`invokeai__slider-form-control ${styleClass}`}
|
className={
|
||||||
{...formControlProps}
|
styleClass
|
||||||
|
? `invokeai__slider-component ${styleClass}`
|
||||||
|
: `invokeai__slider-component`
|
||||||
|
}
|
||||||
|
data-markers={withSliderMarks}
|
||||||
|
{...sliderFormControlProps}
|
||||||
>
|
>
|
||||||
<div className="invokeai__slider-inner-container">
|
|
||||||
<FormLabel
|
<FormLabel
|
||||||
className={`invokeai__slider-form-label`}
|
className="invokeai__slider-component-label"
|
||||||
whiteSpace="nowrap"
|
{...sliderFormLabelProps}
|
||||||
{...formLabelProps}
|
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
|
<HStack w={'100%'} gap={2}>
|
||||||
<Slider
|
<Slider
|
||||||
className={`invokeai__slider-root`}
|
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
|
value={value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onMouseEnter={() => setShowTooltip(true)}
|
||||||
|
onMouseLeave={() => setShowTooltip(false)}
|
||||||
|
focusThumbOnChange={false}
|
||||||
|
isDisabled={isSliderDisabled}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<SliderTrack
|
{withSliderMarks && (
|
||||||
className={`invokeai__slider-track`}
|
<>
|
||||||
{...sliderTrackProps}
|
<SliderMark
|
||||||
|
value={min}
|
||||||
|
className="invokeai__slider-mark invokeai__slider-mark-start"
|
||||||
|
ml={sliderMarkLeftOffset}
|
||||||
|
{...sliderMarkProps}
|
||||||
>
|
>
|
||||||
<SliderFilledTrack
|
{min}
|
||||||
className={`invokeai__slider-filled-track`}
|
</SliderMark>
|
||||||
{...sliderInnerTrackProps}
|
<SliderMark
|
||||||
/>
|
value={max}
|
||||||
|
className="invokeai__slider-mark invokeai__slider-mark-end"
|
||||||
|
ml={sliderMarkRightOffset}
|
||||||
|
{...sliderMarkProps}
|
||||||
|
>
|
||||||
|
{max}
|
||||||
|
</SliderMark>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SliderTrack className="invokeai__slider_track" {...sliderTrackProps}>
|
||||||
|
<SliderFilledTrack className="invokeai__slider_track-filled" />
|
||||||
</SliderTrack>
|
</SliderTrack>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className={`invokeai__slider-thumb-tooltip`}
|
|
||||||
placement="top"
|
|
||||||
hasArrow
|
hasArrow
|
||||||
{...sliderThumbTooltipProps}
|
className="invokeai__slider-component-tooltip"
|
||||||
|
placement="top"
|
||||||
|
isOpen={showTooltip}
|
||||||
|
label={`${value}${tooltipSuffix}`}
|
||||||
|
hidden={hideTooltip}
|
||||||
|
{...sliderTooltipProps}
|
||||||
>
|
>
|
||||||
<SliderThumb
|
<SliderThumb
|
||||||
className={`invokeai__slider-thumb`}
|
className="invokeai__slider-thumb"
|
||||||
{...sliderThumbProps}
|
{...sliderThumbProps}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Slider>
|
</Slider>
|
||||||
</div>
|
|
||||||
|
{withInput && (
|
||||||
|
<NumberInput
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
value={localInputValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
|
className="invokeai__slider-number-field"
|
||||||
|
isDisabled={isInputDisabled}
|
||||||
|
{...sliderNumberInputProps}
|
||||||
|
>
|
||||||
|
<NumberInputField
|
||||||
|
className="invokeai__slider-number-input"
|
||||||
|
width={inputWidth}
|
||||||
|
readOnly={inputReadOnly}
|
||||||
|
{...sliderNumberInputFieldProps}
|
||||||
|
/>
|
||||||
|
<NumberInputStepper {...sliderNumberInputStepperProps}>
|
||||||
|
<NumberIncrementStepper className="invokeai__slider-number-stepper" />
|
||||||
|
<NumberDecrementStepper className="invokeai__slider-number-stepper" />
|
||||||
|
</NumberInputStepper>
|
||||||
|
</NumberInput>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{withReset && (
|
||||||
|
<IAIIconButton
|
||||||
|
size={'sm'}
|
||||||
|
aria-label={'Reset'}
|
||||||
|
tooltip={'Reset'}
|
||||||
|
icon={<BiReset />}
|
||||||
|
onClick={handleResetDisable}
|
||||||
|
isDisabled={isResetDisabled}
|
||||||
|
{...sliderIAIIconButtonProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default IAISlider;
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Heading } from '@chakra-ui/react';
|
import { Heading } from '@chakra-ui/react';
|
||||||
|
import { KeyboardEvent } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
type ImageUploadOverlayProps = {
|
type ImageUploadOverlayProps = {
|
||||||
|
@ -33,7 +33,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.image-uploader-button-outer {
|
.image-uploader-button-outer {
|
||||||
min-width: 20rem;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -42,10 +41,10 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
color: var(--tab-list-text-inactive);
|
color: var(--tab-list-text-inactive);
|
||||||
background-color: var(--btn-grey);
|
background-color: var(--background-color);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--btn-grey-hover);
|
background-color: var(--background-color-light);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,10 +65,10 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 4rem !important;
|
width: 4rem;
|
||||||
height: 4rem !important;
|
height: 4rem;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.2rem !important;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
import { useCallback, ReactNode, useState, useEffect } from 'react';
|
import {
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
useCallback,
|
||||||
|
ReactNode,
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
KeyboardEvent,
|
||||||
|
} from 'react';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
import { uploadImage } from '../../app/socketio/actions';
|
import { uploadImage } from 'app/socketio/actions';
|
||||||
import { ImageUploadDestination, UploadImagePayload } from '../../app/invokeai';
|
import { ImageUploadDestination, UploadImagePayload } from 'app/invokeai';
|
||||||
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
||||||
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
import { tabDict } from '../../features/tabs/InvokeTabs';
|
import { tabDict } from 'features/tabs/InvokeTabs';
|
||||||
import ImageUploadOverlay from './ImageUploadOverlay';
|
import ImageUploadOverlay from './ImageUploadOverlay';
|
||||||
|
|
||||||
type ImageUploaderProps = {
|
type ImageUploaderProps = {
|
||||||
@ -41,7 +47,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
(file: File) => {
|
(file: File) => {
|
||||||
setIsHandlingUpload(true);
|
setIsHandlingUpload(true);
|
||||||
const payload: UploadImagePayload = { file };
|
const payload: UploadImagePayload = { file };
|
||||||
if (['img2img', 'inpainting'].includes(activeTabName)) {
|
if (['img2img', 'inpainting', 'outpainting'].includes(activeTabName)) {
|
||||||
payload.destination = activeTabName as ImageUploadDestination;
|
payload.destination = activeTabName as ImageUploadDestination;
|
||||||
}
|
}
|
||||||
dispatch(uploadImage(payload));
|
dispatch(uploadImage(payload));
|
||||||
@ -137,7 +143,13 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ImageUploaderTriggerContext.Provider value={open}>
|
<ImageUploaderTriggerContext.Provider value={open}>
|
||||||
<div {...getRootProps({ style: {} })}>
|
<div
|
||||||
|
{...getRootProps({ style: {} })}
|
||||||
|
onKeyDown={(e: KeyboardEvent) => {
|
||||||
|
// Bail out if user hits spacebar - do not open the uploader
|
||||||
|
if (e.key === ' ') return;
|
||||||
|
}}
|
||||||
|
>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
{children}
|
{children}
|
||||||
{isDragActive && isHandlingUpload && (
|
{isDragActive && isHandlingUpload && (
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Heading } from '@chakra-ui/react';
|
import { Heading } from '@chakra-ui/react';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { FaUpload } from 'react-icons/fa';
|
import { FaUpload } from 'react-icons/fa';
|
||||||
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
||||||
|
|
||||||
type ImageUploaderButtonProps = {
|
type ImageUploaderButtonProps = {
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { FaUpload } from 'react-icons/fa';
|
import { FaUpload } from 'react-icons/fa';
|
||||||
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
||||||
import IAIIconButton from './IAIIconButton';
|
import IAIIconButton from './IAIIconButton';
|
||||||
|
|
||||||
const ImageUploaderIconButton = () => {
|
const ImageUploaderIconButton = () => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Img2ImgPlaceHolder from '../../../assets/images/image2img.png';
|
import Img2ImgPlaceHolder from 'assets/images/image2img.png';
|
||||||
|
|
||||||
export const ImageToImageWIP = () => {
|
export const ImageToImageWIP = () => {
|
||||||
return (
|
return (
|
||||||
|
21
frontend/src/common/util/openBase64ImageInTab.ts
Normal file
21
frontend/src/common/util/openBase64ImageInTab.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
type Base64AndCaption = {
|
||||||
|
base64: string;
|
||||||
|
caption: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openBase64ImageInTab = (images: Base64AndCaption[]) => {
|
||||||
|
const w = window.open('');
|
||||||
|
if (!w) return;
|
||||||
|
|
||||||
|
images.forEach((i) => {
|
||||||
|
const image = new Image();
|
||||||
|
image.src = i.base64;
|
||||||
|
|
||||||
|
w.document.write(i.caption);
|
||||||
|
w.document.write('</br>');
|
||||||
|
w.document.write(image.outerHTML);
|
||||||
|
w.document.write('</br></br>');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default openBase64ImageInTab;
|
@ -1,20 +1,21 @@
|
|||||||
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../app/constants';
|
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants';
|
||||||
import { OptionsState } from '../../features/options/optionsSlice';
|
import { OptionsState } from 'features/options/optionsSlice';
|
||||||
import { SystemState } from '../../features/system/systemSlice';
|
import { SystemState } from 'features/system/systemSlice';
|
||||||
|
|
||||||
import { stringToSeedWeightsArray } from './seedWeightPairs';
|
import { stringToSeedWeightsArray } from './seedWeightPairs';
|
||||||
import randomInt from './randomInt';
|
import randomInt from './randomInt';
|
||||||
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
import { InvokeTabName } from 'features/tabs/InvokeTabs';
|
||||||
import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice';
|
import { CanvasState, isCanvasMaskLine } from 'features/canvas/canvasSlice';
|
||||||
import generateMask from '../../features/tabs/Inpainting/util/generateMask';
|
import generateMask from 'features/canvas/util/generateMask';
|
||||||
|
import { canvasImageLayerRef } from 'features/canvas/IAICanvas';
|
||||||
|
import openBase64ImageInTab from './openBase64ImageInTab';
|
||||||
|
|
||||||
export type FrontendToBackendParametersConfig = {
|
export type FrontendToBackendParametersConfig = {
|
||||||
generationMode: InvokeTabName;
|
generationMode: InvokeTabName;
|
||||||
optionsState: OptionsState;
|
optionsState: OptionsState;
|
||||||
inpaintingState: InpaintingState;
|
canvasState: CanvasState;
|
||||||
systemState: SystemState;
|
systemState: SystemState;
|
||||||
imageToProcessUrl?: string;
|
imageToProcessUrl?: string;
|
||||||
maskImageElement?: HTMLImageElement;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,10 +28,9 @@ export const frontendToBackendParameters = (
|
|||||||
const {
|
const {
|
||||||
generationMode,
|
generationMode,
|
||||||
optionsState,
|
optionsState,
|
||||||
inpaintingState,
|
canvasState,
|
||||||
systemState,
|
systemState,
|
||||||
imageToProcessUrl,
|
imageToProcessUrl,
|
||||||
maskImageElement,
|
|
||||||
} = config;
|
} = config;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -62,8 +62,11 @@ export const frontendToBackendParameters = (
|
|||||||
shouldRandomizeSeed,
|
shouldRandomizeSeed,
|
||||||
} = optionsState;
|
} = optionsState;
|
||||||
|
|
||||||
const { shouldDisplayInProgressType, saveIntermediatesInterval } =
|
const {
|
||||||
systemState;
|
shouldDisplayInProgressType,
|
||||||
|
saveIntermediatesInterval,
|
||||||
|
enableImageDebugging,
|
||||||
|
} = systemState;
|
||||||
|
|
||||||
const generationParameters: { [k: string]: any } = {
|
const generationParameters: { [k: string]: any } = {
|
||||||
prompt,
|
prompt,
|
||||||
@ -80,6 +83,8 @@ export const frontendToBackendParameters = (
|
|||||||
progress_images: shouldDisplayInProgressType === 'full-res',
|
progress_images: shouldDisplayInProgressType === 'full-res',
|
||||||
progress_latents: shouldDisplayInProgressType === 'latents',
|
progress_latents: shouldDisplayInProgressType === 'latents',
|
||||||
save_intermediates: saveIntermediatesInterval,
|
save_intermediates: saveIntermediatesInterval,
|
||||||
|
generation_mode: generationMode,
|
||||||
|
init_mask: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
generationParameters.seed = shouldRandomizeSeed
|
generationParameters.seed = shouldRandomizeSeed
|
||||||
@ -101,35 +106,36 @@ export const frontendToBackendParameters = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// inpainting exclusive parameters
|
// inpainting exclusive parameters
|
||||||
if (generationMode === 'inpainting' && maskImageElement) {
|
if (
|
||||||
|
['inpainting', 'outpainting'].includes(generationMode) &&
|
||||||
|
canvasImageLayerRef.current
|
||||||
|
) {
|
||||||
const {
|
const {
|
||||||
lines,
|
objects,
|
||||||
boundingBoxCoordinate,
|
boundingBoxCoordinates,
|
||||||
boundingBoxDimensions,
|
boundingBoxDimensions,
|
||||||
inpaintReplace,
|
inpaintReplace,
|
||||||
shouldUseInpaintReplace,
|
shouldUseInpaintReplace,
|
||||||
} = inpaintingState;
|
stageScale,
|
||||||
|
isMaskEnabled,
|
||||||
|
} = canvasState[canvasState.currentCanvas];
|
||||||
|
|
||||||
const boundingBox = {
|
const boundingBox = {
|
||||||
...boundingBoxCoordinate,
|
...boundingBoxCoordinates,
|
||||||
...boundingBoxDimensions,
|
...boundingBoxDimensions,
|
||||||
};
|
};
|
||||||
|
|
||||||
generationParameters.init_img = imageToProcessUrl;
|
const maskDataURL = generateMask(
|
||||||
generationParameters.strength = img2imgStrength;
|
isMaskEnabled ? objects.filter(isCanvasMaskLine) : [],
|
||||||
generationParameters.fit = false;
|
|
||||||
|
|
||||||
const { maskDataURL, isMaskEmpty } = generateMask(
|
|
||||||
maskImageElement,
|
|
||||||
lines,
|
|
||||||
boundingBox
|
boundingBox
|
||||||
);
|
);
|
||||||
|
|
||||||
generationParameters.is_mask_empty = isMaskEmpty;
|
generationParameters.init_mask = maskDataURL;
|
||||||
|
|
||||||
generationParameters.init_mask = maskDataURL.split(
|
generationParameters.fit = false;
|
||||||
'data:image/png;base64,'
|
|
||||||
)[1];
|
generationParameters.init_img = imageToProcessUrl;
|
||||||
|
generationParameters.strength = img2imgStrength;
|
||||||
|
|
||||||
if (shouldUseInpaintReplace) {
|
if (shouldUseInpaintReplace) {
|
||||||
generationParameters.inpaint_replace = inpaintReplace;
|
generationParameters.inpaint_replace = inpaintReplace;
|
||||||
@ -137,8 +143,44 @@ export const frontendToBackendParameters = (
|
|||||||
|
|
||||||
generationParameters.bounding_box = boundingBox;
|
generationParameters.bounding_box = boundingBox;
|
||||||
|
|
||||||
|
if (generationMode === 'outpainting') {
|
||||||
|
const tempScale = canvasImageLayerRef.current.scale();
|
||||||
|
|
||||||
|
canvasImageLayerRef.current.scale({
|
||||||
|
x: 1 / stageScale,
|
||||||
|
y: 1 / stageScale,
|
||||||
|
});
|
||||||
|
|
||||||
|
const absPos = canvasImageLayerRef.current.getAbsolutePosition();
|
||||||
|
|
||||||
|
const imageDataURL = canvasImageLayerRef.current.toDataURL({
|
||||||
|
x: boundingBox.x + absPos.x,
|
||||||
|
y: boundingBox.y + absPos.y,
|
||||||
|
width: boundingBox.width,
|
||||||
|
height: boundingBox.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (enableImageDebugging) {
|
||||||
|
openBase64ImageInTab([
|
||||||
|
{ base64: maskDataURL, caption: 'mask sent as init_mask' },
|
||||||
|
{ base64: imageDataURL, caption: 'image sent as init_img' },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasImageLayerRef.current.scale(tempScale);
|
||||||
|
|
||||||
|
generationParameters.init_img = imageDataURL;
|
||||||
|
|
||||||
// TODO: The server metadata generation needs to be changed to fix this.
|
// TODO: The server metadata generation needs to be changed to fix this.
|
||||||
generationParameters.progress_images = false;
|
generationParameters.progress_images = false;
|
||||||
|
|
||||||
|
generationParameters.seam_size = 96;
|
||||||
|
generationParameters.seam_blur = 16;
|
||||||
|
generationParameters.seam_strength = 0.7;
|
||||||
|
generationParameters.seam_steps = 10;
|
||||||
|
generationParameters.tile_size = 32;
|
||||||
|
generationParameters.force_outpaint = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldGenerateVariations) {
|
if (shouldGenerateVariations) {
|
||||||
@ -171,6 +213,10 @@ export const frontendToBackendParameters = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (enableImageDebugging) {
|
||||||
|
generationParameters.enable_image_debugging = enableImageDebugging;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generationParameters,
|
generationParameters,
|
||||||
esrganParameters,
|
esrganParameters,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import * as InvokeAI from '../../app/invokeai';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
|
||||||
const promptToString = (prompt: InvokeAI.Prompt): string => {
|
const promptToString = (prompt: InvokeAI.Prompt): string => {
|
||||||
if (prompt.length === 1) {
|
if (prompt.length === 1) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import * as InvokeAI from '../../app/invokeai';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
|
||||||
export const stringToSeedWeights = (
|
export const stringToSeedWeights = (
|
||||||
string: string
|
string: string
|
||||||
|
275
frontend/src/features/canvas/IAICanvas.tsx
Normal file
275
frontend/src/features/canvas/IAICanvas.tsx
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
// lib
|
||||||
|
import { MutableRefObject, useEffect, useRef, useState } from 'react';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import { Layer, Stage } from 'react-konva';
|
||||||
|
import { Image as KonvaImage } from 'react-konva';
|
||||||
|
import { Stage as StageType } from 'konva/lib/Stage';
|
||||||
|
|
||||||
|
// app
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import {
|
||||||
|
baseCanvasImageSelector,
|
||||||
|
clearImageToInpaint,
|
||||||
|
currentCanvasSelector,
|
||||||
|
outpaintingCanvasSelector,
|
||||||
|
} from 'features/canvas/canvasSlice';
|
||||||
|
|
||||||
|
// component
|
||||||
|
import IAICanvasMaskLines from './IAICanvasMaskLines';
|
||||||
|
import IAICanvasBrushPreview from './IAICanvasBrushPreview';
|
||||||
|
import { Vector2d } from 'konva/lib/types';
|
||||||
|
import IAICanvasBoundingBoxPreview from './IAICanvasBoundingBoxPreview';
|
||||||
|
import { useToast } from '@chakra-ui/react';
|
||||||
|
import useCanvasHotkeys from './hooks/useCanvasHotkeys';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import IAICanvasMaskCompositer from './IAICanvasMaskCompositer';
|
||||||
|
import useCanvasWheel from './hooks/useCanvasZoom';
|
||||||
|
import useCanvasMouseDown from './hooks/useCanvasMouseDown';
|
||||||
|
import useCanvasMouseUp from './hooks/useCanvasMouseUp';
|
||||||
|
import useCanvasMouseMove from './hooks/useCanvasMouseMove';
|
||||||
|
import useCanvasMouseEnter from './hooks/useCanvasMouseEnter';
|
||||||
|
import useCanvasMouseOut from './hooks/useCanvasMouseOut';
|
||||||
|
import useCanvasDragMove from './hooks/useCanvasDragMove';
|
||||||
|
import IAICanvasOutpaintingObjects from './IAICanvasOutpaintingObjects';
|
||||||
|
import IAICanvasGrid from './IAICanvasGrid';
|
||||||
|
import IAICanvasIntermediateImage from './IAICanvasIntermediateImage';
|
||||||
|
import IAICanvasStatusText from './IAICanvasStatusText';
|
||||||
|
|
||||||
|
const canvasSelector = createSelector(
|
||||||
|
[
|
||||||
|
currentCanvasSelector,
|
||||||
|
outpaintingCanvasSelector,
|
||||||
|
baseCanvasImageSelector,
|
||||||
|
activeTabNameSelector,
|
||||||
|
],
|
||||||
|
(currentCanvas, outpaintingCanvas, baseCanvasImage, activeTabName) => {
|
||||||
|
const {
|
||||||
|
shouldInvertMask,
|
||||||
|
isMaskEnabled,
|
||||||
|
shouldShowCheckboardTransparency,
|
||||||
|
stageScale,
|
||||||
|
shouldShowBoundingBox,
|
||||||
|
shouldLockBoundingBox,
|
||||||
|
isTransformingBoundingBox,
|
||||||
|
isMouseOverBoundingBox,
|
||||||
|
isMovingBoundingBox,
|
||||||
|
stageDimensions,
|
||||||
|
stageCoordinates,
|
||||||
|
isMoveStageKeyHeld,
|
||||||
|
tool,
|
||||||
|
isMovingStage,
|
||||||
|
} = currentCanvas;
|
||||||
|
|
||||||
|
const { shouldShowGrid } = outpaintingCanvas;
|
||||||
|
|
||||||
|
let stageCursor: string | undefined = '';
|
||||||
|
|
||||||
|
if (tool === 'move') {
|
||||||
|
if (isTransformingBoundingBox) {
|
||||||
|
stageCursor = undefined;
|
||||||
|
} else if (isMouseOverBoundingBox) {
|
||||||
|
stageCursor = 'move';
|
||||||
|
} else {
|
||||||
|
if (isMovingStage) {
|
||||||
|
stageCursor = 'grabbing';
|
||||||
|
} else {
|
||||||
|
stageCursor = 'grab';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stageCursor = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
shouldInvertMask,
|
||||||
|
isMaskEnabled,
|
||||||
|
shouldShowCheckboardTransparency,
|
||||||
|
stageScale,
|
||||||
|
shouldShowBoundingBox,
|
||||||
|
shouldLockBoundingBox,
|
||||||
|
shouldShowGrid,
|
||||||
|
isTransformingBoundingBox,
|
||||||
|
isModifyingBoundingBox: isTransformingBoundingBox || isMovingBoundingBox,
|
||||||
|
stageCursor,
|
||||||
|
isMouseOverBoundingBox,
|
||||||
|
stageDimensions,
|
||||||
|
stageCoordinates,
|
||||||
|
isMoveStageKeyHeld,
|
||||||
|
activeTabName,
|
||||||
|
baseCanvasImage,
|
||||||
|
tool,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use a closure allow other components to use these things... not ideal...
|
||||||
|
export let stageRef: MutableRefObject<StageType | null>;
|
||||||
|
export let canvasImageLayerRef: MutableRefObject<Konva.Layer | null>;
|
||||||
|
export let inpaintingImageElementRef: MutableRefObject<HTMLImageElement | null>;
|
||||||
|
|
||||||
|
const IAICanvas = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const {
|
||||||
|
shouldInvertMask,
|
||||||
|
isMaskEnabled,
|
||||||
|
shouldShowCheckboardTransparency,
|
||||||
|
stageScale,
|
||||||
|
shouldShowBoundingBox,
|
||||||
|
isModifyingBoundingBox,
|
||||||
|
stageCursor,
|
||||||
|
stageDimensions,
|
||||||
|
stageCoordinates,
|
||||||
|
shouldShowGrid,
|
||||||
|
activeTabName,
|
||||||
|
baseCanvasImage,
|
||||||
|
tool,
|
||||||
|
} = useAppSelector(canvasSelector);
|
||||||
|
|
||||||
|
useCanvasHotkeys();
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
// set the closure'd refs
|
||||||
|
stageRef = useRef<StageType>(null);
|
||||||
|
canvasImageLayerRef = useRef<Konva.Layer>(null);
|
||||||
|
inpaintingImageElementRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
|
const lastCursorPositionRef = useRef<Vector2d>({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
// Use refs for values that do not affect rendering, other values in redux
|
||||||
|
const didMouseMoveRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
// Load the image into this
|
||||||
|
const [canvasBgImage, setCanvasBgImage] = useState<HTMLImageElement | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleWheel = useCanvasWheel(stageRef);
|
||||||
|
const handleMouseDown = useCanvasMouseDown(stageRef);
|
||||||
|
const handleMouseUp = useCanvasMouseUp(stageRef, didMouseMoveRef);
|
||||||
|
const handleMouseMove = useCanvasMouseMove(
|
||||||
|
stageRef,
|
||||||
|
didMouseMoveRef,
|
||||||
|
lastCursorPositionRef
|
||||||
|
);
|
||||||
|
const handleMouseEnter = useCanvasMouseEnter(stageRef);
|
||||||
|
const handleMouseOut = useCanvasMouseOut();
|
||||||
|
const { handleDragStart, handleDragMove, handleDragEnd } =
|
||||||
|
useCanvasDragMove();
|
||||||
|
|
||||||
|
// Load the image and set the options panel width & height
|
||||||
|
useEffect(() => {
|
||||||
|
if (baseCanvasImage) {
|
||||||
|
const image = new Image();
|
||||||
|
image.onload = () => {
|
||||||
|
inpaintingImageElementRef.current = image;
|
||||||
|
setCanvasBgImage(image);
|
||||||
|
};
|
||||||
|
image.onerror = () => {
|
||||||
|
toast({
|
||||||
|
title: 'Unable to Load Image',
|
||||||
|
description: `Image ${baseCanvasImage.url} failed to load`,
|
||||||
|
status: 'error',
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
dispatch(clearImageToInpaint());
|
||||||
|
};
|
||||||
|
image.src = baseCanvasImage.url;
|
||||||
|
} else {
|
||||||
|
setCanvasBgImage(null);
|
||||||
|
}
|
||||||
|
}, [baseCanvasImage, dispatch, stageScale, toast]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inpainting-canvas-container">
|
||||||
|
<div className="inpainting-canvas-wrapper">
|
||||||
|
<Stage
|
||||||
|
ref={stageRef}
|
||||||
|
style={{ ...(stageCursor ? { cursor: stageCursor } : {}) }}
|
||||||
|
className="inpainting-canvas-stage checkerboard"
|
||||||
|
x={stageCoordinates.x}
|
||||||
|
y={stageCoordinates.y}
|
||||||
|
width={stageDimensions.width}
|
||||||
|
height={stageDimensions.height}
|
||||||
|
scale={{ x: stageScale, y: stageScale }}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseOut}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseOut={handleMouseOut}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragMove={handleDragMove}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
onWheel={handleWheel}
|
||||||
|
listening={
|
||||||
|
tool === 'move' &&
|
||||||
|
!isModifyingBoundingBox &&
|
||||||
|
activeTabName === 'outpainting'
|
||||||
|
}
|
||||||
|
draggable={
|
||||||
|
tool === 'move' &&
|
||||||
|
!isModifyingBoundingBox &&
|
||||||
|
activeTabName === 'outpainting'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Layer visible={shouldShowGrid}>
|
||||||
|
<IAICanvasGrid />
|
||||||
|
</Layer>
|
||||||
|
|
||||||
|
<Layer
|
||||||
|
id={'image-layer'}
|
||||||
|
ref={canvasImageLayerRef}
|
||||||
|
listening={false}
|
||||||
|
imageSmoothingEnabled={false}
|
||||||
|
>
|
||||||
|
<IAICanvasOutpaintingObjects />
|
||||||
|
<IAICanvasIntermediateImage />
|
||||||
|
</Layer>
|
||||||
|
<Layer id={'mask-layer'} visible={isMaskEnabled} listening={false}>
|
||||||
|
<IAICanvasMaskLines visible={true} listening={false} />
|
||||||
|
|
||||||
|
<IAICanvasMaskCompositer listening={false} />
|
||||||
|
|
||||||
|
{canvasBgImage && (
|
||||||
|
<>
|
||||||
|
<KonvaImage
|
||||||
|
image={canvasBgImage}
|
||||||
|
listening={false}
|
||||||
|
globalCompositeOperation="source-in"
|
||||||
|
visible={shouldInvertMask}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<KonvaImage
|
||||||
|
image={canvasBgImage}
|
||||||
|
listening={false}
|
||||||
|
globalCompositeOperation="source-out"
|
||||||
|
visible={
|
||||||
|
!shouldInvertMask && shouldShowCheckboardTransparency
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Layer>
|
||||||
|
<Layer id={'preview-layer'}>
|
||||||
|
<IAICanvasBoundingBoxPreview visible={shouldShowBoundingBox} />
|
||||||
|
<IAICanvasBrushPreview
|
||||||
|
visible={tool !== 'move'}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
</Layer>
|
||||||
|
</Stage>
|
||||||
|
<IAICanvasStatusText />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvas;
|
@ -7,58 +7,60 @@ import { Vector2d } from 'konva/lib/types';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { Group, Rect, Transformer } from 'react-konva';
|
import { Group, Rect, Transformer } from 'react-konva';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||||
import {
|
import {
|
||||||
RootState,
|
baseCanvasImageSelector,
|
||||||
useAppDispatch,
|
currentCanvasSelector,
|
||||||
useAppSelector,
|
outpaintingCanvasSelector,
|
||||||
} from '../../../../app/store';
|
setBoundingBoxCoordinates,
|
||||||
import { roundToMultiple } from '../../../../common/util/roundDownToMultiple';
|
|
||||||
import {
|
|
||||||
InpaintingState,
|
|
||||||
setBoundingBoxCoordinate,
|
|
||||||
setBoundingBoxDimensions,
|
setBoundingBoxDimensions,
|
||||||
setIsMouseOverBoundingBox,
|
setIsMouseOverBoundingBox,
|
||||||
setIsMovingBoundingBox,
|
setIsMovingBoundingBox,
|
||||||
setIsTransformingBoundingBox,
|
setIsTransformingBoundingBox,
|
||||||
} from '../inpaintingSlice';
|
} from 'features/canvas/canvasSlice';
|
||||||
import { rgbaColorToString } from '../util/colorToString';
|
import { GroupConfig } from 'konva/lib/Group';
|
||||||
import {
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
DASH_WIDTH,
|
|
||||||
// MARCHING_ANTS_SPEED,
|
|
||||||
} from '../util/constants';
|
|
||||||
|
|
||||||
const boundingBoxPreviewSelector = createSelector(
|
const boundingBoxPreviewSelector = createSelector(
|
||||||
(state: RootState) => state.inpainting,
|
currentCanvasSelector,
|
||||||
(inpainting: InpaintingState) => {
|
outpaintingCanvasSelector,
|
||||||
|
baseCanvasImageSelector,
|
||||||
|
activeTabNameSelector,
|
||||||
|
(currentCanvas, outpaintingCanvas, baseCanvasImage, activeTabName) => {
|
||||||
const {
|
const {
|
||||||
boundingBoxCoordinate,
|
boundingBoxCoordinates,
|
||||||
boundingBoxDimensions,
|
boundingBoxDimensions,
|
||||||
boundingBoxPreviewFill,
|
stageDimensions,
|
||||||
canvasDimensions,
|
|
||||||
stageScale,
|
stageScale,
|
||||||
imageToInpaint,
|
|
||||||
shouldLockBoundingBox,
|
shouldLockBoundingBox,
|
||||||
isDrawing,
|
isDrawing,
|
||||||
isTransformingBoundingBox,
|
isTransformingBoundingBox,
|
||||||
isMovingBoundingBox,
|
isMovingBoundingBox,
|
||||||
isMouseOverBoundingBox,
|
isMouseOverBoundingBox,
|
||||||
isSpacebarHeld,
|
shouldDarkenOutsideBoundingBox,
|
||||||
} = inpainting;
|
tool,
|
||||||
|
stageCoordinates,
|
||||||
|
} = currentCanvas;
|
||||||
|
const { shouldSnapToGrid } = outpaintingCanvas;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
boundingBoxCoordinate,
|
boundingBoxCoordinates,
|
||||||
boundingBoxDimensions,
|
boundingBoxDimensions,
|
||||||
boundingBoxPreviewFillString: rgbaColorToString(boundingBoxPreviewFill),
|
|
||||||
canvasDimensions,
|
|
||||||
stageScale,
|
|
||||||
imageToInpaint,
|
|
||||||
dash: DASH_WIDTH / stageScale, // scale dash lengths
|
|
||||||
strokeWidth: 1 / stageScale, // scale stroke thickness
|
|
||||||
shouldLockBoundingBox,
|
|
||||||
isDrawing,
|
isDrawing,
|
||||||
isTransformingBoundingBox,
|
|
||||||
isMouseOverBoundingBox,
|
isMouseOverBoundingBox,
|
||||||
|
shouldDarkenOutsideBoundingBox,
|
||||||
isMovingBoundingBox,
|
isMovingBoundingBox,
|
||||||
isSpacebarHeld,
|
isTransformingBoundingBox,
|
||||||
|
shouldLockBoundingBox,
|
||||||
|
stageDimensions,
|
||||||
|
stageScale,
|
||||||
|
baseCanvasImage,
|
||||||
|
activeTabName,
|
||||||
|
shouldSnapToGrid,
|
||||||
|
tool,
|
||||||
|
stageCoordinates,
|
||||||
|
boundingBoxStrokeWidth: (isMouseOverBoundingBox ? 8 : 1) / stageScale,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -68,52 +70,31 @@ const boundingBoxPreviewSelector = createSelector(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
type IAICanvasBoundingBoxPreviewProps = GroupConfig;
|
||||||
* Shades the area around the mask.
|
|
||||||
*/
|
|
||||||
export const InpaintingBoundingBoxPreviewOverlay = () => {
|
|
||||||
const {
|
|
||||||
boundingBoxCoordinate,
|
|
||||||
boundingBoxDimensions,
|
|
||||||
boundingBoxPreviewFillString,
|
|
||||||
canvasDimensions,
|
|
||||||
} = useAppSelector(boundingBoxPreviewSelector);
|
|
||||||
|
|
||||||
return (
|
const IAICanvasBoundingBoxPreview = (
|
||||||
<Group>
|
props: IAICanvasBoundingBoxPreviewProps
|
||||||
<Rect
|
) => {
|
||||||
x={0}
|
const { ...rest } = props;
|
||||||
y={0}
|
|
||||||
height={canvasDimensions.height}
|
|
||||||
width={canvasDimensions.width}
|
|
||||||
fill={boundingBoxPreviewFillString}
|
|
||||||
/>
|
|
||||||
<Rect
|
|
||||||
x={boundingBoxCoordinate.x}
|
|
||||||
y={boundingBoxCoordinate.y}
|
|
||||||
width={boundingBoxDimensions.width}
|
|
||||||
height={boundingBoxDimensions.height}
|
|
||||||
fill={'rgb(255,255,255)'}
|
|
||||||
listening={false}
|
|
||||||
globalCompositeOperation={'destination-out'}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const InpaintingBoundingBoxPreview = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const {
|
const {
|
||||||
boundingBoxCoordinate,
|
boundingBoxCoordinates,
|
||||||
boundingBoxDimensions,
|
boundingBoxDimensions,
|
||||||
stageScale,
|
|
||||||
imageToInpaint,
|
|
||||||
shouldLockBoundingBox,
|
|
||||||
isDrawing,
|
isDrawing,
|
||||||
isTransformingBoundingBox,
|
|
||||||
isMovingBoundingBox,
|
|
||||||
isMouseOverBoundingBox,
|
isMouseOverBoundingBox,
|
||||||
isSpacebarHeld,
|
shouldDarkenOutsideBoundingBox,
|
||||||
|
isMovingBoundingBox,
|
||||||
|
isTransformingBoundingBox,
|
||||||
|
shouldLockBoundingBox,
|
||||||
|
stageCoordinates,
|
||||||
|
stageDimensions,
|
||||||
|
stageScale,
|
||||||
|
baseCanvasImage,
|
||||||
|
activeTabName,
|
||||||
|
shouldSnapToGrid,
|
||||||
|
tool,
|
||||||
|
boundingBoxStrokeWidth,
|
||||||
} = useAppSelector(boundingBoxPreviewSelector);
|
} = useAppSelector(boundingBoxPreviewSelector);
|
||||||
|
|
||||||
const transformerRef = useRef<Konva.Transformer>(null);
|
const transformerRef = useRef<Konva.Transformer>(null);
|
||||||
@ -129,31 +110,60 @@ const InpaintingBoundingBoxPreview = () => {
|
|||||||
|
|
||||||
const handleOnDragMove = useCallback(
|
const handleOnDragMove = useCallback(
|
||||||
(e: KonvaEventObject<DragEvent>) => {
|
(e: KonvaEventObject<DragEvent>) => {
|
||||||
|
if (activeTabName === 'inpainting' || !shouldSnapToGrid) {
|
||||||
dispatch(
|
dispatch(
|
||||||
setBoundingBoxCoordinate({
|
setBoundingBoxCoordinates({
|
||||||
x: Math.floor(e.target.x()),
|
x: e.target.x(),
|
||||||
y: Math.floor(e.target.y()),
|
y: e.target.y(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dragX = e.target.x();
|
||||||
|
const dragY = e.target.y();
|
||||||
|
|
||||||
|
const newX = roundToMultiple(dragX, 64);
|
||||||
|
const newY = roundToMultiple(dragY, 64);
|
||||||
|
|
||||||
|
e.target.x(newX);
|
||||||
|
e.target.y(newY);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
setBoundingBoxCoordinates({
|
||||||
|
x: newX,
|
||||||
|
y: newY,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[dispatch]
|
[activeTabName, dispatch, shouldSnapToGrid]
|
||||||
);
|
);
|
||||||
|
|
||||||
const dragBoundFunc = useCallback(
|
const dragBoundFunc = useCallback(
|
||||||
(position: Vector2d) => {
|
(position: Vector2d) => {
|
||||||
if (!imageToInpaint) return boundingBoxCoordinate;
|
if (!baseCanvasImage) return boundingBoxCoordinates;
|
||||||
|
|
||||||
const { x, y } = position;
|
const { x, y } = position;
|
||||||
|
|
||||||
const maxX = imageToInpaint.width - boundingBoxDimensions.width;
|
const maxX =
|
||||||
const maxY = imageToInpaint.height - boundingBoxDimensions.height;
|
stageDimensions.width - boundingBoxDimensions.width * stageScale;
|
||||||
|
const maxY =
|
||||||
|
stageDimensions.height - boundingBoxDimensions.height * stageScale;
|
||||||
|
|
||||||
const clampedX = Math.floor(_.clamp(x, 0, maxX * stageScale));
|
const clampedX = Math.floor(_.clamp(x, 0, maxX));
|
||||||
const clampedY = Math.floor(_.clamp(y, 0, maxY * stageScale));
|
const clampedY = Math.floor(_.clamp(y, 0, maxY));
|
||||||
|
|
||||||
return { x: clampedX, y: clampedY };
|
return { x: clampedX, y: clampedY };
|
||||||
},
|
},
|
||||||
[boundingBoxCoordinate, boundingBoxDimensions, imageToInpaint, stageScale]
|
[
|
||||||
|
baseCanvasImage,
|
||||||
|
boundingBoxCoordinates,
|
||||||
|
stageDimensions.width,
|
||||||
|
stageDimensions.height,
|
||||||
|
boundingBoxDimensions.width,
|
||||||
|
boundingBoxDimensions.height,
|
||||||
|
stageScale,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOnTransform = useCallback(() => {
|
const handleOnTransform = useCallback(() => {
|
||||||
@ -184,7 +194,7 @@ const InpaintingBoundingBoxPreview = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
setBoundingBoxCoordinate({
|
setBoundingBoxCoordinates({
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
})
|
})
|
||||||
@ -195,6 +205,7 @@ const InpaintingBoundingBoxPreview = () => {
|
|||||||
rect.scaleY(1);
|
rect.scaleY(1);
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
// OK
|
||||||
const anchorDragBoundFunc = useCallback(
|
const anchorDragBoundFunc = useCallback(
|
||||||
(
|
(
|
||||||
oldPos: Vector2d, // old absolute position of anchor point
|
oldPos: Vector2d, // old absolute position of anchor point
|
||||||
@ -253,6 +264,7 @@ const InpaintingBoundingBoxPreview = () => {
|
|||||||
[scaledStep]
|
[scaledStep]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// OK
|
||||||
const boundBoxFunc = useCallback(
|
const boundBoxFunc = useCallback(
|
||||||
(oldBoundBox: Box, newBoundBox: Box) => {
|
(oldBoundBox: Box, newBoundBox: Box) => {
|
||||||
/**
|
/**
|
||||||
@ -260,12 +272,10 @@ const InpaintingBoundingBoxPreview = () => {
|
|||||||
* Unlike anchorDragBoundFunc, it does get a width and height, so
|
* Unlike anchorDragBoundFunc, it does get a width and height, so
|
||||||
* the logic to constrain the size of the bounding box is very simple.
|
* the logic to constrain the size of the bounding box is very simple.
|
||||||
*/
|
*/
|
||||||
if (!imageToInpaint) return oldBoundBox;
|
if (!baseCanvasImage) return oldBoundBox;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
newBoundBox.width + newBoundBox.x > imageToInpaint.width * stageScale ||
|
newBoundBox.width + newBoundBox.x > stageDimensions.width ||
|
||||||
newBoundBox.height + newBoundBox.y >
|
newBoundBox.height + newBoundBox.y > stageDimensions.height ||
|
||||||
imageToInpaint.height * stageScale ||
|
|
||||||
newBoundBox.x < 0 ||
|
newBoundBox.x < 0 ||
|
||||||
newBoundBox.y < 0
|
newBoundBox.y < 0
|
||||||
) {
|
) {
|
||||||
@ -274,101 +284,107 @@ const InpaintingBoundingBoxPreview = () => {
|
|||||||
|
|
||||||
return newBoundBox;
|
return newBoundBox;
|
||||||
},
|
},
|
||||||
[imageToInpaint, stageScale]
|
[baseCanvasImage, stageDimensions]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleStartedTransforming = (e: KonvaEventObject<MouseEvent>) => {
|
const handleStartedTransforming = () => {
|
||||||
e.cancelBubble = true;
|
|
||||||
e.evt.stopImmediatePropagation();
|
|
||||||
console.log("Started transform")
|
|
||||||
dispatch(setIsTransformingBoundingBox(true));
|
dispatch(setIsTransformingBoundingBox(true));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEndedTransforming = (e: KonvaEventObject<MouseEvent>) => {
|
const handleEndedTransforming = () => {
|
||||||
dispatch(setIsTransformingBoundingBox(false));
|
dispatch(setIsTransformingBoundingBox(false));
|
||||||
dispatch(setIsMouseOverBoundingBox(false));
|
dispatch(setIsMouseOverBoundingBox(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStartedMoving = (e: KonvaEventObject<MouseEvent>) => {
|
const handleStartedMoving = () => {
|
||||||
e.cancelBubble = true;
|
|
||||||
e.evt.stopImmediatePropagation();
|
|
||||||
dispatch(setIsMovingBoundingBox(true));
|
dispatch(setIsMovingBoundingBox(true));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEndedModifying = (e: KonvaEventObject<MouseEvent>) => {
|
const handleEndedModifying = () => {
|
||||||
dispatch(setIsTransformingBoundingBox(false));
|
dispatch(setIsTransformingBoundingBox(false));
|
||||||
dispatch(setIsMovingBoundingBox(false));
|
dispatch(setIsMovingBoundingBox(false));
|
||||||
dispatch(setIsMouseOverBoundingBox(false));
|
dispatch(setIsMouseOverBoundingBox(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const spacebarHeldHitFunc = (context: Context, shape: Konva.Shape) => {
|
const handleMouseOver = () => {
|
||||||
context.rect(0, 0, imageToInpaint?.width, imageToInpaint?.height);
|
dispatch(setIsMouseOverBoundingBox(true));
|
||||||
context.fillShape(shape);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const handleMouseOut = () => {
|
||||||
<>
|
|
||||||
<Rect
|
|
||||||
x={boundingBoxCoordinate.x}
|
|
||||||
y={boundingBoxCoordinate.y}
|
|
||||||
width={boundingBoxDimensions.width}
|
|
||||||
height={boundingBoxDimensions.height}
|
|
||||||
ref={shapeRef}
|
|
||||||
stroke={isMouseOverBoundingBox ? 'rgba(255,255,255,0.3)' : 'white'}
|
|
||||||
strokeWidth={Math.floor((isMouseOverBoundingBox ? 8 : 1) / stageScale)}
|
|
||||||
fillEnabled={isSpacebarHeld}
|
|
||||||
hitFunc={isSpacebarHeld ? spacebarHeldHitFunc : undefined}
|
|
||||||
hitStrokeWidth={Math.floor(13 / stageScale)}
|
|
||||||
listening={!isDrawing && !shouldLockBoundingBox}
|
|
||||||
onMouseOver={() => {
|
|
||||||
dispatch(setIsMouseOverBoundingBox(true));
|
|
||||||
}}
|
|
||||||
onMouseOut={() => {
|
|
||||||
!isTransformingBoundingBox &&
|
!isTransformingBoundingBox &&
|
||||||
!isMovingBoundingBox &&
|
!isMovingBoundingBox &&
|
||||||
dispatch(setIsMouseOverBoundingBox(false));
|
dispatch(setIsMouseOverBoundingBox(false));
|
||||||
}}
|
};
|
||||||
onMouseDown={handleStartedMoving}
|
|
||||||
onMouseUp={handleEndedModifying}
|
return (
|
||||||
|
<Group {...rest}>
|
||||||
|
<Rect
|
||||||
|
offsetX={stageCoordinates.x / stageScale}
|
||||||
|
offsetY={stageCoordinates.y / stageScale}
|
||||||
|
height={stageDimensions.height / stageScale}
|
||||||
|
width={stageDimensions.width / stageScale}
|
||||||
|
fill={'rgba(0,0,0,0.4)'}
|
||||||
|
listening={false}
|
||||||
|
visible={shouldDarkenOutsideBoundingBox}
|
||||||
|
/>
|
||||||
|
<Rect
|
||||||
|
x={boundingBoxCoordinates.x}
|
||||||
|
y={boundingBoxCoordinates.y}
|
||||||
|
width={boundingBoxDimensions.width}
|
||||||
|
height={boundingBoxDimensions.height}
|
||||||
|
fill={'rgb(255,255,255)'}
|
||||||
|
listening={false}
|
||||||
|
visible={shouldDarkenOutsideBoundingBox}
|
||||||
|
globalCompositeOperation={'destination-out'}
|
||||||
|
/>
|
||||||
|
<Rect
|
||||||
|
dragBoundFunc={
|
||||||
|
activeTabName === 'inpainting' ? dragBoundFunc : undefined
|
||||||
|
}
|
||||||
draggable={true}
|
draggable={true}
|
||||||
onDragMove={handleOnDragMove}
|
fillEnabled={tool === 'move'}
|
||||||
dragBoundFunc={dragBoundFunc}
|
height={boundingBoxDimensions.height}
|
||||||
onTransform={handleOnTransform}
|
listening={!isDrawing && tool === 'move'}
|
||||||
onDragEnd={handleEndedModifying}
|
onDragEnd={handleEndedModifying}
|
||||||
|
onDragMove={handleOnDragMove}
|
||||||
|
onMouseDown={handleStartedMoving}
|
||||||
|
onMouseOut={handleMouseOut}
|
||||||
|
onMouseOver={handleMouseOver}
|
||||||
|
onMouseUp={handleEndedModifying}
|
||||||
|
onTransform={handleOnTransform}
|
||||||
onTransformEnd={handleEndedTransforming}
|
onTransformEnd={handleEndedTransforming}
|
||||||
|
ref={shapeRef}
|
||||||
|
stroke={isMouseOverBoundingBox ? 'rgba(255,255,255,0.7)' : 'white'}
|
||||||
|
strokeWidth={boundingBoxStrokeWidth}
|
||||||
|
width={boundingBoxDimensions.width}
|
||||||
|
x={boundingBoxCoordinates.x}
|
||||||
|
y={boundingBoxCoordinates.y}
|
||||||
/>
|
/>
|
||||||
<Transformer
|
<Transformer
|
||||||
ref={transformerRef}
|
|
||||||
anchorCornerRadius={3}
|
anchorCornerRadius={3}
|
||||||
|
anchorDragBoundFunc={anchorDragBoundFunc}
|
||||||
anchorFill={'rgba(212,216,234,1)'}
|
anchorFill={'rgba(212,216,234,1)'}
|
||||||
anchorSize={15}
|
anchorSize={15}
|
||||||
anchorStroke={'rgb(42,42,42)'}
|
anchorStroke={'rgb(42,42,42)'}
|
||||||
borderDash={[4, 4]}
|
borderDash={[4, 4]}
|
||||||
borderStroke={'black'}
|
|
||||||
rotateEnabled={false}
|
|
||||||
borderEnabled={true}
|
borderEnabled={true}
|
||||||
|
borderStroke={'black'}
|
||||||
|
boundBoxFunc={boundBoxFunc}
|
||||||
|
draggable={false}
|
||||||
|
enabledAnchors={tool === 'move' ? undefined : []}
|
||||||
flipEnabled={false}
|
flipEnabled={false}
|
||||||
ignoreStroke={true}
|
ignoreStroke={true}
|
||||||
keepRatio={false}
|
keepRatio={false}
|
||||||
listening={!isDrawing && !shouldLockBoundingBox}
|
listening={!isDrawing && tool === 'move'}
|
||||||
|
onDragEnd={handleEndedModifying}
|
||||||
onMouseDown={handleStartedTransforming}
|
onMouseDown={handleStartedTransforming}
|
||||||
onMouseUp={handleEndedTransforming}
|
onMouseUp={handleEndedTransforming}
|
||||||
enabledAnchors={shouldLockBoundingBox ? [] : undefined}
|
|
||||||
boundBoxFunc={boundBoxFunc}
|
|
||||||
anchorDragBoundFunc={anchorDragBoundFunc}
|
|
||||||
onDragEnd={handleEndedModifying}
|
|
||||||
onTransformEnd={handleEndedTransforming}
|
onTransformEnd={handleEndedTransforming}
|
||||||
onMouseOver={() => {
|
ref={transformerRef}
|
||||||
dispatch(setIsMouseOverBoundingBox(true));
|
rotateEnabled={false}
|
||||||
}}
|
|
||||||
onMouseOut={() => {
|
|
||||||
!isTransformingBoundingBox &&
|
|
||||||
!isMovingBoundingBox &&
|
|
||||||
dispatch(setIsMouseOverBoundingBox(false));
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</Group>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InpaintingBoundingBoxPreview;
|
export default IAICanvasBoundingBoxPreview;
|
95
frontend/src/features/canvas/IAICanvasBrushButtonPopover.tsx
Normal file
95
frontend/src/features/canvas/IAICanvasBrushButtonPopover.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import {
|
||||||
|
currentCanvasSelector,
|
||||||
|
outpaintingCanvasSelector,
|
||||||
|
setBrushColor,
|
||||||
|
setBrushSize,
|
||||||
|
setTool,
|
||||||
|
} from './canvasSlice';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { FaPaintBrush } from 'react-icons/fa';
|
||||||
|
import IAIPopover from 'common/components/IAIPopover';
|
||||||
|
import IAIColorPicker from 'common/components/IAIColorPicker';
|
||||||
|
import IAISlider from 'common/components/IAISlider';
|
||||||
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
import IAINumberInput from 'common/components/IAINumberInput';
|
||||||
|
|
||||||
|
export const selector = createSelector(
|
||||||
|
[currentCanvasSelector, outpaintingCanvasSelector, activeTabNameSelector],
|
||||||
|
(currentCanvas, outpaintingCanvas, activeTabName) => {
|
||||||
|
const {
|
||||||
|
layer,
|
||||||
|
maskColor,
|
||||||
|
brushColor,
|
||||||
|
brushSize,
|
||||||
|
eraserSize,
|
||||||
|
tool,
|
||||||
|
shouldDarkenOutsideBoundingBox,
|
||||||
|
shouldShowIntermediates,
|
||||||
|
} = currentCanvas;
|
||||||
|
|
||||||
|
const { shouldShowGrid, shouldSnapToGrid, shouldAutoSave } =
|
||||||
|
outpaintingCanvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
layer,
|
||||||
|
tool,
|
||||||
|
maskColor,
|
||||||
|
brushColor,
|
||||||
|
brushSize,
|
||||||
|
eraserSize,
|
||||||
|
activeTabName,
|
||||||
|
shouldShowGrid,
|
||||||
|
shouldSnapToGrid,
|
||||||
|
shouldAutoSave,
|
||||||
|
shouldDarkenOutsideBoundingBox,
|
||||||
|
shouldShowIntermediates,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const IAICanvasBrushButtonPopover = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { tool, brushColor, brushSize } = useAppSelector(selector);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIPopover
|
||||||
|
trigger="hover"
|
||||||
|
triggerComponent={
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Brush (B)"
|
||||||
|
tooltip="Brush (B)"
|
||||||
|
icon={<FaPaintBrush />}
|
||||||
|
data-selected={tool === 'brush'}
|
||||||
|
onClick={() => dispatch(setTool('brush'))}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Flex minWidth={'15rem'} direction={'column'} gap={'1rem'} width={'100%'}>
|
||||||
|
<Flex gap={'1rem'} justifyContent="space-between">
|
||||||
|
<IAISlider
|
||||||
|
label="Size"
|
||||||
|
value={brushSize}
|
||||||
|
withInput
|
||||||
|
onChange={(newSize) => dispatch(setBrushSize(newSize))}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<IAIColorPicker
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
color={brushColor}
|
||||||
|
onChange={(newColor) => dispatch(setBrushColor(newColor))}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</IAIPopover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasBrushButtonPopover;
|
121
frontend/src/features/canvas/IAICanvasBrushPreview.tsx
Normal file
121
frontend/src/features/canvas/IAICanvasBrushPreview.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { GroupConfig } from 'konva/lib/Group';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { Circle, Group } from 'react-konva';
|
||||||
|
import { useAppSelector } from 'app/store';
|
||||||
|
import { currentCanvasSelector } from 'features/canvas/canvasSlice';
|
||||||
|
import { rgbaColorToString } from './util/colorToString';
|
||||||
|
|
||||||
|
const canvasBrushPreviewSelector = createSelector(
|
||||||
|
currentCanvasSelector,
|
||||||
|
(currentCanvas) => {
|
||||||
|
const {
|
||||||
|
cursorPosition,
|
||||||
|
stageDimensions: { width, height },
|
||||||
|
brushSize,
|
||||||
|
eraserSize,
|
||||||
|
maskColor,
|
||||||
|
brushColor,
|
||||||
|
tool,
|
||||||
|
layer,
|
||||||
|
shouldShowBrush,
|
||||||
|
isMovingBoundingBox,
|
||||||
|
isTransformingBoundingBox,
|
||||||
|
stageScale,
|
||||||
|
} = currentCanvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
cursorPosition,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
radius: tool === 'brush' ? brushSize / 2 : eraserSize / 2,
|
||||||
|
brushColorString: rgbaColorToString(
|
||||||
|
layer === 'mask' ? maskColor : brushColor
|
||||||
|
),
|
||||||
|
tool,
|
||||||
|
shouldShowBrush,
|
||||||
|
shouldDrawBrushPreview:
|
||||||
|
!(
|
||||||
|
isMovingBoundingBox ||
|
||||||
|
isTransformingBoundingBox ||
|
||||||
|
!cursorPosition
|
||||||
|
) && shouldShowBrush,
|
||||||
|
strokeWidth: 1.5 / stageScale,
|
||||||
|
dotRadius: 1.5 / stageScale,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a black circle around the canvas brush preview.
|
||||||
|
*/
|
||||||
|
const IAICanvasBrushPreview = (props: GroupConfig) => {
|
||||||
|
const { ...rest } = props;
|
||||||
|
const {
|
||||||
|
cursorPosition,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
radius,
|
||||||
|
brushColorString,
|
||||||
|
tool,
|
||||||
|
shouldDrawBrushPreview,
|
||||||
|
dotRadius,
|
||||||
|
strokeWidth,
|
||||||
|
} = useAppSelector(canvasBrushPreviewSelector);
|
||||||
|
|
||||||
|
if (!shouldDrawBrushPreview) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group listening={false} {...rest}>
|
||||||
|
<Circle
|
||||||
|
x={cursorPosition ? cursorPosition.x : width / 2}
|
||||||
|
y={cursorPosition ? cursorPosition.y : height / 2}
|
||||||
|
radius={radius}
|
||||||
|
fill={brushColorString}
|
||||||
|
listening={false}
|
||||||
|
globalCompositeOperation={
|
||||||
|
tool === 'eraser' ? 'destination-out' : 'source-over'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Circle
|
||||||
|
x={cursorPosition ? cursorPosition.x : width / 2}
|
||||||
|
y={cursorPosition ? cursorPosition.y : height / 2}
|
||||||
|
radius={radius}
|
||||||
|
stroke={'rgba(255,255,255,0.4)'}
|
||||||
|
strokeWidth={strokeWidth * 2}
|
||||||
|
strokeEnabled={true}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
<Circle
|
||||||
|
x={cursorPosition ? cursorPosition.x : width / 2}
|
||||||
|
y={cursorPosition ? cursorPosition.y : height / 2}
|
||||||
|
radius={radius}
|
||||||
|
stroke={'rgba(0,0,0,1)'}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeEnabled={true}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
<Circle
|
||||||
|
x={cursorPosition ? cursorPosition.x : width / 2}
|
||||||
|
y={cursorPosition ? cursorPosition.y : height / 2}
|
||||||
|
radius={dotRadius * 2}
|
||||||
|
fill={'rgba(255,255,255,0.4)'}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
<Circle
|
||||||
|
x={cursorPosition ? cursorPosition.x : width / 2}
|
||||||
|
y={cursorPosition ? cursorPosition.y : height / 2}
|
||||||
|
radius={dotRadius}
|
||||||
|
fill={'rgba(0,0,0,1)'}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasBrushPreview;
|
100
frontend/src/features/canvas/IAICanvasControls.tsx
Normal file
100
frontend/src/features/canvas/IAICanvasControls.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import IAICanvasBrushControl from './IAICanvasControls/IAICanvasBrushControl';
|
||||||
|
import IAICanvasEraserControl from './IAICanvasControls/IAICanvasEraserControl';
|
||||||
|
import IAICanvasUndoControl from './IAICanvasControls/IAICanvasUndoButton';
|
||||||
|
import IAICanvasRedoControl from './IAICanvasControls/IAICanvasRedoButton';
|
||||||
|
import { Button, ButtonGroup } from '@chakra-ui/react';
|
||||||
|
import IAICanvasMaskClear from './IAICanvasControls/IAICanvasMaskControls/IAICanvasMaskClear';
|
||||||
|
import IAICanvasMaskVisibilityControl from './IAICanvasControls/IAICanvasMaskControls/IAICanvasMaskVisibilityControl';
|
||||||
|
import IAICanvasMaskInvertControl from './IAICanvasControls/IAICanvasMaskControls/IAICanvasMaskInvertControl';
|
||||||
|
import IAICanvasLockBoundingBoxControl from './IAICanvasControls/IAICanvasLockBoundingBoxControl';
|
||||||
|
import IAICanvasShowHideBoundingBoxControl from './IAICanvasControls/IAICanvasShowHideBoundingBoxControl';
|
||||||
|
import ImageUploaderIconButton from 'common/components/ImageUploaderIconButton';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import {
|
||||||
|
currentCanvasSelector,
|
||||||
|
GenericCanvasState,
|
||||||
|
outpaintingCanvasSelector,
|
||||||
|
OutpaintingCanvasState,
|
||||||
|
uploadOutpaintingMergedImage,
|
||||||
|
} from './canvasSlice';
|
||||||
|
import { RootState, useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import { OptionsState } from 'features/options/optionsSlice';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import IAICanvasImageEraserControl from './IAICanvasControls/IAICanvasImageEraserControl';
|
||||||
|
import { canvasImageLayerRef } from './IAICanvas';
|
||||||
|
import { uploadImage } from 'app/socketio/actions';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { FaSave } from 'react-icons/fa';
|
||||||
|
|
||||||
|
export const canvasControlsSelector = createSelector(
|
||||||
|
[
|
||||||
|
outpaintingCanvasSelector,
|
||||||
|
(state: RootState) => state.options,
|
||||||
|
activeTabNameSelector,
|
||||||
|
],
|
||||||
|
(
|
||||||
|
outpaintingCanvas: OutpaintingCanvasState,
|
||||||
|
options: OptionsState,
|
||||||
|
activeTabName
|
||||||
|
) => {
|
||||||
|
const { stageScale, boundingBoxCoordinates, boundingBoxDimensions } =
|
||||||
|
outpaintingCanvas;
|
||||||
|
return {
|
||||||
|
activeTabName,
|
||||||
|
stageScale,
|
||||||
|
boundingBoxCoordinates,
|
||||||
|
boundingBoxDimensions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const IAICanvasControls = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const {
|
||||||
|
activeTabName,
|
||||||
|
boundingBoxCoordinates,
|
||||||
|
boundingBoxDimensions,
|
||||||
|
stageScale,
|
||||||
|
} = useAppSelector(canvasControlsSelector);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inpainting-settings">
|
||||||
|
<ButtonGroup isAttached={true}>
|
||||||
|
<IAICanvasBrushControl />
|
||||||
|
<IAICanvasEraserControl />
|
||||||
|
{activeTabName === 'outpainting' && <IAICanvasImageEraserControl />}
|
||||||
|
</ButtonGroup>
|
||||||
|
<ButtonGroup isAttached={true}>
|
||||||
|
<IAICanvasMaskVisibilityControl />
|
||||||
|
<IAICanvasMaskInvertControl />
|
||||||
|
<IAICanvasLockBoundingBoxControl />
|
||||||
|
<IAICanvasShowHideBoundingBoxControl />
|
||||||
|
<IAICanvasMaskClear />
|
||||||
|
</ButtonGroup>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Save"
|
||||||
|
tooltip="Save"
|
||||||
|
icon={<FaSave />}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(uploadOutpaintingMergedImage(canvasImageLayerRef));
|
||||||
|
}}
|
||||||
|
fontSize={20}
|
||||||
|
/>
|
||||||
|
<ButtonGroup isAttached={true}>
|
||||||
|
<IAICanvasUndoControl />
|
||||||
|
<IAICanvasRedoControl />
|
||||||
|
</ButtonGroup>
|
||||||
|
<ButtonGroup isAttached={true}>
|
||||||
|
<ImageUploaderIconButton />
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasControls;
|
@ -1,37 +1,31 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import React from 'react';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { FaPaintBrush } from 'react-icons/fa';
|
import { FaPaintBrush } from 'react-icons/fa';
|
||||||
import {
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
RootState,
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
useAppDispatch,
|
import IAINumberInput from 'common/components/IAINumberInput';
|
||||||
useAppSelector,
|
import IAIPopover from 'common/components/IAIPopover';
|
||||||
} from '../../../../app/store';
|
import IAISlider from 'common/components/IAISlider';
|
||||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
|
||||||
import IAIPopover from '../../../../common/components/IAIPopover';
|
|
||||||
import IAISlider from '../../../../common/components/IAISlider';
|
|
||||||
import { activeTabNameSelector } from '../../../options/optionsSelectors';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
InpaintingState,
|
currentCanvasSelector,
|
||||||
setBrushSize,
|
setBrushSize,
|
||||||
setShouldShowBrushPreview,
|
setShouldShowBrushPreview,
|
||||||
setTool,
|
setTool,
|
||||||
} from '../inpaintingSlice';
|
} from 'features/canvas/canvasSlice';
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import InpaintingMaskColorPicker from './InpaintingMaskControls/InpaintingMaskColorPicker';
|
import IAICanvasMaskColorPicker from './IAICanvasMaskControls/IAICanvasMaskColorPicker';
|
||||||
|
|
||||||
const inpaintingBrushSelector = createSelector(
|
const inpaintingBrushSelector = createSelector(
|
||||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
[currentCanvasSelector, activeTabNameSelector],
|
||||||
(inpainting: InpaintingState, activeTabName) => {
|
(currentCanvas, activeTabName) => {
|
||||||
const { tool, brushSize, shouldShowMask } = inpainting;
|
const { tool, brushSize } = currentCanvas;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tool,
|
tool,
|
||||||
brushSize,
|
brushSize,
|
||||||
shouldShowMask,
|
|
||||||
activeTabName,
|
activeTabName,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -42,9 +36,9 @@ const inpaintingBrushSelector = createSelector(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function InpaintingBrushControl() {
|
export default function IAICanvasBrushControl() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { tool, brushSize, shouldShowMask, activeTabName } = useAppSelector(
|
const { tool, brushSize, activeTabName } = useAppSelector(
|
||||||
inpaintingBrushSelector
|
inpaintingBrushSelector
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -63,9 +57,6 @@ export default function InpaintingBrushControl() {
|
|||||||
dispatch(setBrushSize(v));
|
dispatch(setBrushSize(v));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hotkeys
|
|
||||||
|
|
||||||
// Decrease brush size
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'[',
|
'[',
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
@ -77,9 +68,9 @@ export default function InpaintingBrushControl() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
enabled: true,
|
||||||
},
|
},
|
||||||
[activeTabName, shouldShowMask, brushSize]
|
[activeTabName, brushSize]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Increase brush size
|
// Increase brush size
|
||||||
@ -90,9 +81,9 @@ export default function InpaintingBrushControl() {
|
|||||||
handleChangeBrushSize(brushSize + 5);
|
handleChangeBrushSize(brushSize + 5);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
enabled: true,
|
||||||
},
|
},
|
||||||
[activeTabName, shouldShowMask, brushSize]
|
[activeTabName, brushSize]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set tool to brush
|
// Set tool to brush
|
||||||
@ -103,9 +94,9 @@ export default function InpaintingBrushControl() {
|
|||||||
handleSelectBrushTool();
|
handleSelectBrushTool();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
enabled: true,
|
||||||
},
|
},
|
||||||
[activeTabName, shouldShowMask]
|
[activeTabName]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -120,7 +111,6 @@ export default function InpaintingBrushControl() {
|
|||||||
icon={<FaPaintBrush />}
|
icon={<FaPaintBrush />}
|
||||||
onClick={handleSelectBrushTool}
|
onClick={handleSelectBrushTool}
|
||||||
data-selected={tool === 'brush'}
|
data-selected={tool === 'brush'}
|
||||||
isDisabled={!shouldShowMask}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -131,9 +121,6 @@ export default function InpaintingBrushControl() {
|
|||||||
onChange={handleChangeBrushSize}
|
onChange={handleChangeBrushSize}
|
||||||
min={1}
|
min={1}
|
||||||
max={200}
|
max={200}
|
||||||
width="100px"
|
|
||||||
focusThumbOnChange={false}
|
|
||||||
isDisabled={!shouldShowMask}
|
|
||||||
/>
|
/>
|
||||||
<IAINumberInput
|
<IAINumberInput
|
||||||
value={brushSize}
|
value={brushSize}
|
||||||
@ -141,9 +128,8 @@ export default function InpaintingBrushControl() {
|
|||||||
width={'80px'}
|
width={'80px'}
|
||||||
min={1}
|
min={1}
|
||||||
max={999}
|
max={999}
|
||||||
isDisabled={!shouldShowMask}
|
|
||||||
/>
|
/>
|
||||||
<InpaintingMaskColorPicker />
|
<IAICanvasMaskColorPicker />
|
||||||
</div>
|
</div>
|
||||||
</IAIPopover>
|
</IAIPopover>
|
||||||
);
|
);
|
@ -1,10 +1,9 @@
|
|||||||
import React from 'react';
|
|
||||||
import { FaTrash } from 'react-icons/fa';
|
import { FaTrash } from 'react-icons/fa';
|
||||||
import { useAppDispatch } from '../../../../app/store';
|
import { useAppDispatch } from 'app/store';
|
||||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import { clearImageToInpaint } from '../inpaintingSlice';
|
import { clearImageToInpaint } from 'features/canvas/canvasSlice';
|
||||||
|
|
||||||
export default function InpaintingClearImageControl() {
|
export default function IAICanvasClearImageControl() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleClearImage = () => {
|
const handleClearImage = () => {
|
@ -0,0 +1,61 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { FaEraser } from 'react-icons/fa';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { currentCanvasSelector, setTool } from 'features/canvas/canvasSlice';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
|
||||||
|
const eraserSelector = createSelector(
|
||||||
|
[currentCanvasSelector, activeTabNameSelector],
|
||||||
|
(currentCanvas, activeTabName) => {
|
||||||
|
const { tool, isMaskEnabled } = currentCanvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
tool,
|
||||||
|
isMaskEnabled,
|
||||||
|
activeTabName,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function IAICanvasEraserControl() {
|
||||||
|
const { tool, isMaskEnabled, activeTabName } = useAppSelector(eraserSelector);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const handleSelectEraserTool = () => dispatch(setTool('eraser'));
|
||||||
|
|
||||||
|
// Hotkeys
|
||||||
|
// Set tool to maskEraser
|
||||||
|
useHotkeys(
|
||||||
|
'e',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelectEraserTool();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
[activeTabName]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label={
|
||||||
|
activeTabName === 'inpainting' ? 'Eraser (E)' : 'Erase Mask (E)'
|
||||||
|
}
|
||||||
|
tooltip={activeTabName === 'inpainting' ? 'Eraser (E)' : 'Erase Mask (E)'}
|
||||||
|
icon={<FaEraser />}
|
||||||
|
onClick={handleSelectEraserTool}
|
||||||
|
data-selected={tool === 'eraser'}
|
||||||
|
isDisabled={!isMaskEnabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { currentCanvasSelector, setTool } from 'features/canvas/canvasSlice';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import { BsEraser } from 'react-icons/bs';
|
||||||
|
|
||||||
|
const imageEraserSelector = createSelector(
|
||||||
|
[currentCanvasSelector, activeTabNameSelector],
|
||||||
|
(currentCanvas, activeTabName) => {
|
||||||
|
const { tool, isMaskEnabled } = currentCanvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
tool,
|
||||||
|
isMaskEnabled,
|
||||||
|
activeTabName,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function IAICanvasImageEraserControl() {
|
||||||
|
const { tool, isMaskEnabled, activeTabName } =
|
||||||
|
useAppSelector(imageEraserSelector);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const handleSelectEraserTool = () => dispatch(setTool('eraser'));
|
||||||
|
|
||||||
|
// Hotkeys
|
||||||
|
useHotkeys(
|
||||||
|
'shift+e',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelectEraserTool();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
[activeTabName, isMaskEnabled]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Erase Canvas (Shift+E)"
|
||||||
|
tooltip="Erase Canvas (Shift+E)"
|
||||||
|
icon={<BsEraser />}
|
||||||
|
fontSize={18}
|
||||||
|
onClick={handleSelectEraserTool}
|
||||||
|
data-selected={tool === 'eraser'}
|
||||||
|
isDisabled={!isMaskEnabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import { FaLock, FaUnlock } from 'react-icons/fa';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import {
|
||||||
|
currentCanvasSelector,
|
||||||
|
GenericCanvasState,
|
||||||
|
setShouldLockBoundingBox,
|
||||||
|
} from 'features/canvas/canvasSlice';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
const canvasLockBoundingBoxSelector = createSelector(
|
||||||
|
currentCanvasSelector,
|
||||||
|
(currentCanvas: GenericCanvasState) => {
|
||||||
|
const { shouldLockBoundingBox } = currentCanvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
shouldLockBoundingBox,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const IAICanvasLockBoundingBoxControl = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { shouldLockBoundingBox } = useAppSelector(
|
||||||
|
canvasLockBoundingBoxSelector
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Lock Inpainting Box"
|
||||||
|
tooltip="Lock Inpainting Box"
|
||||||
|
icon={shouldLockBoundingBox ? <FaLock /> : <FaUnlock />}
|
||||||
|
data-selected={shouldLockBoundingBox}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(setShouldLockBoundingBox(!shouldLockBoundingBox));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasLockBoundingBoxControl;
|
@ -0,0 +1,38 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { FaMask } from 'react-icons/fa';
|
||||||
|
|
||||||
|
import IAIPopover from 'common/components/IAIPopover';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
|
||||||
|
import IAICanvasMaskInvertControl from './IAICanvasMaskControls/IAICanvasMaskInvertControl';
|
||||||
|
import IAICanvasMaskVisibilityControl from './IAICanvasMaskControls/IAICanvasMaskVisibilityControl';
|
||||||
|
import IAICanvasMaskColorPicker from './IAICanvasMaskControls/IAICanvasMaskColorPicker';
|
||||||
|
|
||||||
|
export default function IAICanvasMaskControl() {
|
||||||
|
const [maskOptionsOpen, setMaskOptionsOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IAIPopover
|
||||||
|
trigger="hover"
|
||||||
|
onOpen={() => setMaskOptionsOpen(true)}
|
||||||
|
onClose={() => setMaskOptionsOpen(false)}
|
||||||
|
triggerComponent={
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Mask Options"
|
||||||
|
tooltip="Mask Options"
|
||||||
|
icon={<FaMask />}
|
||||||
|
cursor={'pointer'}
|
||||||
|
data-selected={maskOptionsOpen}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="inpainting-button-dropdown">
|
||||||
|
<IAICanvasMaskVisibilityControl />
|
||||||
|
<IAICanvasMaskInvertControl />
|
||||||
|
<IAICanvasMaskColorPicker />
|
||||||
|
</div>
|
||||||
|
</IAIPopover>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
import { RgbaColor } from 'react-colorful';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import IAIColorPicker from 'common/components/IAIColorPicker';
|
||||||
|
import {
|
||||||
|
currentCanvasSelector,
|
||||||
|
GenericCanvasState,
|
||||||
|
setMaskColor,
|
||||||
|
} from 'features/canvas/canvasSlice';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[currentCanvasSelector, activeTabNameSelector],
|
||||||
|
(currentCanvas: GenericCanvasState, activeTabName) => {
|
||||||
|
const { brushColor } = currentCanvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
brushColor,
|
||||||
|
activeTabName,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function IAICanvasBrushColorPicker() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { brushColor, activeTabName } = useAppSelector(selector);
|
||||||
|
|
||||||
|
const handleChangeBrushColor = (newColor: RgbaColor) => {
|
||||||
|
dispatch(setMaskColor(newColor));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decrease brush opacity
|
||||||
|
useHotkeys(
|
||||||
|
'shift+[',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleChangeBrushColor({
|
||||||
|
...brushColor,
|
||||||
|
a: Math.max(brushColor.a - 0.05, 0),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
[activeTabName, brushColor.a]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Increase brush opacity
|
||||||
|
useHotkeys(
|
||||||
|
'shift+]',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleChangeBrushColor({
|
||||||
|
...brushColor,
|
||||||
|
a: Math.min(brushColor.a + 0.05, 1),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
[activeTabName, brushColor.a]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIColorPicker color={brushColor} onChange={handleChangeBrushColor} />
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { FaPlus } from 'react-icons/fa';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import {
|
||||||
|
clearMask,
|
||||||
|
currentCanvasSelector,
|
||||||
|
InpaintingCanvasState,
|
||||||
|
isCanvasMaskLine,
|
||||||
|
OutpaintingCanvasState,
|
||||||
|
} from 'features/canvas/canvasSlice';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { useToast } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const canvasMaskClearSelector = createSelector(
|
||||||
|
[currentCanvasSelector, activeTabNameSelector],
|
||||||
|
(currentCanvas, activeTabName) => {
|
||||||
|
const { isMaskEnabled, objects } = currentCanvas as
|
||||||
|
| InpaintingCanvasState
|
||||||
|
| OutpaintingCanvasState;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isMaskEnabled,
|
||||||
|
activeTabName,
|
||||||
|
isMaskEmpty: objects.filter(isCanvasMaskLine).length === 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function IAICanvasMaskClear() {
|
||||||
|
const { isMaskEnabled, activeTabName, isMaskEmpty } = useAppSelector(
|
||||||
|
canvasMaskClearSelector
|
||||||
|
);
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const handleClearMask = () => {
|
||||||
|
dispatch(clearMask());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear mask
|
||||||
|
useHotkeys(
|
||||||
|
'shift+c',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleClearMask();
|
||||||
|
toast({
|
||||||
|
title: 'Mask Cleared',
|
||||||
|
status: 'success',
|
||||||
|
duration: 2500,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !isMaskEmpty,
|
||||||
|
},
|
||||||
|
[activeTabName, isMaskEmpty, isMaskEnabled]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Clear Mask (Shift+C)"
|
||||||
|
tooltip="Clear Mask (Shift+C)"
|
||||||
|
icon={<FaPlus size={20} style={{ transform: 'rotate(45deg)' }} />}
|
||||||
|
onClick={handleClearMask}
|
||||||
|
isDisabled={isMaskEmpty || !isMaskEnabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,27 +1,31 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RgbaColor } from 'react-colorful';
|
import { RgbaColor } from 'react-colorful';
|
||||||
import { FaPalette } from 'react-icons/fa';
|
import { FaPalette } from 'react-icons/fa';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import IAIColorPicker from 'common/components/IAIColorPicker';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import IAIPopover from 'common/components/IAIPopover';
|
||||||
import {
|
import {
|
||||||
RootState,
|
currentCanvasSelector,
|
||||||
useAppDispatch,
|
GenericCanvasState,
|
||||||
useAppSelector,
|
setMaskColor,
|
||||||
} from '../../../../../app/store';
|
} from 'features/canvas/canvasSlice';
|
||||||
import IAIColorPicker from '../../../../../common/components/IAIColorPicker';
|
|
||||||
import IAIIconButton from '../../../../../common/components/IAIIconButton';
|
|
||||||
import IAIPopover from '../../../../../common/components/IAIPopover';
|
|
||||||
import { InpaintingState, setMaskColor } from '../../inpaintingSlice';
|
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { activeTabNameSelector } from '../../../../options/optionsSelectors';
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
const inpaintingMaskColorPickerSelector = createSelector(
|
const maskColorPickerSelector = createSelector(
|
||||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
[currentCanvasSelector, activeTabNameSelector],
|
||||||
(inpainting: InpaintingState, activeTabName) => {
|
(currentCanvas: GenericCanvasState, activeTabName) => {
|
||||||
const { shouldShowMask, maskColor } = inpainting;
|
const { isMaskEnabled, maskColor } = currentCanvas;
|
||||||
|
|
||||||
return { shouldShowMask, maskColor, activeTabName };
|
return {
|
||||||
|
isMaskEnabled,
|
||||||
|
maskColor,
|
||||||
|
activeTabName,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
memoizeOptions: {
|
memoizeOptions: {
|
||||||
@ -30,9 +34,9 @@ const inpaintingMaskColorPickerSelector = createSelector(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function InpaintingMaskColorPicker() {
|
export default function IAICanvasMaskColorPicker() {
|
||||||
const { shouldShowMask, maskColor, activeTabName } = useAppSelector(
|
const { isMaskEnabled, maskColor, activeTabName } = useAppSelector(
|
||||||
inpaintingMaskColorPickerSelector
|
maskColorPickerSelector
|
||||||
);
|
);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const handleChangeMaskColor = (newColor: RgbaColor) => {
|
const handleChangeMaskColor = (newColor: RgbaColor) => {
|
||||||
@ -51,9 +55,9 @@ export default function InpaintingMaskColorPicker() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
enabled: true,
|
||||||
},
|
},
|
||||||
[activeTabName, shouldShowMask, maskColor.a]
|
[activeTabName, isMaskEnabled, maskColor.a]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Increase mask opacity
|
// Increase mask opacity
|
||||||
@ -63,13 +67,13 @@ export default function InpaintingMaskColorPicker() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleChangeMaskColor({
|
handleChangeMaskColor({
|
||||||
...maskColor,
|
...maskColor,
|
||||||
a: Math.min(maskColor.a + 0.05, 100),
|
a: Math.min(maskColor.a + 0.05, 1),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
enabled: true,
|
||||||
},
|
},
|
||||||
[activeTabName, shouldShowMask, maskColor.a]
|
[activeTabName, isMaskEnabled, maskColor.a]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -80,7 +84,7 @@ export default function InpaintingMaskColorPicker() {
|
|||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label="Mask Color"
|
aria-label="Mask Color"
|
||||||
icon={<FaPalette />}
|
icon={<FaPalette />}
|
||||||
isDisabled={!shouldShowMask}
|
isDisabled={!isMaskEnabled}
|
||||||
cursor={'pointer'}
|
cursor={'pointer'}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
@ -1,24 +1,27 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import React from 'react';
|
|
||||||
import { MdInvertColors, MdInvertColorsOff } from 'react-icons/md';
|
import { MdInvertColors, MdInvertColorsOff } from 'react-icons/md';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import {
|
import {
|
||||||
RootState,
|
currentCanvasSelector,
|
||||||
useAppDispatch,
|
GenericCanvasState,
|
||||||
useAppSelector,
|
setShouldInvertMask,
|
||||||
} from '../../../../../app/store';
|
} from 'features/canvas/canvasSlice';
|
||||||
import IAIIconButton from '../../../../../common/components/IAIIconButton';
|
|
||||||
import { InpaintingState, setShouldInvertMask } from '../../inpaintingSlice';
|
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { activeTabNameSelector } from '../../../../options/optionsSelectors';
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
const inpaintingMaskInvertSelector = createSelector(
|
const canvasMaskInvertSelector = createSelector(
|
||||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
[currentCanvasSelector, activeTabNameSelector],
|
||||||
(inpainting: InpaintingState, activeTabName) => {
|
(currentCanvas: GenericCanvasState, activeTabName) => {
|
||||||
const { shouldShowMask, shouldInvertMask } = inpainting;
|
const { isMaskEnabled, shouldInvertMask } = currentCanvas;
|
||||||
|
|
||||||
return { shouldInvertMask, shouldShowMask, activeTabName };
|
return {
|
||||||
|
shouldInvertMask,
|
||||||
|
isMaskEnabled,
|
||||||
|
activeTabName,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
memoizeOptions: {
|
memoizeOptions: {
|
||||||
@ -27,9 +30,9 @@ const inpaintingMaskInvertSelector = createSelector(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function InpaintingMaskInvertControl() {
|
export default function IAICanvasMaskInvertControl() {
|
||||||
const { shouldInvertMask, shouldShowMask, activeTabName } = useAppSelector(
|
const { shouldInvertMask, isMaskEnabled, activeTabName } = useAppSelector(
|
||||||
inpaintingMaskInvertSelector
|
canvasMaskInvertSelector
|
||||||
);
|
);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@ -44,9 +47,9 @@ export default function InpaintingMaskInvertControl() {
|
|||||||
handleToggleShouldInvertMask();
|
handleToggleShouldInvertMask();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
enabled: true,
|
||||||
},
|
},
|
||||||
[activeTabName, shouldInvertMask, shouldShowMask]
|
[activeTabName, shouldInvertMask, isMaskEnabled]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -62,7 +65,7 @@ export default function InpaintingMaskInvertControl() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
onClick={handleToggleShouldInvertMask}
|
onClick={handleToggleShouldInvertMask}
|
||||||
isDisabled={!shouldShowMask}
|
isDisabled={!isMaskEnabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { BiHide, BiShow } from 'react-icons/bi';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import {
|
||||||
|
currentCanvasSelector,
|
||||||
|
GenericCanvasState,
|
||||||
|
setIsMaskEnabled,
|
||||||
|
} from 'features/canvas/canvasSlice';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
const canvasMaskVisibilitySelector = createSelector(
|
||||||
|
[currentCanvasSelector, activeTabNameSelector],
|
||||||
|
(currentCanvas: GenericCanvasState, activeTabName) => {
|
||||||
|
const { isMaskEnabled } = currentCanvas;
|
||||||
|
|
||||||
|
return { isMaskEnabled, activeTabName };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function IAICanvasMaskVisibilityControl() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const { isMaskEnabled, activeTabName } = useAppSelector(
|
||||||
|
canvasMaskVisibilitySelector
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleShouldShowMask = () =>
|
||||||
|
dispatch(setIsMaskEnabled(!isMaskEnabled));
|
||||||
|
// Hotkeys
|
||||||
|
// Show/hide mask
|
||||||
|
useHotkeys(
|
||||||
|
'h',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggleShouldShowMask();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: activeTabName === 'inpainting' || activeTabName == 'outpainting',
|
||||||
|
},
|
||||||
|
[activeTabName, isMaskEnabled]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Hide Mask (H)"
|
||||||
|
tooltip="Hide Mask (H)"
|
||||||
|
data-alert={!isMaskEnabled}
|
||||||
|
icon={isMaskEnabled ? <BiShow size={22} /> : <BiHide size={22} />}
|
||||||
|
onClick={handleToggleShouldShowMask}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { FaRedo } from 'react-icons/fa';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import { currentCanvasSelector, redo } from 'features/canvas/canvasSlice';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
const canvasRedoSelector = createSelector(
|
||||||
|
[currentCanvasSelector, activeTabNameSelector],
|
||||||
|
(currentCanvas, activeTabName) => {
|
||||||
|
const { futureObjects } = currentCanvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
canRedo: futureObjects.length > 0,
|
||||||
|
activeTabName,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function IAICanvasRedoButton() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { canRedo, activeTabName } = useAppSelector(canvasRedoSelector);
|
||||||
|
|
||||||
|
const handleRedo = () => {
|
||||||
|
dispatch(redo());
|
||||||
|
};
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['meta+shift+z', 'control+shift+z', 'control+y', 'meta+y'],
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleRedo();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: canRedo,
|
||||||
|
},
|
||||||
|
[activeTabName, canRedo]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Redo"
|
||||||
|
tooltip="Redo"
|
||||||
|
icon={<FaRedo />}
|
||||||
|
onClick={handleRedo}
|
||||||
|
isDisabled={!canRedo}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import { FaVectorSquare } from 'react-icons/fa';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import {
|
||||||
|
currentCanvasSelector,
|
||||||
|
GenericCanvasState,
|
||||||
|
setShouldShowBoundingBox,
|
||||||
|
} from 'features/canvas/canvasSlice';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
const canvasShowHideBoundingBoxControlSelector = createSelector(
|
||||||
|
currentCanvasSelector,
|
||||||
|
(currentCanvas: GenericCanvasState) => {
|
||||||
|
const { shouldShowBoundingBox } = currentCanvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
shouldShowBoundingBox,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const IAICanvasShowHideBoundingBoxControl = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { shouldShowBoundingBox } = useAppSelector(
|
||||||
|
canvasShowHideBoundingBoxControlSelector
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Hide Inpainting Box (Shift+H)"
|
||||||
|
tooltip="Hide Inpainting Box (Shift+H)"
|
||||||
|
icon={<FaVectorSquare />}
|
||||||
|
data-alert={!shouldShowBoundingBox}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(setShouldShowBoundingBox(!shouldShowBoundingBox));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasShowHideBoundingBoxControl;
|
@ -1,16 +1,11 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { VscSplitHorizontal } from 'react-icons/vsc';
|
import { VscSplitHorizontal } from 'react-icons/vsc';
|
||||||
import {
|
import { RootState, useAppDispatch, useAppSelector } from 'app/store';
|
||||||
RootState,
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
useAppDispatch,
|
import { setShowDualDisplay } from 'features/options/optionsSlice';
|
||||||
useAppSelector,
|
import { setDoesCanvasNeedScaling } from 'features/canvas/canvasSlice';
|
||||||
} from '../../../../app/store';
|
|
||||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
|
||||||
import { setShowDualDisplay } from '../../../options/optionsSlice';
|
|
||||||
import { setNeedsCache } from '../inpaintingSlice';
|
|
||||||
|
|
||||||
export default function InpaintingSplitLayoutControl() {
|
export default function IAICanvasSplitLayoutControl() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const showDualDisplay = useAppSelector(
|
const showDualDisplay = useAppSelector(
|
||||||
(state: RootState) => state.options.showDualDisplay
|
(state: RootState) => state.options.showDualDisplay
|
||||||
@ -18,7 +13,7 @@ export default function InpaintingSplitLayoutControl() {
|
|||||||
|
|
||||||
const handleDualDisplay = () => {
|
const handleDualDisplay = () => {
|
||||||
dispatch(setShowDualDisplay(!showDualDisplay));
|
dispatch(setShowDualDisplay(!showDualDisplay));
|
||||||
dispatch(setNeedsCache(true));
|
dispatch(setDoesCanvasNeedScaling(true));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hotkeys
|
// Hotkeys
|
@ -0,0 +1,58 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { FaUndo } from 'react-icons/fa';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { currentCanvasSelector, undo } from 'features/canvas/canvasSlice';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
|
||||||
|
const canvasUndoSelector = createSelector(
|
||||||
|
[currentCanvasSelector, activeTabNameSelector],
|
||||||
|
(canvas, activeTabName) => {
|
||||||
|
const { pastObjects } = canvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
canUndo: pastObjects.length > 0,
|
||||||
|
activeTabName,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function IAICanvasUndoButton() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const { canUndo, activeTabName } = useAppSelector(canvasUndoSelector);
|
||||||
|
|
||||||
|
const handleUndo = () => {
|
||||||
|
dispatch(undo());
|
||||||
|
};
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['meta+z', 'control+z'],
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleUndo();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: canUndo,
|
||||||
|
},
|
||||||
|
[activeTabName, canUndo]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Undo"
|
||||||
|
tooltip="Undo"
|
||||||
|
icon={<FaUndo />}
|
||||||
|
onClick={handleUndo}
|
||||||
|
isDisabled={!canUndo}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { currentCanvasSelector, setEraserSize, setTool } from './canvasSlice';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { FaEraser } from 'react-icons/fa';
|
||||||
|
import IAIPopover from 'common/components/IAIPopover';
|
||||||
|
import IAISlider from 'common/components/IAISlider';
|
||||||
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
|
export const selector = createSelector(
|
||||||
|
[currentCanvasSelector],
|
||||||
|
(currentCanvas) => {
|
||||||
|
const { eraserSize, tool } = currentCanvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
tool,
|
||||||
|
eraserSize,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const IAICanvasEraserButtonPopover = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { tool, eraserSize } = useAppSelector(selector);
|
||||||
|
|
||||||
|
const handleSelectEraserTool = () => dispatch(setTool('eraser'));
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'e',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelectEraserTool();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
[tool]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIPopover
|
||||||
|
trigger="hover"
|
||||||
|
triggerComponent={
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Eraser (E)"
|
||||||
|
tooltip="Eraser (E)"
|
||||||
|
icon={<FaEraser />}
|
||||||
|
data-selected={tool === 'eraser'}
|
||||||
|
onClick={() => dispatch(setTool('eraser'))}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Flex minWidth={'15rem'} direction={'column'} gap={'1rem'}>
|
||||||
|
<IAISlider
|
||||||
|
label="Size"
|
||||||
|
value={eraserSize}
|
||||||
|
withInput
|
||||||
|
onChange={(newSize) => dispatch(setEraserSize(newSize))}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</IAIPopover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasEraserButtonPopover;
|
49
frontend/src/features/canvas/IAICanvasEraserLines.tsx
Normal file
49
frontend/src/features/canvas/IAICanvasEraserLines.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// import { GroupConfig } from 'konva/lib/Group';
|
||||||
|
// import { Group, Line } from 'react-konva';
|
||||||
|
// import { RootState, useAppSelector } from 'app/store';
|
||||||
|
// import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
// import { OutpaintingCanvasState } from './canvasSlice';
|
||||||
|
|
||||||
|
// export const canvasEraserLinesSelector = createSelector(
|
||||||
|
// (state: RootState) => state.canvas.outpainting,
|
||||||
|
// (outpainting: OutpaintingCanvasState) => {
|
||||||
|
// const { eraserLines } = outpainting;
|
||||||
|
// return {
|
||||||
|
// eraserLines,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// type IAICanvasEraserLinesProps = GroupConfig;
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Draws the lines which comprise the mask.
|
||||||
|
// *
|
||||||
|
// * Uses globalCompositeOperation to handle the brush and eraser tools.
|
||||||
|
// */
|
||||||
|
// const IAICanvasEraserLines = (props: IAICanvasEraserLinesProps) => {
|
||||||
|
// const { ...rest } = props;
|
||||||
|
// const { eraserLines } = useAppSelector(canvasEraserLinesSelector);
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <Group {...rest} globalCompositeOperation={'destination-out'}>
|
||||||
|
// {eraserLines.map((line, i) => (
|
||||||
|
// <Line
|
||||||
|
// key={i}
|
||||||
|
// points={line.points}
|
||||||
|
// stroke={'rgb(0,0,0)'} // The lines can be any color, just need alpha > 0
|
||||||
|
// strokeWidth={line.strokeWidth * 2}
|
||||||
|
// tension={0}
|
||||||
|
// lineCap="round"
|
||||||
|
// lineJoin="round"
|
||||||
|
// shadowForStrokeEnabled={false}
|
||||||
|
// listening={false}
|
||||||
|
// globalCompositeOperation={'source-over'}
|
||||||
|
// />
|
||||||
|
// ))}
|
||||||
|
// </Group>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export default IAICanvasEraserLines;
|
||||||
|
export default {}
|
88
frontend/src/features/canvas/IAICanvasGrid.tsx
Normal file
88
frontend/src/features/canvas/IAICanvasGrid.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// Grid drawing adapted from https://longviewcoder.com/2021/12/08/konva-a-better-grid/
|
||||||
|
|
||||||
|
import { useColorMode } from '@chakra-ui/react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { Group, Line as KonvaLine } from 'react-konva';
|
||||||
|
import useUnscaleCanvasValue from './hooks/useUnscaleCanvasValue';
|
||||||
|
import { stageRef } from './IAICanvas';
|
||||||
|
|
||||||
|
const IAICanvasGrid = () => {
|
||||||
|
const { colorMode } = useColorMode();
|
||||||
|
const unscale = useUnscaleCanvasValue();
|
||||||
|
|
||||||
|
if (!stageRef.current) return null;
|
||||||
|
const gridLineColor =
|
||||||
|
colorMode === 'light' ? 'rgba(0,0,0,0.3)' : 'rgba(255,255,255,0.3)';
|
||||||
|
|
||||||
|
const stage = stageRef.current;
|
||||||
|
const width = stage.width();
|
||||||
|
const height = stage.height();
|
||||||
|
const x = stage.x();
|
||||||
|
const y = stage.y();
|
||||||
|
|
||||||
|
const stageRect = {
|
||||||
|
x1: 0,
|
||||||
|
y1: 0,
|
||||||
|
x2: width,
|
||||||
|
y2: height,
|
||||||
|
offset: {
|
||||||
|
x: unscale(x),
|
||||||
|
y: unscale(y),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const gridOffset = {
|
||||||
|
x: Math.ceil(unscale(x) / 64) * 64,
|
||||||
|
y: Math.ceil(unscale(y) / 64) * 64,
|
||||||
|
};
|
||||||
|
|
||||||
|
const gridRect = {
|
||||||
|
x1: -gridOffset.x,
|
||||||
|
y1: -gridOffset.y,
|
||||||
|
x2: unscale(width) - gridOffset.x + 64,
|
||||||
|
y2: unscale(height) - gridOffset.y + 64,
|
||||||
|
};
|
||||||
|
|
||||||
|
const gridFullRect = {
|
||||||
|
x1: Math.min(stageRect.x1, gridRect.x1),
|
||||||
|
y1: Math.min(stageRect.y1, gridRect.y1),
|
||||||
|
x2: Math.max(stageRect.x2, gridRect.x2),
|
||||||
|
y2: Math.max(stageRect.y2, gridRect.y2),
|
||||||
|
};
|
||||||
|
|
||||||
|
const fullRect = gridFullRect;
|
||||||
|
|
||||||
|
const // find the x & y size of the grid
|
||||||
|
xSize = fullRect.x2 - fullRect.x1,
|
||||||
|
ySize = fullRect.y2 - fullRect.y1,
|
||||||
|
// compute the number of steps required on each axis.
|
||||||
|
xSteps = Math.round(xSize / 64) + 1,
|
||||||
|
ySteps = Math.round(ySize / 64) + 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group>
|
||||||
|
{_.range(0, xSteps).map((i) => (
|
||||||
|
<KonvaLine
|
||||||
|
key={`x_${i}`}
|
||||||
|
x={fullRect.x1 + i * 64}
|
||||||
|
y={fullRect.y1}
|
||||||
|
points={[0, 0, 0, ySize]}
|
||||||
|
stroke={gridLineColor}
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{_.range(0, ySteps).map((i) => (
|
||||||
|
<KonvaLine
|
||||||
|
key={`y_${i}`}
|
||||||
|
x={fullRect.x1}
|
||||||
|
y={fullRect.y1 + i * 64}
|
||||||
|
points={[0, 0, xSize, 0]}
|
||||||
|
stroke={gridLineColor}
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasGrid;
|
15
frontend/src/features/canvas/IAICanvasImage.tsx
Normal file
15
frontend/src/features/canvas/IAICanvasImage.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Image } from 'react-konva';
|
||||||
|
import useImage from 'use-image';
|
||||||
|
|
||||||
|
type IAICanvasImageProps = {
|
||||||
|
url: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
const IAICanvasImage = (props: IAICanvasImageProps) => {
|
||||||
|
const { url, x, y } = props;
|
||||||
|
const [image] = useImage(url);
|
||||||
|
return <Image x={x} y={y} image={image} listening={false} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasImage;
|
59
frontend/src/features/canvas/IAICanvasIntermediateImage.tsx
Normal file
59
frontend/src/features/canvas/IAICanvasIntermediateImage.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { RootState, useAppSelector } from 'app/store';
|
||||||
|
import { GalleryState } from 'features/gallery/gallerySlice';
|
||||||
|
import { ImageConfig } from 'konva/lib/shapes/Image';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Image as KonvaImage } from 'react-konva';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[(state: RootState) => state.gallery],
|
||||||
|
(gallery: GalleryState) => {
|
||||||
|
return gallery.intermediateImage ? gallery.intermediateImage : null;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type Props = Omit<ImageConfig, 'image'>;
|
||||||
|
|
||||||
|
const IAICanvasIntermediateImage = (props: Props) => {
|
||||||
|
const { ...rest } = props;
|
||||||
|
const intermediateImage = useAppSelector(selector);
|
||||||
|
|
||||||
|
const [loadedImageElement, setLoadedImageElement] =
|
||||||
|
useState<HTMLImageElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!intermediateImage) return;
|
||||||
|
const tempImage = new Image();
|
||||||
|
|
||||||
|
tempImage.onload = () => {
|
||||||
|
setLoadedImageElement(tempImage);
|
||||||
|
};
|
||||||
|
tempImage.src = intermediateImage.url;
|
||||||
|
}, [intermediateImage]);
|
||||||
|
|
||||||
|
if (!intermediateImage?.boundingBox) return null;
|
||||||
|
|
||||||
|
const {
|
||||||
|
boundingBox: { x, y, width, height },
|
||||||
|
} = intermediateImage;
|
||||||
|
|
||||||
|
return loadedImageElement ? (
|
||||||
|
<KonvaImage
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
image={loadedImageElement}
|
||||||
|
listening={false}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasIntermediateImage;
|
69
frontend/src/features/canvas/IAICanvasMaskButtonPopover.tsx
Normal file
69
frontend/src/features/canvas/IAICanvasMaskButtonPopover.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { Button, Flex } from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import {
|
||||||
|
clearMask,
|
||||||
|
currentCanvasSelector,
|
||||||
|
setIsMaskEnabled,
|
||||||
|
setLayer,
|
||||||
|
setMaskColor,
|
||||||
|
} from './canvasSlice';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { FaMask } from 'react-icons/fa';
|
||||||
|
import IAIPopover from 'common/components/IAIPopover';
|
||||||
|
import IAICheckbox from 'common/components/IAICheckbox';
|
||||||
|
import IAIColorPicker from 'common/components/IAIColorPicker';
|
||||||
|
|
||||||
|
export const selector = createSelector(
|
||||||
|
[currentCanvasSelector],
|
||||||
|
(currentCanvas) => {
|
||||||
|
const { maskColor, layer, isMaskEnabled } = currentCanvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
layer,
|
||||||
|
maskColor,
|
||||||
|
isMaskEnabled,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const IAICanvasMaskButtonPopover = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { layer, maskColor, isMaskEnabled } = useAppSelector(selector);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIPopover
|
||||||
|
trigger="hover"
|
||||||
|
triggerComponent={
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Select Mask Layer"
|
||||||
|
tooltip="Select Mask Layer"
|
||||||
|
data-alert={layer === 'mask'}
|
||||||
|
onClick={() => dispatch(setLayer(layer === 'mask' ? 'base' : 'mask'))}
|
||||||
|
icon={<FaMask />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Flex direction={'column'} gap={'0.5rem'}>
|
||||||
|
<Button onClick={() => dispatch(clearMask())}>Clear Mask</Button>
|
||||||
|
<IAICheckbox
|
||||||
|
label="Enable Mask"
|
||||||
|
isChecked={isMaskEnabled}
|
||||||
|
onChange={(e) => dispatch(setIsMaskEnabled(e.target.checked))}
|
||||||
|
/>
|
||||||
|
<IAICheckbox label="Invert Mask" />
|
||||||
|
<IAIColorPicker
|
||||||
|
color={maskColor}
|
||||||
|
onChange={(newColor) => dispatch(setMaskColor(newColor))}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</IAIPopover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasMaskButtonPopover;
|
49
frontend/src/features/canvas/IAICanvasMaskCompositer.tsx
Normal file
49
frontend/src/features/canvas/IAICanvasMaskCompositer.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store';
|
||||||
|
import { RectConfig } from 'konva/lib/shapes/Rect';
|
||||||
|
import { Rect } from 'react-konva';
|
||||||
|
import {
|
||||||
|
currentCanvasSelector,
|
||||||
|
InpaintingCanvasState,
|
||||||
|
OutpaintingCanvasState,
|
||||||
|
} from './canvasSlice';
|
||||||
|
import { rgbaColorToString } from './util/colorToString';
|
||||||
|
|
||||||
|
export const canvasMaskCompositerSelector = createSelector(
|
||||||
|
currentCanvasSelector,
|
||||||
|
(currentCanvas) => {
|
||||||
|
const { maskColor, stageCoordinates, stageDimensions, stageScale } =
|
||||||
|
currentCanvas as InpaintingCanvasState | OutpaintingCanvasState;
|
||||||
|
|
||||||
|
return {
|
||||||
|
stageCoordinates,
|
||||||
|
stageDimensions,
|
||||||
|
stageScale,
|
||||||
|
maskColorString: rgbaColorToString(maskColor),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type IAICanvasMaskCompositerProps = RectConfig;
|
||||||
|
|
||||||
|
const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => {
|
||||||
|
const { ...rest } = props;
|
||||||
|
|
||||||
|
const { maskColorString, stageCoordinates, stageDimensions, stageScale } =
|
||||||
|
useAppSelector(canvasMaskCompositerSelector);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Rect
|
||||||
|
offsetX={stageCoordinates.x / stageScale}
|
||||||
|
offsetY={stageCoordinates.y / stageScale}
|
||||||
|
height={stageDimensions.height / stageScale}
|
||||||
|
width={stageDimensions.width / stageScale}
|
||||||
|
fill={maskColorString}
|
||||||
|
globalCompositeOperation={'source-in'}
|
||||||
|
listening={false}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasMaskCompositer;
|
64
frontend/src/features/canvas/IAICanvasMaskLines.tsx
Normal file
64
frontend/src/features/canvas/IAICanvasMaskLines.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { GroupConfig } from 'konva/lib/Group';
|
||||||
|
import { Group, Line } from 'react-konva';
|
||||||
|
import { useAppSelector } from 'app/store';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import {
|
||||||
|
currentCanvasSelector,
|
||||||
|
GenericCanvasState,
|
||||||
|
InpaintingCanvasState,
|
||||||
|
isCanvasMaskLine,
|
||||||
|
OutpaintingCanvasState,
|
||||||
|
} from './canvasSlice';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export const canvasLinesSelector = createSelector(
|
||||||
|
currentCanvasSelector,
|
||||||
|
(currentCanvas: GenericCanvasState) => {
|
||||||
|
const { objects } = currentCanvas as
|
||||||
|
| InpaintingCanvasState
|
||||||
|
| OutpaintingCanvasState;
|
||||||
|
return {
|
||||||
|
objects,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type InpaintingCanvasLinesProps = GroupConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the lines which comprise the mask.
|
||||||
|
*
|
||||||
|
* Uses globalCompositeOperation to handle the brush and eraser tools.
|
||||||
|
*/
|
||||||
|
const IAICanvasLines = (props: InpaintingCanvasLinesProps) => {
|
||||||
|
const { ...rest } = props;
|
||||||
|
const { objects } = useAppSelector(canvasLinesSelector);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group listening={false} {...rest}>
|
||||||
|
{objects.filter(isCanvasMaskLine).map((line, i) => (
|
||||||
|
<Line
|
||||||
|
key={i}
|
||||||
|
points={line.points}
|
||||||
|
stroke={'rgb(0,0,0)'} // The lines can be any color, just need alpha > 0
|
||||||
|
strokeWidth={line.strokeWidth * 2}
|
||||||
|
tension={0}
|
||||||
|
lineCap="round"
|
||||||
|
lineJoin="round"
|
||||||
|
shadowForStrokeEnabled={false}
|
||||||
|
listening={false}
|
||||||
|
globalCompositeOperation={
|
||||||
|
line.tool === 'brush' ? 'source-over' : 'destination-out'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasLines;
|
112
frontend/src/features/canvas/IAICanvasOutpaintingControls.tsx
Normal file
112
frontend/src/features/canvas/IAICanvasOutpaintingControls.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { ButtonGroup } from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import {
|
||||||
|
currentCanvasSelector,
|
||||||
|
resetCanvas,
|
||||||
|
setTool,
|
||||||
|
uploadOutpaintingMergedImage,
|
||||||
|
} from './canvasSlice';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { canvasImageLayerRef } from './IAICanvas';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import {
|
||||||
|
FaArrowsAlt,
|
||||||
|
FaCopy,
|
||||||
|
FaDownload,
|
||||||
|
FaLayerGroup,
|
||||||
|
FaSave,
|
||||||
|
FaTrash,
|
||||||
|
FaUpload,
|
||||||
|
} from 'react-icons/fa';
|
||||||
|
import IAICanvasUndoButton from './IAICanvasControls/IAICanvasUndoButton';
|
||||||
|
import IAICanvasRedoButton from './IAICanvasControls/IAICanvasRedoButton';
|
||||||
|
import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover';
|
||||||
|
import IAICanvasEraserButtonPopover from './IAICanvasEraserButtonPopover';
|
||||||
|
import IAICanvasBrushButtonPopover from './IAICanvasBrushButtonPopover';
|
||||||
|
import IAICanvasMaskButtonPopover from './IAICanvasMaskButtonPopover';
|
||||||
|
|
||||||
|
export const canvasControlsSelector = createSelector(
|
||||||
|
[currentCanvasSelector],
|
||||||
|
(currentCanvas) => {
|
||||||
|
const { tool } = currentCanvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
tool,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const IAICanvasOutpaintingControls = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { tool } = useAppSelector(canvasControlsSelector);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inpainting-settings">
|
||||||
|
<IAICanvasMaskButtonPopover />
|
||||||
|
<ButtonGroup isAttached>
|
||||||
|
<IAICanvasBrushButtonPopover />
|
||||||
|
<IAICanvasEraserButtonPopover />
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Move (M)"
|
||||||
|
tooltip="Move (M)"
|
||||||
|
icon={<FaArrowsAlt />}
|
||||||
|
data-selected={tool === 'move'}
|
||||||
|
onClick={() => dispatch(setTool('move'))}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
|
<ButtonGroup isAttached>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Merge Visible"
|
||||||
|
tooltip="Merge Visible"
|
||||||
|
icon={<FaLayerGroup />}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(uploadOutpaintingMergedImage(canvasImageLayerRef));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Save Selection to Gallery"
|
||||||
|
tooltip="Save Selection to Gallery"
|
||||||
|
icon={<FaSave />}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Copy Selection"
|
||||||
|
tooltip="Copy Selection"
|
||||||
|
icon={<FaCopy />}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Download Selection"
|
||||||
|
tooltip="Download Selection"
|
||||||
|
icon={<FaDownload />}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
|
<ButtonGroup isAttached>
|
||||||
|
<IAICanvasUndoButton />
|
||||||
|
<IAICanvasRedoButton />
|
||||||
|
</ButtonGroup>
|
||||||
|
<ButtonGroup isAttached>
|
||||||
|
<IAICanvasSettingsButtonPopover />
|
||||||
|
</ButtonGroup>
|
||||||
|
<ButtonGroup isAttached>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Upload"
|
||||||
|
tooltip="Upload"
|
||||||
|
icon={<FaUpload />}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Reset Canvas"
|
||||||
|
tooltip="Reset Canvas"
|
||||||
|
icon={<FaTrash />}
|
||||||
|
onClick={() => dispatch(resetCanvas())}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasOutpaintingControls;
|
65
frontend/src/features/canvas/IAICanvasOutpaintingObjects.tsx
Normal file
65
frontend/src/features/canvas/IAICanvasOutpaintingObjects.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { RootState, useAppSelector } from 'app/store';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { Group, Line } from 'react-konva';
|
||||||
|
import {
|
||||||
|
CanvasState,
|
||||||
|
isCanvasBaseImage,
|
||||||
|
isCanvasBaseLine,
|
||||||
|
} from './canvasSlice';
|
||||||
|
import IAICanvasImage from './IAICanvasImage';
|
||||||
|
import { rgbaColorToString } from './util/colorToString';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[(state: RootState) => state.canvas],
|
||||||
|
(canvas: CanvasState) => {
|
||||||
|
return {
|
||||||
|
objects:
|
||||||
|
canvas.currentCanvas === 'outpainting'
|
||||||
|
? canvas.outpainting.objects
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const IAICanvasOutpaintingObjects = () => {
|
||||||
|
const { objects } = useAppSelector(selector);
|
||||||
|
|
||||||
|
if (!objects) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group name="outpainting-objects" listening={false}>
|
||||||
|
{objects.map((obj, i) => {
|
||||||
|
if (isCanvasBaseImage(obj)) {
|
||||||
|
return (
|
||||||
|
<IAICanvasImage key={i} x={obj.x} y={obj.y} url={obj.image.url} />
|
||||||
|
);
|
||||||
|
} else if (isCanvasBaseLine(obj)) {
|
||||||
|
return (
|
||||||
|
<Line
|
||||||
|
key={i}
|
||||||
|
points={obj.points}
|
||||||
|
stroke={obj.color ? rgbaColorToString(obj.color) : 'rgb(0,0,0)'} // The lines can be any color, just need alpha > 0
|
||||||
|
strokeWidth={obj.strokeWidth * 2}
|
||||||
|
tension={0}
|
||||||
|
lineCap="round"
|
||||||
|
lineJoin="round"
|
||||||
|
shadowForStrokeEnabled={false}
|
||||||
|
listening={false}
|
||||||
|
globalCompositeOperation={
|
||||||
|
obj.tool === 'brush' ? 'source-over' : 'destination-out'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasOutpaintingObjects;
|
78
frontend/src/features/canvas/IAICanvasResizer.tsx
Normal file
78
frontend/src/features/canvas/IAICanvasResizer.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { Spinner } from '@chakra-ui/react';
|
||||||
|
import { useLayoutEffect, useRef } from 'react';
|
||||||
|
import { RootState, useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import {
|
||||||
|
baseCanvasImageSelector,
|
||||||
|
CanvasState,
|
||||||
|
currentCanvasSelector,
|
||||||
|
GenericCanvasState,
|
||||||
|
setStageDimensions,
|
||||||
|
setStageScale,
|
||||||
|
} from 'features/canvas/canvasSlice';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
import { first } from 'lodash';
|
||||||
|
|
||||||
|
const canvasResizerSelector = createSelector(
|
||||||
|
(state: RootState) => state.canvas,
|
||||||
|
baseCanvasImageSelector,
|
||||||
|
activeTabNameSelector,
|
||||||
|
(canvas: CanvasState, baseCanvasImage, activeTabName) => {
|
||||||
|
const { doesCanvasNeedScaling } = canvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
doesCanvasNeedScaling,
|
||||||
|
activeTabName,
|
||||||
|
baseCanvasImage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const IAICanvasResizer = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { doesCanvasNeedScaling, activeTabName, baseCanvasImage } =
|
||||||
|
useAppSelector(canvasResizerSelector);
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (!ref.current || !baseCanvasImage) return;
|
||||||
|
|
||||||
|
const width = ref.current.clientWidth;
|
||||||
|
const height = ref.current.clientHeight;
|
||||||
|
|
||||||
|
const scale = Math.min(
|
||||||
|
1,
|
||||||
|
Math.min(width / baseCanvasImage.width, height / baseCanvasImage.height)
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(setStageScale(scale));
|
||||||
|
|
||||||
|
if (activeTabName === 'inpainting') {
|
||||||
|
dispatch(
|
||||||
|
setStageDimensions({
|
||||||
|
width: Math.floor(baseCanvasImage.width * scale),
|
||||||
|
height: Math.floor(baseCanvasImage.height * scale),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (activeTabName === 'outpainting') {
|
||||||
|
dispatch(
|
||||||
|
setStageDimensions({
|
||||||
|
width: Math.floor(width),
|
||||||
|
height: Math.floor(height),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}, [dispatch, baseCanvasImage, doesCanvasNeedScaling, activeTabName]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className="inpainting-canvas-area">
|
||||||
|
<Spinner thickness="2px" speed="1s" size="xl" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasResizer;
|
101
frontend/src/features/canvas/IAICanvasSettingsButtonPopover.tsx
Normal file
101
frontend/src/features/canvas/IAICanvasSettingsButtonPopover.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import {
|
||||||
|
currentCanvasSelector,
|
||||||
|
outpaintingCanvasSelector,
|
||||||
|
setShouldAutoSave,
|
||||||
|
setShouldDarkenOutsideBoundingBox,
|
||||||
|
setShouldShowGrid,
|
||||||
|
setShouldShowIntermediates,
|
||||||
|
setShouldSnapToGrid,
|
||||||
|
} from './canvasSlice';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { FaWrench } from 'react-icons/fa';
|
||||||
|
import IAIPopover from 'common/components/IAIPopover';
|
||||||
|
import IAICheckbox from 'common/components/IAICheckbox';
|
||||||
|
|
||||||
|
export const canvasControlsSelector = createSelector(
|
||||||
|
[currentCanvasSelector, outpaintingCanvasSelector],
|
||||||
|
(currentCanvas, outpaintingCanvas) => {
|
||||||
|
const { shouldDarkenOutsideBoundingBox, shouldShowIntermediates } =
|
||||||
|
currentCanvas;
|
||||||
|
|
||||||
|
const { shouldShowGrid, shouldSnapToGrid, shouldAutoSave } =
|
||||||
|
outpaintingCanvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
shouldShowGrid,
|
||||||
|
shouldSnapToGrid,
|
||||||
|
shouldAutoSave,
|
||||||
|
shouldDarkenOutsideBoundingBox,
|
||||||
|
shouldShowIntermediates,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const IAICanvasSettingsButtonPopover = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const {
|
||||||
|
shouldShowIntermediates,
|
||||||
|
shouldShowGrid,
|
||||||
|
shouldSnapToGrid,
|
||||||
|
shouldAutoSave,
|
||||||
|
shouldDarkenOutsideBoundingBox,
|
||||||
|
} = useAppSelector(canvasControlsSelector);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIPopover
|
||||||
|
trigger="hover"
|
||||||
|
triggerComponent={
|
||||||
|
<IAIIconButton
|
||||||
|
variant="link"
|
||||||
|
data-variant="link"
|
||||||
|
tooltip="Canvas Settings"
|
||||||
|
aria-label="Canvas Settings"
|
||||||
|
icon={<FaWrench />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Flex direction={'column'} gap={'0.5rem'}>
|
||||||
|
<IAICheckbox
|
||||||
|
label="Show Intermediates"
|
||||||
|
isChecked={shouldShowIntermediates}
|
||||||
|
onChange={(e) =>
|
||||||
|
dispatch(setShouldShowIntermediates(e.target.checked))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<IAICheckbox
|
||||||
|
label="Show Grid"
|
||||||
|
isChecked={shouldShowGrid}
|
||||||
|
onChange={(e) => dispatch(setShouldShowGrid(e.target.checked))}
|
||||||
|
/>
|
||||||
|
<IAICheckbox
|
||||||
|
label="Snap to Grid"
|
||||||
|
isChecked={shouldSnapToGrid}
|
||||||
|
onChange={(e) => dispatch(setShouldSnapToGrid(e.target.checked))}
|
||||||
|
/>
|
||||||
|
<IAICheckbox
|
||||||
|
label="Darken Outside Selection"
|
||||||
|
isChecked={shouldDarkenOutsideBoundingBox}
|
||||||
|
onChange={(e) =>
|
||||||
|
dispatch(setShouldDarkenOutsideBoundingBox(e.target.checked))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<IAICheckbox
|
||||||
|
label="Auto Save to Gallery"
|
||||||
|
isChecked={shouldAutoSave}
|
||||||
|
onChange={(e) => dispatch(setShouldAutoSave(e.target.checked))}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</IAIPopover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasSettingsButtonPopover;
|
74
frontend/src/features/canvas/IAICanvasStatusText.tsx
Normal file
74
frontend/src/features/canvas/IAICanvasStatusText.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { currentCanvasSelector } from './canvasSlice';
|
||||||
|
|
||||||
|
const roundToHundreth = (val: number): number => {
|
||||||
|
return Math.round(val * 100) / 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[currentCanvasSelector],
|
||||||
|
(currentCanvas) => {
|
||||||
|
const {
|
||||||
|
stageDimensions: { width: stageWidth, height: stageHeight },
|
||||||
|
stageCoordinates: { x: stageX, y: stageY },
|
||||||
|
boundingBoxDimensions: { width: boxWidth, height: boxHeight },
|
||||||
|
boundingBoxCoordinates: { x: boxX, y: boxY },
|
||||||
|
cursorPosition,
|
||||||
|
stageScale,
|
||||||
|
} = currentCanvas;
|
||||||
|
|
||||||
|
const position = cursorPosition
|
||||||
|
? { cursorX: cursorPosition.x, cursorY: cursorPosition.y }
|
||||||
|
: { cursorX: -1, cursorY: -1 };
|
||||||
|
|
||||||
|
return {
|
||||||
|
stageWidth,
|
||||||
|
stageHeight,
|
||||||
|
stageX,
|
||||||
|
stageY,
|
||||||
|
boxWidth,
|
||||||
|
boxHeight,
|
||||||
|
boxX,
|
||||||
|
boxY,
|
||||||
|
stageScale,
|
||||||
|
...position,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const IAICanvasStatusText = () => {
|
||||||
|
const {
|
||||||
|
stageWidth,
|
||||||
|
stageHeight,
|
||||||
|
stageX,
|
||||||
|
stageY,
|
||||||
|
boxWidth,
|
||||||
|
boxHeight,
|
||||||
|
boxX,
|
||||||
|
boxY,
|
||||||
|
cursorX,
|
||||||
|
cursorY,
|
||||||
|
stageScale,
|
||||||
|
} = useAppSelector(selector);
|
||||||
|
return (
|
||||||
|
<div className="canvas-status-text">
|
||||||
|
<div>{`Stage: ${stageWidth} x ${stageHeight}`}</div>
|
||||||
|
<div>{`Stage: ${roundToHundreth(stageX)}, ${roundToHundreth(
|
||||||
|
stageY
|
||||||
|
)}`}</div>
|
||||||
|
<div>{`Scale: ${roundToHundreth(stageScale)}`}</div>
|
||||||
|
<div>{`Box: ${boxWidth} x ${boxHeight}`}</div>
|
||||||
|
<div>{`Box: ${roundToHundreth(boxX)}, ${roundToHundreth(boxY)}`}</div>
|
||||||
|
<div>{`Cursor: ${cursorX}, ${cursorY}`}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasStatusText;
|
764
frontend/src/features/canvas/canvasSlice.ts
Normal file
764
frontend/src/features/canvas/canvasSlice.ts
Normal file
@ -0,0 +1,764 @@
|
|||||||
|
import {
|
||||||
|
createAsyncThunk,
|
||||||
|
createSelector,
|
||||||
|
createSlice,
|
||||||
|
} from '@reduxjs/toolkit';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { IRect, Vector2d } from 'konva/lib/types';
|
||||||
|
import { RgbaColor } from 'react-colorful';
|
||||||
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
|
||||||
|
import { RootState } from 'app/store';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import { MutableRefObject } from 'react';
|
||||||
|
import Konva from 'konva';
|
||||||
|
|
||||||
|
export interface GenericCanvasState {
|
||||||
|
tool: CanvasTool;
|
||||||
|
brushSize: number;
|
||||||
|
brushColor: RgbaColor;
|
||||||
|
eraserSize: number;
|
||||||
|
maskColor: RgbaColor;
|
||||||
|
cursorPosition: Vector2d | null;
|
||||||
|
stageDimensions: Dimensions;
|
||||||
|
stageCoordinates: Vector2d;
|
||||||
|
boundingBoxDimensions: Dimensions;
|
||||||
|
boundingBoxCoordinates: Vector2d;
|
||||||
|
boundingBoxPreviewFill: RgbaColor;
|
||||||
|
shouldShowBoundingBox: boolean;
|
||||||
|
shouldDarkenOutsideBoundingBox: boolean;
|
||||||
|
isMaskEnabled: boolean;
|
||||||
|
shouldInvertMask: boolean;
|
||||||
|
shouldShowCheckboardTransparency: boolean;
|
||||||
|
shouldShowBrush: boolean;
|
||||||
|
shouldShowBrushPreview: boolean;
|
||||||
|
stageScale: number;
|
||||||
|
isDrawing: boolean;
|
||||||
|
isTransformingBoundingBox: boolean;
|
||||||
|
isMouseOverBoundingBox: boolean;
|
||||||
|
isMovingBoundingBox: boolean;
|
||||||
|
isMovingStage: boolean;
|
||||||
|
shouldUseInpaintReplace: boolean;
|
||||||
|
inpaintReplace: number;
|
||||||
|
shouldLockBoundingBox: boolean;
|
||||||
|
isMoveBoundingBoxKeyHeld: boolean;
|
||||||
|
isMoveStageKeyHeld: boolean;
|
||||||
|
intermediateImage?: InvokeAI.Image;
|
||||||
|
shouldShowIntermediates: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CanvasLayer = 'base' | 'mask';
|
||||||
|
|
||||||
|
export type CanvasDrawingTool = 'brush' | 'eraser';
|
||||||
|
|
||||||
|
export type CanvasTool = CanvasDrawingTool | 'move';
|
||||||
|
|
||||||
|
export type Dimensions = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CanvasAnyLine = {
|
||||||
|
kind: 'line';
|
||||||
|
tool: CanvasDrawingTool;
|
||||||
|
strokeWidth: number;
|
||||||
|
points: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CanvasImage = {
|
||||||
|
kind: 'image';
|
||||||
|
layer: 'base';
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
image: InvokeAI.Image;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CanvasMaskLine = CanvasAnyLine & {
|
||||||
|
layer: 'mask';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CanvasLine = CanvasAnyLine & {
|
||||||
|
layer: 'base';
|
||||||
|
color?: RgbaColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CanvasObject = CanvasImage | CanvasLine | CanvasMaskLine;
|
||||||
|
|
||||||
|
// type guards
|
||||||
|
export const isCanvasMaskLine = (obj: CanvasObject): obj is CanvasMaskLine =>
|
||||||
|
obj.kind === 'line' && obj.layer === 'mask';
|
||||||
|
|
||||||
|
export const isCanvasBaseLine = (obj: CanvasObject): obj is CanvasLine =>
|
||||||
|
obj.kind === 'line' && obj.layer === 'base';
|
||||||
|
|
||||||
|
export const isCanvasBaseImage = (obj: CanvasObject): obj is CanvasImage =>
|
||||||
|
obj.kind === 'image' && obj.layer === 'base';
|
||||||
|
|
||||||
|
export const isCanvasAnyLine = (
|
||||||
|
obj: CanvasObject
|
||||||
|
): obj is CanvasMaskLine | CanvasLine => obj.kind === 'line';
|
||||||
|
|
||||||
|
export type OutpaintingCanvasState = GenericCanvasState & {
|
||||||
|
layer: CanvasLayer;
|
||||||
|
objects: CanvasObject[];
|
||||||
|
pastObjects: CanvasObject[][];
|
||||||
|
futureObjects: CanvasObject[][];
|
||||||
|
shouldShowGrid: boolean;
|
||||||
|
shouldSnapToGrid: boolean;
|
||||||
|
shouldAutoSave: boolean;
|
||||||
|
stagingArea: {
|
||||||
|
images: CanvasImage[];
|
||||||
|
selectedImageIndex: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InpaintingCanvasState = GenericCanvasState & {
|
||||||
|
layer: 'mask';
|
||||||
|
objects: CanvasObject[];
|
||||||
|
pastObjects: CanvasObject[][];
|
||||||
|
futureObjects: CanvasObject[][];
|
||||||
|
imageToInpaint?: InvokeAI.Image;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BaseCanvasState = InpaintingCanvasState | OutpaintingCanvasState;
|
||||||
|
|
||||||
|
export type ValidCanvasName = 'inpainting' | 'outpainting';
|
||||||
|
|
||||||
|
export interface CanvasState {
|
||||||
|
doesCanvasNeedScaling: boolean;
|
||||||
|
currentCanvas: ValidCanvasName;
|
||||||
|
inpainting: InpaintingCanvasState;
|
||||||
|
outpainting: OutpaintingCanvasState;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialGenericCanvasState: GenericCanvasState = {
|
||||||
|
tool: 'brush',
|
||||||
|
brushColor: { r: 90, g: 90, b: 255, a: 1 },
|
||||||
|
brushSize: 50,
|
||||||
|
maskColor: { r: 255, g: 90, b: 90, a: 0.5 },
|
||||||
|
eraserSize: 50,
|
||||||
|
stageDimensions: { width: 0, height: 0 },
|
||||||
|
stageCoordinates: { x: 0, y: 0 },
|
||||||
|
boundingBoxDimensions: { width: 512, height: 512 },
|
||||||
|
boundingBoxCoordinates: { x: 0, y: 0 },
|
||||||
|
boundingBoxPreviewFill: { r: 0, g: 0, b: 0, a: 0.5 },
|
||||||
|
shouldShowBoundingBox: true,
|
||||||
|
shouldDarkenOutsideBoundingBox: false,
|
||||||
|
cursorPosition: null,
|
||||||
|
isMaskEnabled: true,
|
||||||
|
shouldInvertMask: false,
|
||||||
|
shouldShowCheckboardTransparency: false,
|
||||||
|
shouldShowBrush: true,
|
||||||
|
shouldShowBrushPreview: false,
|
||||||
|
isDrawing: false,
|
||||||
|
isTransformingBoundingBox: false,
|
||||||
|
isMouseOverBoundingBox: false,
|
||||||
|
isMovingBoundingBox: false,
|
||||||
|
stageScale: 1,
|
||||||
|
shouldUseInpaintReplace: false,
|
||||||
|
inpaintReplace: 0.1,
|
||||||
|
shouldLockBoundingBox: false,
|
||||||
|
isMoveBoundingBoxKeyHeld: false,
|
||||||
|
isMoveStageKeyHeld: false,
|
||||||
|
shouldShowIntermediates: true,
|
||||||
|
isMovingStage: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialCanvasState: CanvasState = {
|
||||||
|
currentCanvas: 'inpainting',
|
||||||
|
doesCanvasNeedScaling: false,
|
||||||
|
inpainting: {
|
||||||
|
layer: 'mask',
|
||||||
|
objects: [],
|
||||||
|
pastObjects: [],
|
||||||
|
futureObjects: [],
|
||||||
|
...initialGenericCanvasState,
|
||||||
|
},
|
||||||
|
outpainting: {
|
||||||
|
layer: 'base',
|
||||||
|
objects: [],
|
||||||
|
pastObjects: [],
|
||||||
|
futureObjects: [],
|
||||||
|
stagingArea: {
|
||||||
|
images: [],
|
||||||
|
selectedImageIndex: 0,
|
||||||
|
},
|
||||||
|
shouldShowGrid: true,
|
||||||
|
shouldSnapToGrid: true,
|
||||||
|
shouldAutoSave: false,
|
||||||
|
...initialGenericCanvasState,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const canvasSlice = createSlice({
|
||||||
|
name: 'canvas',
|
||||||
|
initialState: initialCanvasState,
|
||||||
|
reducers: {
|
||||||
|
setTool: (state, action: PayloadAction<CanvasTool>) => {
|
||||||
|
const tool = action.payload;
|
||||||
|
state[state.currentCanvas].tool = action.payload;
|
||||||
|
if (tool !== 'move') {
|
||||||
|
state[state.currentCanvas].isTransformingBoundingBox = false;
|
||||||
|
state[state.currentCanvas].isMouseOverBoundingBox = false;
|
||||||
|
state[state.currentCanvas].isMovingBoundingBox = false;
|
||||||
|
state[state.currentCanvas].isMovingStage = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setLayer: (state, action: PayloadAction<CanvasLayer>) => {
|
||||||
|
state[state.currentCanvas].layer = action.payload;
|
||||||
|
},
|
||||||
|
toggleTool: (state) => {
|
||||||
|
const currentTool = state[state.currentCanvas].tool;
|
||||||
|
if (currentTool !== 'move') {
|
||||||
|
state[state.currentCanvas].tool =
|
||||||
|
currentTool === 'brush' ? 'eraser' : 'brush';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setMaskColor: (state, action: PayloadAction<RgbaColor>) => {
|
||||||
|
state[state.currentCanvas].maskColor = action.payload;
|
||||||
|
},
|
||||||
|
setBrushColor: (state, action: PayloadAction<RgbaColor>) => {
|
||||||
|
state[state.currentCanvas].brushColor = action.payload;
|
||||||
|
},
|
||||||
|
setBrushSize: (state, action: PayloadAction<number>) => {
|
||||||
|
state[state.currentCanvas].brushSize = action.payload;
|
||||||
|
},
|
||||||
|
setEraserSize: (state, action: PayloadAction<number>) => {
|
||||||
|
state[state.currentCanvas].eraserSize = action.payload;
|
||||||
|
},
|
||||||
|
clearMask: (state) => {
|
||||||
|
state[state.currentCanvas].pastObjects.push(
|
||||||
|
state[state.currentCanvas].objects
|
||||||
|
);
|
||||||
|
state[state.currentCanvas].objects = state[
|
||||||
|
state.currentCanvas
|
||||||
|
].objects.filter((obj) => !isCanvasMaskLine(obj));
|
||||||
|
state[state.currentCanvas].futureObjects = [];
|
||||||
|
state[state.currentCanvas].shouldInvertMask = false;
|
||||||
|
},
|
||||||
|
toggleShouldInvertMask: (state) => {
|
||||||
|
state[state.currentCanvas].shouldInvertMask =
|
||||||
|
!state[state.currentCanvas].shouldInvertMask;
|
||||||
|
},
|
||||||
|
toggleShouldShowMask: (state) => {
|
||||||
|
state[state.currentCanvas].isMaskEnabled =
|
||||||
|
!state[state.currentCanvas].isMaskEnabled;
|
||||||
|
},
|
||||||
|
setShouldInvertMask: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state[state.currentCanvas].shouldInvertMask = action.payload;
|
||||||
|
},
|
||||||
|
setIsMaskEnabled: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state[state.currentCanvas].isMaskEnabled = action.payload;
|
||||||
|
state[state.currentCanvas].layer = action.payload ? 'mask' : 'base';
|
||||||
|
},
|
||||||
|
setShouldShowCheckboardTransparency: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<boolean>
|
||||||
|
) => {
|
||||||
|
state[state.currentCanvas].shouldShowCheckboardTransparency =
|
||||||
|
action.payload;
|
||||||
|
},
|
||||||
|
setShouldShowBrushPreview: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state[state.currentCanvas].shouldShowBrushPreview = action.payload;
|
||||||
|
},
|
||||||
|
setShouldShowBrush: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state[state.currentCanvas].shouldShowBrush = action.payload;
|
||||||
|
},
|
||||||
|
setCursorPosition: (state, action: PayloadAction<Vector2d | null>) => {
|
||||||
|
state[state.currentCanvas].cursorPosition = action.payload;
|
||||||
|
},
|
||||||
|
clearImageToInpaint: (state) => {
|
||||||
|
state.inpainting.imageToInpaint = undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
setImageToOutpaint: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||||
|
const { width: canvasWidth, height: canvasHeight } =
|
||||||
|
state.outpainting.stageDimensions;
|
||||||
|
const { width, height } = state.outpainting.boundingBoxDimensions;
|
||||||
|
const { x, y } = state.outpainting.boundingBoxCoordinates;
|
||||||
|
|
||||||
|
const maxWidth = Math.min(action.payload.width, canvasWidth);
|
||||||
|
const maxHeight = Math.min(action.payload.height, canvasHeight);
|
||||||
|
|
||||||
|
const newCoordinates: Vector2d = { x, y };
|
||||||
|
const newDimensions: Dimensions = { width, height };
|
||||||
|
|
||||||
|
if (width + x > maxWidth) {
|
||||||
|
// Bounding box at least needs to be translated
|
||||||
|
if (width > maxWidth) {
|
||||||
|
// Bounding box also needs to be resized
|
||||||
|
newDimensions.width = roundDownToMultiple(maxWidth, 64);
|
||||||
|
}
|
||||||
|
newCoordinates.x = maxWidth - newDimensions.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (height + y > maxHeight) {
|
||||||
|
// Bounding box at least needs to be translated
|
||||||
|
if (height > maxHeight) {
|
||||||
|
// Bounding box also needs to be resized
|
||||||
|
newDimensions.height = roundDownToMultiple(maxHeight, 64);
|
||||||
|
}
|
||||||
|
newCoordinates.y = maxHeight - newDimensions.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.outpainting.boundingBoxDimensions = newDimensions;
|
||||||
|
state.outpainting.boundingBoxCoordinates = newCoordinates;
|
||||||
|
|
||||||
|
// state.outpainting.imageToInpaint = action.payload;
|
||||||
|
state.outpainting.objects = [
|
||||||
|
{
|
||||||
|
kind: 'image',
|
||||||
|
layer: 'base',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
image: action.payload,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
state.doesCanvasNeedScaling = true;
|
||||||
|
},
|
||||||
|
setImageToInpaint: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||||
|
const { width: canvasWidth, height: canvasHeight } =
|
||||||
|
state.inpainting.stageDimensions;
|
||||||
|
const { width, height } = state.inpainting.boundingBoxDimensions;
|
||||||
|
const { x, y } = state.inpainting.boundingBoxCoordinates;
|
||||||
|
|
||||||
|
const maxWidth = Math.min(action.payload.width, canvasWidth);
|
||||||
|
const maxHeight = Math.min(action.payload.height, canvasHeight);
|
||||||
|
|
||||||
|
const newCoordinates: Vector2d = { x, y };
|
||||||
|
const newDimensions: Dimensions = { width, height };
|
||||||
|
|
||||||
|
if (width + x > maxWidth) {
|
||||||
|
// Bounding box at least needs to be translated
|
||||||
|
if (width > maxWidth) {
|
||||||
|
// Bounding box also needs to be resized
|
||||||
|
newDimensions.width = roundDownToMultiple(maxWidth, 64);
|
||||||
|
}
|
||||||
|
newCoordinates.x = maxWidth - newDimensions.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (height + y > maxHeight) {
|
||||||
|
// Bounding box at least needs to be translated
|
||||||
|
if (height > maxHeight) {
|
||||||
|
// Bounding box also needs to be resized
|
||||||
|
newDimensions.height = roundDownToMultiple(maxHeight, 64);
|
||||||
|
}
|
||||||
|
newCoordinates.y = maxHeight - newDimensions.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.inpainting.boundingBoxDimensions = newDimensions;
|
||||||
|
state.inpainting.boundingBoxCoordinates = newCoordinates;
|
||||||
|
|
||||||
|
state.inpainting.imageToInpaint = action.payload;
|
||||||
|
state.doesCanvasNeedScaling = true;
|
||||||
|
},
|
||||||
|
setStageDimensions: (state, action: PayloadAction<Dimensions>) => {
|
||||||
|
state[state.currentCanvas].stageDimensions = action.payload;
|
||||||
|
|
||||||
|
const { width: canvasWidth, height: canvasHeight } = action.payload;
|
||||||
|
|
||||||
|
const { width: boundingBoxWidth, height: boundingBoxHeight } =
|
||||||
|
state[state.currentCanvas].boundingBoxDimensions;
|
||||||
|
|
||||||
|
const newBoundingBoxWidth = roundDownToMultiple(
|
||||||
|
_.clamp(
|
||||||
|
boundingBoxWidth,
|
||||||
|
64,
|
||||||
|
canvasWidth / state[state.currentCanvas].stageScale
|
||||||
|
),
|
||||||
|
64
|
||||||
|
);
|
||||||
|
const newBoundingBoxHeight = roundDownToMultiple(
|
||||||
|
_.clamp(
|
||||||
|
boundingBoxHeight,
|
||||||
|
64,
|
||||||
|
canvasHeight / state[state.currentCanvas].stageScale
|
||||||
|
),
|
||||||
|
64
|
||||||
|
);
|
||||||
|
|
||||||
|
state[state.currentCanvas].boundingBoxDimensions = {
|
||||||
|
width: newBoundingBoxWidth,
|
||||||
|
height: newBoundingBoxHeight,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
setBoundingBoxDimensions: (state, action: PayloadAction<Dimensions>) => {
|
||||||
|
state[state.currentCanvas].boundingBoxDimensions = action.payload;
|
||||||
|
const { width: boundingBoxWidth, height: boundingBoxHeight } =
|
||||||
|
action.payload;
|
||||||
|
const { x: boundingBoxX, y: boundingBoxY } =
|
||||||
|
state[state.currentCanvas].boundingBoxCoordinates;
|
||||||
|
const { width: canvasWidth, height: canvasHeight } =
|
||||||
|
state[state.currentCanvas].stageDimensions;
|
||||||
|
|
||||||
|
const scaledCanvasWidth =
|
||||||
|
canvasWidth / state[state.currentCanvas].stageScale;
|
||||||
|
const scaledCanvasHeight =
|
||||||
|
canvasHeight / state[state.currentCanvas].stageScale;
|
||||||
|
|
||||||
|
const roundedCanvasWidth = roundDownToMultiple(scaledCanvasWidth, 64);
|
||||||
|
const roundedCanvasHeight = roundDownToMultiple(scaledCanvasHeight, 64);
|
||||||
|
|
||||||
|
const roundedBoundingBoxWidth = roundDownToMultiple(boundingBoxWidth, 64);
|
||||||
|
const roundedBoundingBoxHeight = roundDownToMultiple(
|
||||||
|
boundingBoxHeight,
|
||||||
|
64
|
||||||
|
);
|
||||||
|
|
||||||
|
const overflowX = boundingBoxX + boundingBoxWidth - scaledCanvasWidth;
|
||||||
|
const overflowY = boundingBoxY + boundingBoxHeight - scaledCanvasHeight;
|
||||||
|
|
||||||
|
const newBoundingBoxWidth = _.clamp(
|
||||||
|
roundedBoundingBoxWidth,
|
||||||
|
64,
|
||||||
|
roundedCanvasWidth
|
||||||
|
);
|
||||||
|
|
||||||
|
const newBoundingBoxHeight = _.clamp(
|
||||||
|
roundedBoundingBoxHeight,
|
||||||
|
64,
|
||||||
|
roundedCanvasHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
const overflowCorrectedX =
|
||||||
|
overflowX > 0 ? boundingBoxX - overflowX : boundingBoxX;
|
||||||
|
|
||||||
|
const overflowCorrectedY =
|
||||||
|
overflowY > 0 ? boundingBoxY - overflowY : boundingBoxY;
|
||||||
|
|
||||||
|
const clampedX = _.clamp(
|
||||||
|
overflowCorrectedX,
|
||||||
|
state[state.currentCanvas].stageCoordinates.x,
|
||||||
|
roundedCanvasWidth - newBoundingBoxWidth
|
||||||
|
);
|
||||||
|
|
||||||
|
const clampedY = _.clamp(
|
||||||
|
overflowCorrectedY,
|
||||||
|
state[state.currentCanvas].stageCoordinates.y,
|
||||||
|
roundedCanvasHeight - newBoundingBoxHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
state[state.currentCanvas].boundingBoxDimensions = {
|
||||||
|
width: newBoundingBoxWidth,
|
||||||
|
height: newBoundingBoxHeight,
|
||||||
|
};
|
||||||
|
|
||||||
|
state[state.currentCanvas].boundingBoxCoordinates = {
|
||||||
|
x: clampedX,
|
||||||
|
y: clampedY,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
setBoundingBoxCoordinates: (state, action: PayloadAction<Vector2d>) => {
|
||||||
|
state[state.currentCanvas].boundingBoxCoordinates = action.payload;
|
||||||
|
},
|
||||||
|
setStageCoordinates: (state, action: PayloadAction<Vector2d>) => {
|
||||||
|
state[state.currentCanvas].stageCoordinates = action.payload;
|
||||||
|
},
|
||||||
|
setBoundingBoxPreviewFill: (state, action: PayloadAction<RgbaColor>) => {
|
||||||
|
state[state.currentCanvas].boundingBoxPreviewFill = action.payload;
|
||||||
|
},
|
||||||
|
setDoesCanvasNeedScaling: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.doesCanvasNeedScaling = action.payload;
|
||||||
|
},
|
||||||
|
setStageScale: (state, action: PayloadAction<number>) => {
|
||||||
|
state[state.currentCanvas].stageScale = action.payload;
|
||||||
|
state.doesCanvasNeedScaling = false;
|
||||||
|
},
|
||||||
|
setShouldDarkenOutsideBoundingBox: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<boolean>
|
||||||
|
) => {
|
||||||
|
state[state.currentCanvas].shouldDarkenOutsideBoundingBox =
|
||||||
|
action.payload;
|
||||||
|
},
|
||||||
|
setIsDrawing: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state[state.currentCanvas].isDrawing = action.payload;
|
||||||
|
},
|
||||||
|
setClearBrushHistory: (state) => {
|
||||||
|
state[state.currentCanvas].pastObjects = [];
|
||||||
|
state[state.currentCanvas].futureObjects = [];
|
||||||
|
},
|
||||||
|
setShouldUseInpaintReplace: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state[state.currentCanvas].shouldUseInpaintReplace = action.payload;
|
||||||
|
},
|
||||||
|
setInpaintReplace: (state, action: PayloadAction<number>) => {
|
||||||
|
state[state.currentCanvas].inpaintReplace = action.payload;
|
||||||
|
},
|
||||||
|
setShouldLockBoundingBox: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state[state.currentCanvas].shouldLockBoundingBox = action.payload;
|
||||||
|
},
|
||||||
|
toggleShouldLockBoundingBox: (state) => {
|
||||||
|
state[state.currentCanvas].shouldLockBoundingBox =
|
||||||
|
!state[state.currentCanvas].shouldLockBoundingBox;
|
||||||
|
},
|
||||||
|
setShouldShowBoundingBox: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state[state.currentCanvas].shouldShowBoundingBox = action.payload;
|
||||||
|
},
|
||||||
|
setIsTransformingBoundingBox: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state[state.currentCanvas].isTransformingBoundingBox = action.payload;
|
||||||
|
},
|
||||||
|
setIsMovingBoundingBox: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state[state.currentCanvas].isMovingBoundingBox = action.payload;
|
||||||
|
},
|
||||||
|
setIsMouseOverBoundingBox: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state[state.currentCanvas].isMouseOverBoundingBox = action.payload;
|
||||||
|
},
|
||||||
|
setIsMoveBoundingBoxKeyHeld: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state[state.currentCanvas].isMoveBoundingBoxKeyHeld = action.payload;
|
||||||
|
},
|
||||||
|
setIsMoveStageKeyHeld: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state[state.currentCanvas].isMoveStageKeyHeld = action.payload;
|
||||||
|
},
|
||||||
|
setCurrentCanvas: (state, action: PayloadAction<ValidCanvasName>) => {
|
||||||
|
state.currentCanvas = action.payload;
|
||||||
|
},
|
||||||
|
addImageToOutpaintingSesion: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
boundingBox: IRect;
|
||||||
|
image: InvokeAI.Image;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const { boundingBox, image } = action.payload;
|
||||||
|
if (!boundingBox || !image) return;
|
||||||
|
|
||||||
|
const { x, y } = boundingBox;
|
||||||
|
|
||||||
|
state.outpainting.pastObjects.push([...state.outpainting.objects]);
|
||||||
|
state.outpainting.futureObjects = [];
|
||||||
|
|
||||||
|
state.outpainting.objects.push({
|
||||||
|
kind: 'image',
|
||||||
|
layer: 'base',
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
image,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addLine: (state, action: PayloadAction<number[]>) => {
|
||||||
|
const { tool, layer, brushColor, brushSize, eraserSize } =
|
||||||
|
state[state.currentCanvas];
|
||||||
|
|
||||||
|
if (tool === 'move') return;
|
||||||
|
|
||||||
|
state[state.currentCanvas].pastObjects.push(
|
||||||
|
state[state.currentCanvas].objects
|
||||||
|
);
|
||||||
|
|
||||||
|
state[state.currentCanvas].objects.push({
|
||||||
|
kind: 'line',
|
||||||
|
layer,
|
||||||
|
tool,
|
||||||
|
strokeWidth: tool === 'brush' ? brushSize / 2 : eraserSize / 2,
|
||||||
|
points: action.payload,
|
||||||
|
...(layer === 'base' && tool === 'brush' && { color: brushColor }),
|
||||||
|
});
|
||||||
|
|
||||||
|
state[state.currentCanvas].futureObjects = [];
|
||||||
|
},
|
||||||
|
addPointToCurrentLine: (state, action: PayloadAction<number[]>) => {
|
||||||
|
const lastLine =
|
||||||
|
state[state.currentCanvas].objects.findLast(isCanvasAnyLine);
|
||||||
|
|
||||||
|
if (!lastLine) return;
|
||||||
|
|
||||||
|
lastLine.points.push(...action.payload);
|
||||||
|
},
|
||||||
|
undo: (state) => {
|
||||||
|
if (state.outpainting.objects.length === 0) return;
|
||||||
|
|
||||||
|
const newObjects = state.outpainting.pastObjects.pop();
|
||||||
|
if (!newObjects) return;
|
||||||
|
state.outpainting.futureObjects.unshift(state.outpainting.objects);
|
||||||
|
state.outpainting.objects = newObjects;
|
||||||
|
},
|
||||||
|
redo: (state) => {
|
||||||
|
if (state.outpainting.futureObjects.length === 0) return;
|
||||||
|
const newObjects = state.outpainting.futureObjects.shift();
|
||||||
|
if (!newObjects) return;
|
||||||
|
state.outpainting.pastObjects.push(state.outpainting.objects);
|
||||||
|
state.outpainting.objects = newObjects;
|
||||||
|
},
|
||||||
|
setShouldShowGrid: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.outpainting.shouldShowGrid = action.payload;
|
||||||
|
},
|
||||||
|
setIsMovingStage: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state[state.currentCanvas].isMovingStage = action.payload;
|
||||||
|
},
|
||||||
|
setShouldSnapToGrid: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.outpainting.shouldSnapToGrid = action.payload;
|
||||||
|
},
|
||||||
|
setShouldAutoSave: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.outpainting.shouldAutoSave = action.payload;
|
||||||
|
},
|
||||||
|
setShouldShowIntermediates: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state[state.currentCanvas].shouldShowIntermediates = action.payload;
|
||||||
|
},
|
||||||
|
resetCanvas: (state) => {
|
||||||
|
state[state.currentCanvas].pastObjects.push(
|
||||||
|
state[state.currentCanvas].objects
|
||||||
|
);
|
||||||
|
|
||||||
|
state[state.currentCanvas].objects = [];
|
||||||
|
state[state.currentCanvas].futureObjects = [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder.addCase(uploadOutpaintingMergedImage.fulfilled, (state, action) => {
|
||||||
|
if (!action.payload) return;
|
||||||
|
state.outpainting.pastObjects.push([...state.outpainting.objects]);
|
||||||
|
state.outpainting.futureObjects = [];
|
||||||
|
|
||||||
|
state.outpainting.objects = [
|
||||||
|
{
|
||||||
|
kind: 'image',
|
||||||
|
layer: 'base',
|
||||||
|
...action.payload,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
setTool,
|
||||||
|
setLayer,
|
||||||
|
setBrushColor,
|
||||||
|
setBrushSize,
|
||||||
|
setEraserSize,
|
||||||
|
addLine,
|
||||||
|
addPointToCurrentLine,
|
||||||
|
setShouldInvertMask,
|
||||||
|
setIsMaskEnabled,
|
||||||
|
setShouldShowCheckboardTransparency,
|
||||||
|
setShouldShowBrushPreview,
|
||||||
|
setMaskColor,
|
||||||
|
clearMask,
|
||||||
|
clearImageToInpaint,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
setCursorPosition,
|
||||||
|
setStageDimensions,
|
||||||
|
setImageToInpaint,
|
||||||
|
setImageToOutpaint,
|
||||||
|
setBoundingBoxDimensions,
|
||||||
|
setBoundingBoxCoordinates,
|
||||||
|
setBoundingBoxPreviewFill,
|
||||||
|
setDoesCanvasNeedScaling,
|
||||||
|
setStageScale,
|
||||||
|
toggleTool,
|
||||||
|
setShouldShowBoundingBox,
|
||||||
|
setShouldDarkenOutsideBoundingBox,
|
||||||
|
setIsDrawing,
|
||||||
|
setShouldShowBrush,
|
||||||
|
setClearBrushHistory,
|
||||||
|
setShouldUseInpaintReplace,
|
||||||
|
setInpaintReplace,
|
||||||
|
setShouldLockBoundingBox,
|
||||||
|
toggleShouldLockBoundingBox,
|
||||||
|
setIsMovingBoundingBox,
|
||||||
|
setIsTransformingBoundingBox,
|
||||||
|
setIsMouseOverBoundingBox,
|
||||||
|
setIsMoveBoundingBoxKeyHeld,
|
||||||
|
setIsMoveStageKeyHeld,
|
||||||
|
setStageCoordinates,
|
||||||
|
setCurrentCanvas,
|
||||||
|
addImageToOutpaintingSesion,
|
||||||
|
resetCanvas,
|
||||||
|
setShouldShowGrid,
|
||||||
|
setShouldSnapToGrid,
|
||||||
|
setShouldAutoSave,
|
||||||
|
setShouldShowIntermediates,
|
||||||
|
setIsMovingStage,
|
||||||
|
} = canvasSlice.actions;
|
||||||
|
|
||||||
|
export default canvasSlice.reducer;
|
||||||
|
|
||||||
|
export const uploadOutpaintingMergedImage = createAsyncThunk(
|
||||||
|
'canvas/uploadOutpaintingMergedImage',
|
||||||
|
async (
|
||||||
|
canvasImageLayerRef: MutableRefObject<Konva.Layer | null>,
|
||||||
|
thunkAPI
|
||||||
|
) => {
|
||||||
|
const { getState } = thunkAPI;
|
||||||
|
|
||||||
|
const state = getState() as RootState;
|
||||||
|
const stageScale = state.canvas.outpainting.stageScale;
|
||||||
|
|
||||||
|
if (!canvasImageLayerRef.current) return;
|
||||||
|
const tempScale = canvasImageLayerRef.current.scale();
|
||||||
|
|
||||||
|
const { x: relativeX, y: relativeY } =
|
||||||
|
canvasImageLayerRef.current.getClientRect({
|
||||||
|
relativeTo: canvasImageLayerRef.current.getParent(),
|
||||||
|
});
|
||||||
|
|
||||||
|
canvasImageLayerRef.current.scale({
|
||||||
|
x: 1 / stageScale,
|
||||||
|
y: 1 / stageScale,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientRect = canvasImageLayerRef.current.getClientRect();
|
||||||
|
|
||||||
|
const imageDataURL = canvasImageLayerRef.current.toDataURL(clientRect);
|
||||||
|
|
||||||
|
canvasImageLayerRef.current.scale(tempScale);
|
||||||
|
|
||||||
|
if (!imageDataURL) return;
|
||||||
|
|
||||||
|
const response = await fetch(window.location.origin + '/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
dataURL: imageDataURL,
|
||||||
|
name: 'outpaintingmerge.png',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = (await response.json()) as InvokeAI.ImageUploadResponse;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { destination, ...rest } = data;
|
||||||
|
const image = {
|
||||||
|
uuid: uuidv4(),
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
image,
|
||||||
|
x: relativeX,
|
||||||
|
y: relativeY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const currentCanvasSelector = (state: RootState): BaseCanvasState =>
|
||||||
|
state.canvas[state.canvas.currentCanvas];
|
||||||
|
|
||||||
|
export const outpaintingCanvasSelector = (
|
||||||
|
state: RootState
|
||||||
|
): OutpaintingCanvasState => state.canvas.outpainting;
|
||||||
|
|
||||||
|
export const inpaintingCanvasSelector = (
|
||||||
|
state: RootState
|
||||||
|
): InpaintingCanvasState => state.canvas.inpainting;
|
||||||
|
|
||||||
|
export const baseCanvasImageSelector = createSelector(
|
||||||
|
[(state: RootState) => state.canvas, activeTabNameSelector],
|
||||||
|
(canvas: CanvasState, activeTabName) => {
|
||||||
|
if (activeTabName === 'inpainting') {
|
||||||
|
return canvas.inpainting.imageToInpaint;
|
||||||
|
} else if (activeTabName === 'outpainting') {
|
||||||
|
const firstImageObject = canvas.outpainting.objects.find(
|
||||||
|
(obj) => obj.kind === 'image'
|
||||||
|
);
|
||||||
|
if (firstImageObject && firstImageObject.kind === 'image') {
|
||||||
|
return firstImageObject.image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
49
frontend/src/features/canvas/hooks/useCanvasDragMove.ts
Normal file
49
frontend/src/features/canvas/hooks/useCanvasDragMove.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import { KonvaEventObject } from 'konva/lib/Node';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
currentCanvasSelector,
|
||||||
|
setIsMovingStage,
|
||||||
|
setStageCoordinates,
|
||||||
|
} from '../canvasSlice';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[currentCanvasSelector, activeTabNameSelector],
|
||||||
|
(canvas, activeTabName) => {
|
||||||
|
const { tool } = canvas;
|
||||||
|
return {
|
||||||
|
tool,
|
||||||
|
|
||||||
|
activeTabName,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ memoizeOptions: { resultEqualityCheck: _.isEqual } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const useCanvasDrag = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { tool, activeTabName } = useAppSelector(selector);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleDragStart: useCallback(() => {
|
||||||
|
if (tool !== 'move' || activeTabName !== 'outpainting') return;
|
||||||
|
dispatch(setIsMovingStage(true));
|
||||||
|
}, [activeTabName, dispatch, tool]),
|
||||||
|
handleDragMove: useCallback(
|
||||||
|
(e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
if (tool !== 'move' || activeTabName !== 'outpainting') return;
|
||||||
|
dispatch(setStageCoordinates(e.target.getPosition()));
|
||||||
|
},
|
||||||
|
[activeTabName, dispatch, tool]
|
||||||
|
),
|
||||||
|
handleDragEnd: useCallback(() => {
|
||||||
|
if (tool !== 'move' || activeTabName !== 'outpainting') return;
|
||||||
|
dispatch(setIsMovingStage(false));
|
||||||
|
}, [activeTabName, dispatch, tool]),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCanvasDrag;
|
111
frontend/src/features/canvas/hooks/useCanvasHotkeys.ts
Normal file
111
frontend/src/features/canvas/hooks/useCanvasHotkeys.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import { OptionsState } from 'features/options/optionsSlice';
|
||||||
|
import {
|
||||||
|
CanvasTool,
|
||||||
|
setShouldShowBoundingBox,
|
||||||
|
setTool,
|
||||||
|
toggleShouldLockBoundingBox,
|
||||||
|
} from 'features/canvas/canvasSlice';
|
||||||
|
import { RootState, useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import { currentCanvasSelector, GenericCanvasState } from '../canvasSlice';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
const inpaintingCanvasHotkeysSelector = createSelector(
|
||||||
|
[
|
||||||
|
(state: RootState) => state.options,
|
||||||
|
currentCanvasSelector,
|
||||||
|
activeTabNameSelector,
|
||||||
|
],
|
||||||
|
(options: OptionsState, currentCanvas: GenericCanvasState, activeTabName) => {
|
||||||
|
const {
|
||||||
|
isMaskEnabled,
|
||||||
|
cursorPosition,
|
||||||
|
shouldLockBoundingBox,
|
||||||
|
shouldShowBoundingBox,
|
||||||
|
tool,
|
||||||
|
} = currentCanvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeTabName,
|
||||||
|
isMaskEnabled,
|
||||||
|
isCursorOnCanvas: Boolean(cursorPosition),
|
||||||
|
shouldLockBoundingBox,
|
||||||
|
shouldShowBoundingBox,
|
||||||
|
tool,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const useInpaintingCanvasHotkeys = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { isMaskEnabled, activeTabName, shouldShowBoundingBox, tool } =
|
||||||
|
useAppSelector(inpaintingCanvasHotkeysSelector);
|
||||||
|
|
||||||
|
const previousToolRef = useRef<CanvasTool | null>(null);
|
||||||
|
// Toggle lock bounding box
|
||||||
|
useHotkeys(
|
||||||
|
'shift+w',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch(toggleShouldLockBoundingBox());
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
[activeTabName]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'shift+h',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch(setShouldShowBoundingBox(!shouldShowBoundingBox));
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
[activeTabName, shouldShowBoundingBox]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['space'],
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (e.repeat) return;
|
||||||
|
|
||||||
|
if (tool !== 'move') {
|
||||||
|
previousToolRef.current = tool;
|
||||||
|
dispatch(setTool('move'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ keyup: false, keydown: true },
|
||||||
|
[tool, previousToolRef]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['space'],
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (e.repeat) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
tool === 'move' &&
|
||||||
|
previousToolRef.current &&
|
||||||
|
previousToolRef.current !== 'move'
|
||||||
|
) {
|
||||||
|
dispatch(setTool(previousToolRef.current));
|
||||||
|
previousToolRef.current = 'move';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ keyup: true, keydown: false },
|
||||||
|
[tool, previousToolRef]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useInpaintingCanvasHotkeys;
|
56
frontend/src/features/canvas/hooks/useCanvasMouseDown.ts
Normal file
56
frontend/src/features/canvas/hooks/useCanvasMouseDown.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import { KonvaEventObject } from 'konva/lib/Node';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { MutableRefObject, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
addLine,
|
||||||
|
currentCanvasSelector,
|
||||||
|
setIsDrawing,
|
||||||
|
setIsMovingStage,
|
||||||
|
} from '../canvasSlice';
|
||||||
|
import getScaledCursorPosition from '../util/getScaledCursorPosition';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[activeTabNameSelector, currentCanvasSelector],
|
||||||
|
(activeTabName, currentCanvas) => {
|
||||||
|
const { tool } = currentCanvas;
|
||||||
|
return {
|
||||||
|
tool,
|
||||||
|
activeTabName,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ memoizeOptions: { resultEqualityCheck: _.isEqual } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const useCanvasMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { tool } = useAppSelector(selector);
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
(e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
if (!stageRef.current) return;
|
||||||
|
|
||||||
|
if (tool === 'move') {
|
||||||
|
dispatch(setIsMovingStage(true));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
|
||||||
|
|
||||||
|
if (!scaledCursorPosition) return;
|
||||||
|
|
||||||
|
e.evt.preventDefault();
|
||||||
|
|
||||||
|
dispatch(setIsDrawing(true));
|
||||||
|
|
||||||
|
// Add a new line starting from the current cursor position.
|
||||||
|
dispatch(addLine([scaledCursorPosition.x, scaledCursorPosition.y]));
|
||||||
|
},
|
||||||
|
[stageRef, dispatch, tool]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCanvasMouseDown;
|
48
frontend/src/features/canvas/hooks/useCanvasMouseEnter.ts
Normal file
48
frontend/src/features/canvas/hooks/useCanvasMouseEnter.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import { KonvaEventObject } from 'konva/lib/Node';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { MutableRefObject, useCallback } from 'react';
|
||||||
|
import { addLine, currentCanvasSelector, setIsDrawing } from '../canvasSlice';
|
||||||
|
import getScaledCursorPosition from '../util/getScaledCursorPosition';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[activeTabNameSelector, currentCanvasSelector],
|
||||||
|
(activeTabName, currentCanvas) => {
|
||||||
|
const { tool } = currentCanvas;
|
||||||
|
return {
|
||||||
|
tool,
|
||||||
|
activeTabName,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ memoizeOptions: { resultEqualityCheck: _.isEqual } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const useCanvasMouseEnter = (
|
||||||
|
stageRef: MutableRefObject<Konva.Stage | null>
|
||||||
|
) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { tool } = useAppSelector(selector);
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
(e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
if (e.evt.buttons !== 1) return;
|
||||||
|
|
||||||
|
if (!stageRef.current) return;
|
||||||
|
|
||||||
|
const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
|
||||||
|
|
||||||
|
if (!scaledCursorPosition || tool === 'move') return;
|
||||||
|
|
||||||
|
dispatch(setIsDrawing(true));
|
||||||
|
|
||||||
|
// Add a new line starting from the current cursor position.
|
||||||
|
dispatch(addLine([scaledCursorPosition.x, scaledCursorPosition.y]));
|
||||||
|
},
|
||||||
|
[stageRef, tool, dispatch]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCanvasMouseEnter;
|
64
frontend/src/features/canvas/hooks/useCanvasMouseMove.ts
Normal file
64
frontend/src/features/canvas/hooks/useCanvasMouseMove.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import { Vector2d } from 'konva/lib/types';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { MutableRefObject, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
addPointToCurrentLine,
|
||||||
|
currentCanvasSelector,
|
||||||
|
GenericCanvasState,
|
||||||
|
setCursorPosition,
|
||||||
|
} from '../canvasSlice';
|
||||||
|
import getScaledCursorPosition from '../util/getScaledCursorPosition';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[activeTabNameSelector, currentCanvasSelector],
|
||||||
|
(activeTabName, canvas: GenericCanvasState) => {
|
||||||
|
const { tool, isDrawing } = canvas;
|
||||||
|
return {
|
||||||
|
tool,
|
||||||
|
isDrawing,
|
||||||
|
activeTabName,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ memoizeOptions: { resultEqualityCheck: _.isEqual } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const useCanvasMouseMove = (
|
||||||
|
stageRef: MutableRefObject<Konva.Stage | null>,
|
||||||
|
didMouseMoveRef: MutableRefObject<boolean>,
|
||||||
|
lastCursorPositionRef: MutableRefObject<Vector2d>
|
||||||
|
) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { isDrawing, tool } = useAppSelector(selector);
|
||||||
|
|
||||||
|
return useCallback(() => {
|
||||||
|
if (!stageRef.current) return;
|
||||||
|
|
||||||
|
const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
|
||||||
|
|
||||||
|
if (!scaledCursorPosition) return;
|
||||||
|
|
||||||
|
dispatch(setCursorPosition(scaledCursorPosition));
|
||||||
|
|
||||||
|
lastCursorPositionRef.current = scaledCursorPosition;
|
||||||
|
|
||||||
|
if (!isDrawing || tool === 'move') return;
|
||||||
|
|
||||||
|
didMouseMoveRef.current = true;
|
||||||
|
dispatch(
|
||||||
|
addPointToCurrentLine([scaledCursorPosition.x, scaledCursorPosition.y])
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
didMouseMoveRef,
|
||||||
|
dispatch,
|
||||||
|
isDrawing,
|
||||||
|
lastCursorPositionRef,
|
||||||
|
stageRef,
|
||||||
|
tool,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCanvasMouseMove;
|
15
frontend/src/features/canvas/hooks/useCanvasMouseOut.ts
Normal file
15
frontend/src/features/canvas/hooks/useCanvasMouseOut.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useAppDispatch } from 'app/store';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { setCursorPosition, setIsDrawing } from '../canvasSlice';
|
||||||
|
|
||||||
|
const useCanvasMouseOut = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
return useCallback(() => {
|
||||||
|
dispatch(setCursorPosition(null));
|
||||||
|
dispatch(setIsDrawing(false));
|
||||||
|
}, [dispatch]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCanvasMouseOut;
|
64
frontend/src/features/canvas/hooks/useCanvasMouseUp.ts
Normal file
64
frontend/src/features/canvas/hooks/useCanvasMouseUp.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { MutableRefObject, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
// addPointToCurrentEraserLine,
|
||||||
|
addPointToCurrentLine,
|
||||||
|
currentCanvasSelector,
|
||||||
|
GenericCanvasState,
|
||||||
|
setIsDrawing,
|
||||||
|
setIsMovingStage,
|
||||||
|
} from '../canvasSlice';
|
||||||
|
import getScaledCursorPosition from '../util/getScaledCursorPosition';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[activeTabNameSelector, currentCanvasSelector],
|
||||||
|
(activeTabName, canvas: GenericCanvasState) => {
|
||||||
|
const { tool, isDrawing } = canvas;
|
||||||
|
return {
|
||||||
|
tool,
|
||||||
|
isDrawing,
|
||||||
|
activeTabName,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ memoizeOptions: { resultEqualityCheck: _.isEqual } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const useCanvasMouseUp = (
|
||||||
|
stageRef: MutableRefObject<Konva.Stage | null>,
|
||||||
|
didMouseMoveRef: MutableRefObject<boolean>
|
||||||
|
) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { tool, isDrawing } = useAppSelector(selector);
|
||||||
|
|
||||||
|
return useCallback(() => {
|
||||||
|
if (tool === 'move') {
|
||||||
|
dispatch(setIsMovingStage(false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!didMouseMoveRef.current && isDrawing && stageRef.current) {
|
||||||
|
const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
|
||||||
|
|
||||||
|
if (!scaledCursorPosition) return;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend the current line.
|
||||||
|
* In this case, the mouse didn't move, so we append the same point to
|
||||||
|
* the line's existing points. This allows the line to render as a circle
|
||||||
|
* centered on that point.
|
||||||
|
*/
|
||||||
|
dispatch(
|
||||||
|
addPointToCurrentLine([scaledCursorPosition.x, scaledCursorPosition.y])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
didMouseMoveRef.current = false;
|
||||||
|
}
|
||||||
|
dispatch(setIsDrawing(false));
|
||||||
|
}, [didMouseMoveRef, dispatch, isDrawing, stageRef, tool]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCanvasMouseUp;
|
83
frontend/src/features/canvas/hooks/useCanvasZoom.ts
Normal file
83
frontend/src/features/canvas/hooks/useCanvasZoom.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import { KonvaEventObject } from 'konva/lib/Node';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { MutableRefObject, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
currentCanvasSelector,
|
||||||
|
GenericCanvasState,
|
||||||
|
setStageCoordinates,
|
||||||
|
setStageScale,
|
||||||
|
} from '../canvasSlice';
|
||||||
|
import {
|
||||||
|
CANVAS_SCALE_BY,
|
||||||
|
MAX_CANVAS_SCALE,
|
||||||
|
MIN_CANVAS_SCALE,
|
||||||
|
} from '../util/constants';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[activeTabNameSelector, currentCanvasSelector],
|
||||||
|
(activeTabName, canvas: GenericCanvasState) => {
|
||||||
|
const { isMoveStageKeyHeld, stageScale } = canvas;
|
||||||
|
return {
|
||||||
|
isMoveStageKeyHeld,
|
||||||
|
stageScale,
|
||||||
|
activeTabName,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ memoizeOptions: { resultEqualityCheck: _.isEqual } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const useCanvasWheel = (stageRef: MutableRefObject<Konva.Stage | null>) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { isMoveStageKeyHeld, stageScale, activeTabName } =
|
||||||
|
useAppSelector(selector);
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
(e: KonvaEventObject<WheelEvent>) => {
|
||||||
|
// stop default scrolling
|
||||||
|
if (activeTabName !== 'outpainting') return;
|
||||||
|
|
||||||
|
e.evt.preventDefault();
|
||||||
|
|
||||||
|
// const oldScale = stageRef.current.scaleX();
|
||||||
|
if (!stageRef.current || isMoveStageKeyHeld) return;
|
||||||
|
|
||||||
|
const cursorPos = stageRef.current.getPointerPosition();
|
||||||
|
|
||||||
|
if (!cursorPos) return;
|
||||||
|
|
||||||
|
const mousePointTo = {
|
||||||
|
x: (cursorPos.x - stageRef.current.x()) / stageScale,
|
||||||
|
y: (cursorPos.y - stageRef.current.y()) / stageScale,
|
||||||
|
};
|
||||||
|
|
||||||
|
let delta = e.evt.deltaY;
|
||||||
|
|
||||||
|
// when we zoom on trackpad, e.evt.ctrlKey is true
|
||||||
|
// in that case lets revert direction
|
||||||
|
if (e.evt.ctrlKey) {
|
||||||
|
delta = -delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newScale = _.clamp(
|
||||||
|
stageScale * CANVAS_SCALE_BY ** delta,
|
||||||
|
MIN_CANVAS_SCALE,
|
||||||
|
MAX_CANVAS_SCALE
|
||||||
|
);
|
||||||
|
|
||||||
|
const newPos = {
|
||||||
|
x: cursorPos.x - mousePointTo.x * newScale,
|
||||||
|
y: cursorPos.y - mousePointTo.y * newScale,
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(setStageScale(newScale));
|
||||||
|
dispatch(setStageCoordinates(newPos));
|
||||||
|
},
|
||||||
|
[activeTabName, dispatch, isMoveStageKeyHeld, stageRef, stageScale]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCanvasWheel;
|
23
frontend/src/features/canvas/hooks/useUnscaleCanvasValue.ts
Normal file
23
frontend/src/features/canvas/hooks/useUnscaleCanvasValue.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { currentCanvasSelector, GenericCanvasState } from '../canvasSlice';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[currentCanvasSelector],
|
||||||
|
(currentCanvas: GenericCanvasState) => {
|
||||||
|
return currentCanvas.stageScale;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const useUnscaleCanvasValue = () => {
|
||||||
|
const stageScale = useAppSelector(selector);
|
||||||
|
return (value: number) => value / stageScale;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useUnscaleCanvasValue;
|
@ -7,4 +7,8 @@ export const MARCHING_ANTS_SPEED = 30;
|
|||||||
// bounding box anchor size
|
// bounding box anchor size
|
||||||
export const TRANSFORMER_ANCHOR_SIZE = 15;
|
export const TRANSFORMER_ANCHOR_SIZE = 15;
|
||||||
|
|
||||||
|
export const CANVAS_SCALE_BY = 0.999;
|
||||||
|
|
||||||
|
export const MIN_CANVAS_SCALE = 0.1
|
||||||
|
|
||||||
|
export const MAX_CANVAS_SCALE = 20
|
64
frontend/src/features/canvas/util/generateMask.ts
Normal file
64
frontend/src/features/canvas/util/generateMask.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import Konva from 'konva';
|
||||||
|
import { IRect } from 'konva/lib/types';
|
||||||
|
import { CanvasMaskLine } from 'features/canvas/canvasSlice';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generating a mask image from InpaintingCanvas.tsx is not as simple
|
||||||
|
* as calling toDataURL() on the canvas, because the mask may be represented
|
||||||
|
* by colored lines or transparency, or the user may have inverted the mask
|
||||||
|
* display.
|
||||||
|
*
|
||||||
|
* So we need to regenerate the mask image by creating an offscreen canvas,
|
||||||
|
* drawing the mask and compositing everything correctly to output a valid
|
||||||
|
* mask image.
|
||||||
|
*/
|
||||||
|
const generateMask = (lines: CanvasMaskLine[], boundingBox: IRect): string => {
|
||||||
|
// create an offscreen canvas and add the mask to it
|
||||||
|
const { width, height } = boundingBox;
|
||||||
|
|
||||||
|
const offscreenContainer = document.createElement('div');
|
||||||
|
|
||||||
|
const stage = new Konva.Stage({
|
||||||
|
container: offscreenContainer,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseLayer = new Konva.Layer();
|
||||||
|
const maskLayer = new Konva.Layer();
|
||||||
|
|
||||||
|
// composite the image onto the mask layer
|
||||||
|
baseLayer.add(
|
||||||
|
new Konva.Rect({
|
||||||
|
...boundingBox,
|
||||||
|
fill: 'white',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
lines.forEach((line) =>
|
||||||
|
maskLayer.add(
|
||||||
|
new Konva.Line({
|
||||||
|
points: line.points,
|
||||||
|
stroke: 'black',
|
||||||
|
strokeWidth: line.strokeWidth * 2,
|
||||||
|
tension: 0,
|
||||||
|
lineCap: 'round',
|
||||||
|
lineJoin: 'round',
|
||||||
|
shadowForStrokeEnabled: false,
|
||||||
|
globalCompositeOperation:
|
||||||
|
line.tool === 'brush' ? 'source-over' : 'destination-out',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
stage.add(baseLayer);
|
||||||
|
stage.add(maskLayer);
|
||||||
|
|
||||||
|
const dataURL = stage.toDataURL({ ...boundingBox });
|
||||||
|
|
||||||
|
offscreenContainer.remove();
|
||||||
|
|
||||||
|
return dataURL;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default generateMask;
|
@ -13,11 +13,18 @@
|
|||||||
max-width: 25rem;
|
max-width: 25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.current-image-send-to-popover {
|
||||||
|
.invokeai__button {
|
||||||
|
place-content: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.chakra-popover__popper {
|
.chakra-popover__popper {
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-image-btn {
|
.delete-image-btn {
|
||||||
|
background-color: var(--btn-base-color);
|
||||||
svg {
|
svg {
|
||||||
fill: var(--btn-delete-image);
|
fill: var(--btn-delete-image);
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,25 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from 'app/store';
|
||||||
import {
|
import {
|
||||||
OptionsState,
|
OptionsState,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
setAllParameters,
|
setAllParameters,
|
||||||
setInitialImage,
|
setInitialImage,
|
||||||
|
setIsLightBoxOpen,
|
||||||
setPrompt,
|
setPrompt,
|
||||||
setSeed,
|
setSeed,
|
||||||
setShouldShowImageDetails,
|
setShouldShowImageDetails,
|
||||||
} from '../options/optionsSlice';
|
} from 'features/options/optionsSlice';
|
||||||
import DeleteImageModal from './DeleteImageModal';
|
import DeleteImageModal from './DeleteImageModal';
|
||||||
import { SystemState } from '../system/systemSlice';
|
import { SystemState } from 'features/system/systemSlice';
|
||||||
import IAIButton from '../../common/components/IAIButton';
|
import IAIButton from 'common/components/IAIButton';
|
||||||
import { runESRGAN, runFacetool } from '../../app/socketio/actions';
|
import { runESRGAN, runFacetool } from 'app/socketio/actions';
|
||||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import UpscaleOptions from '../options/AdvancedOptions/Upscale/UpscaleOptions';
|
import UpscaleOptions from 'features/options/AdvancedOptions/Upscale/UpscaleOptions';
|
||||||
import FaceRestoreOptions from '../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
import FaceRestoreOptions from 'features/options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { ButtonGroup, Link, useClipboard, useToast } from '@chakra-ui/react';
|
import { ButtonGroup, Link, useClipboard, useToast } from '@chakra-ui/react';
|
||||||
import {
|
import {
|
||||||
@ -36,11 +37,12 @@ import {
|
|||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import {
|
import {
|
||||||
setImageToInpaint,
|
setImageToInpaint,
|
||||||
setNeedsCache,
|
setDoesCanvasNeedScaling,
|
||||||
} from '../tabs/Inpainting/inpaintingSlice';
|
setImageToOutpaint,
|
||||||
|
} from 'features/canvas/canvasSlice';
|
||||||
import { GalleryState } from './gallerySlice';
|
import { GalleryState } from './gallerySlice';
|
||||||
import { activeTabNameSelector } from '../options/optionsSelectors';
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
import IAIPopover from '../../common/components/IAIPopover';
|
import IAIPopover from 'common/components/IAIPopover';
|
||||||
|
|
||||||
const systemSelector = createSelector(
|
const systemSelector = createSelector(
|
||||||
[
|
[
|
||||||
@ -58,8 +60,12 @@ const systemSelector = createSelector(
|
|||||||
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
|
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
|
||||||
system;
|
system;
|
||||||
|
|
||||||
const { upscalingLevel, facetoolStrength, shouldShowImageDetails } =
|
const {
|
||||||
options;
|
upscalingLevel,
|
||||||
|
facetoolStrength,
|
||||||
|
shouldShowImageDetails,
|
||||||
|
isLightBoxOpen,
|
||||||
|
} = options;
|
||||||
|
|
||||||
const { intermediateImage, currentImage } = gallery;
|
const { intermediateImage, currentImage } = gallery;
|
||||||
|
|
||||||
@ -74,6 +80,7 @@ const systemSelector = createSelector(
|
|||||||
currentImage,
|
currentImage,
|
||||||
shouldShowImageDetails,
|
shouldShowImageDetails,
|
||||||
activeTabName,
|
activeTabName,
|
||||||
|
isLightBoxOpen,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -99,28 +106,31 @@ const CurrentImageButtons = () => {
|
|||||||
shouldDisableToolbarButtons,
|
shouldDisableToolbarButtons,
|
||||||
shouldShowImageDetails,
|
shouldShowImageDetails,
|
||||||
currentImage,
|
currentImage,
|
||||||
|
isLightBoxOpen,
|
||||||
} = useAppSelector(systemSelector);
|
} = useAppSelector(systemSelector);
|
||||||
|
|
||||||
const { onCopy } = useClipboard(
|
|
||||||
currentImage ? window.location.toString() + currentImage.url : ''
|
|
||||||
);
|
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const handleClickUseAsInitialImage = () => {
|
const handleClickUseAsInitialImage = () => {
|
||||||
if (!currentImage) return;
|
if (!currentImage) return;
|
||||||
|
if (isLightBoxOpen) dispatch(setIsLightBoxOpen(false));
|
||||||
dispatch(setInitialImage(currentImage));
|
dispatch(setInitialImage(currentImage));
|
||||||
dispatch(setActiveTab('img2img'));
|
dispatch(setActiveTab('img2img'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyImageLink = () => {
|
const handleCopyImageLink = () => {
|
||||||
onCopy();
|
navigator.clipboard
|
||||||
|
.writeText(
|
||||||
|
currentImage ? window.location.toString() + currentImage.url : ''
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
toast({
|
toast({
|
||||||
title: 'Image Link Copied',
|
title: 'Image Link Copied',
|
||||||
status: 'success',
|
status: 'success',
|
||||||
duration: 2500,
|
duration: 2500,
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
});
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
@ -308,11 +318,27 @@ const CurrentImageButtons = () => {
|
|||||||
|
|
||||||
const handleSendToInpainting = () => {
|
const handleSendToInpainting = () => {
|
||||||
if (!currentImage) return;
|
if (!currentImage) return;
|
||||||
|
if (isLightBoxOpen) dispatch(setIsLightBoxOpen(false));
|
||||||
|
|
||||||
dispatch(setImageToInpaint(currentImage));
|
dispatch(setImageToInpaint(currentImage));
|
||||||
|
|
||||||
dispatch(setActiveTab('inpainting'));
|
dispatch(setActiveTab('inpainting'));
|
||||||
dispatch(setNeedsCache(true));
|
dispatch(setDoesCanvasNeedScaling(true));
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Sent to Inpainting',
|
||||||
|
status: 'success',
|
||||||
|
duration: 2500,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendToOutpainting = () => {
|
||||||
|
if (!currentImage) return;
|
||||||
|
if (isLightBoxOpen) dispatch(setIsLightBoxOpen(false));
|
||||||
|
|
||||||
|
dispatch(setImageToOutpaint(currentImage));
|
||||||
|
dispatch(setActiveTab('outpainting'));
|
||||||
|
dispatch(setDoesCanvasNeedScaling(true));
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Sent to Inpainting',
|
title: 'Sent to Inpainting',
|
||||||
@ -363,6 +389,13 @@ const CurrentImageButtons = () => {
|
|||||||
>
|
>
|
||||||
Send to Inpainting
|
Send to Inpainting
|
||||||
</IAIButton>
|
</IAIButton>
|
||||||
|
<IAIButton
|
||||||
|
size={'sm'}
|
||||||
|
onClick={handleSendToOutpainting}
|
||||||
|
leftIcon={<FaShare />}
|
||||||
|
>
|
||||||
|
Send to Outpainting
|
||||||
|
</IAIButton>
|
||||||
<IAIButton
|
<IAIButton
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
onClick={handleCopyImageLink}
|
onClick={handleCopyImageLink}
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { RootState, useAppSelector } from '../../app/store';
|
import { RootState, useAppSelector } from 'app/store';
|
||||||
import CurrentImageButtons from './CurrentImageButtons';
|
import CurrentImageButtons from './CurrentImageButtons';
|
||||||
import { MdPhoto } from 'react-icons/md';
|
import { MdPhoto } from 'react-icons/md';
|
||||||
import CurrentImagePreview from './CurrentImagePreview';
|
import CurrentImagePreview from './CurrentImagePreview';
|
||||||
import { GalleryState } from './gallerySlice';
|
import { GalleryState } from './gallerySlice';
|
||||||
import { OptionsState } from '../options/optionsSlice';
|
import { OptionsState } from 'features/options/optionsSlice';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { activeTabNameSelector } from '../options/optionsSelectors';
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
|
||||||
export const currentImageDisplaySelector = createSelector(
|
export const currentImageDisplaySelector = createSelector(
|
||||||
[
|
[
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { IconButton, Image, Spinner } from '@chakra-ui/react';
|
import { IconButton, Image, Spinner } from '@chakra-ui/react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
import { RootState, useAppDispatch, useAppSelector } from 'app/store';
|
||||||
import {
|
import {
|
||||||
GalleryCategory,
|
GalleryCategory,
|
||||||
GalleryState,
|
GalleryState,
|
||||||
@ -10,7 +10,7 @@ import {
|
|||||||
} from './gallerySlice';
|
} from './gallerySlice';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { OptionsState } from '../options/optionsSlice';
|
import { OptionsState, setIsLightBoxOpen } from 'features/options/optionsSlice';
|
||||||
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
||||||
|
|
||||||
export const imagesSelector = createSelector(
|
export const imagesSelector = createSelector(
|
||||||
@ -76,6 +76,10 @@ export default function CurrentImagePreview() {
|
|||||||
dispatch(selectNextImage());
|
dispatch(selectNextImage());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLightBox = () => {
|
||||||
|
dispatch(setIsLightBoxOpen(true));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'current-image-preview'}>
|
<div className={'current-image-preview'}>
|
||||||
{imageToDisplay && (
|
{imageToDisplay && (
|
||||||
@ -83,6 +87,7 @@ export default function CurrentImagePreview() {
|
|||||||
src={imageToDisplay.url}
|
src={imageToDisplay.url}
|
||||||
width={imageToDisplay.width}
|
width={imageToDisplay.width}
|
||||||
height={imageToDisplay.height}
|
height={imageToDisplay.height}
|
||||||
|
onClick={handleLightBox}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!shouldShowImageDetails && (
|
{!shouldShowImageDetails && (
|
||||||
|
@ -22,11 +22,11 @@ import {
|
|||||||
SyntheticEvent,
|
SyntheticEvent,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
import { deleteImage } from '../../app/socketio/actions';
|
import { deleteImage } from 'app/socketio/actions';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from 'app/store';
|
||||||
import { setShouldConfirmOnDelete, SystemState } from '../system/systemSlice';
|
import { setShouldConfirmOnDelete, SystemState } from 'features/system/systemSlice';
|
||||||
import * as InvokeAI from '../../app/invokeai';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
useToast,
|
useToast,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
import { setCurrentImage } from './gallerySlice';
|
import { setCurrentImage } from './gallerySlice';
|
||||||
import { FaCheck, FaTrashAlt } from 'react-icons/fa';
|
import { FaCheck, FaTrashAlt } from 'react-icons/fa';
|
||||||
import DeleteImageModal from './DeleteImageModal';
|
import DeleteImageModal from './DeleteImageModal';
|
||||||
@ -16,12 +16,16 @@ import {
|
|||||||
setAllImageToImageParameters,
|
setAllImageToImageParameters,
|
||||||
setAllTextToImageParameters,
|
setAllTextToImageParameters,
|
||||||
setInitialImage,
|
setInitialImage,
|
||||||
|
setIsLightBoxOpen,
|
||||||
setPrompt,
|
setPrompt,
|
||||||
setSeed,
|
setSeed,
|
||||||
} from '../options/optionsSlice';
|
} from 'features/options/optionsSlice';
|
||||||
import * as InvokeAI from '../../app/invokeai';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
import * as ContextMenu from '@radix-ui/react-context-menu';
|
import * as ContextMenu from '@radix-ui/react-context-menu';
|
||||||
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice';
|
import {
|
||||||
|
setImageToInpaint,
|
||||||
|
setImageToOutpaint,
|
||||||
|
} from 'features/canvas/canvasSlice';
|
||||||
import { hoverableImageSelector } from './gallerySliceSelectors';
|
import { hoverableImageSelector } from './gallerySliceSelectors';
|
||||||
|
|
||||||
interface HoverableImageProps {
|
interface HoverableImageProps {
|
||||||
@ -44,6 +48,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
galleryImageObjectFit,
|
galleryImageObjectFit,
|
||||||
galleryImageMinimumWidth,
|
galleryImageMinimumWidth,
|
||||||
mayDeleteImage,
|
mayDeleteImage,
|
||||||
|
isLightBoxOpen,
|
||||||
} = useAppSelector(hoverableImageSelector);
|
} = useAppSelector(hoverableImageSelector);
|
||||||
const { image, isSelected } = props;
|
const { image, isSelected } = props;
|
||||||
const { url, uuid, metadata } = image;
|
const { url, uuid, metadata } = image;
|
||||||
@ -77,6 +82,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSendToImageToImage = () => {
|
const handleSendToImageToImage = () => {
|
||||||
|
if (isLightBoxOpen) dispatch(setIsLightBoxOpen(false));
|
||||||
dispatch(setInitialImage(image));
|
dispatch(setInitialImage(image));
|
||||||
if (activeTabName !== 'img2img') {
|
if (activeTabName !== 'img2img') {
|
||||||
dispatch(setActiveTab('img2img'));
|
dispatch(setActiveTab('img2img'));
|
||||||
@ -90,6 +96,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSendToInpainting = () => {
|
const handleSendToInpainting = () => {
|
||||||
|
if (isLightBoxOpen) dispatch(setIsLightBoxOpen(false));
|
||||||
dispatch(setImageToInpaint(image));
|
dispatch(setImageToInpaint(image));
|
||||||
if (activeTabName !== 'inpainting') {
|
if (activeTabName !== 'inpainting') {
|
||||||
dispatch(setActiveTab('inpainting'));
|
dispatch(setActiveTab('inpainting'));
|
||||||
@ -102,6 +109,20 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSendToOutpainting = () => {
|
||||||
|
if (isLightBoxOpen) dispatch(setIsLightBoxOpen(false));
|
||||||
|
dispatch(setImageToOutpaint(image));
|
||||||
|
if (activeTabName !== 'outpainting') {
|
||||||
|
dispatch(setActiveTab('outpainting'));
|
||||||
|
}
|
||||||
|
toast({
|
||||||
|
title: 'Sent to Outpainting',
|
||||||
|
status: 'success',
|
||||||
|
duration: 2500,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleUseAllParameters = () => {
|
const handleUseAllParameters = () => {
|
||||||
metadata && dispatch(setAllTextToImageParameters(metadata));
|
metadata && dispatch(setAllTextToImageParameters(metadata));
|
||||||
toast({
|
toast({
|
||||||
@ -228,6 +249,9 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
<ContextMenu.Item onClickCapture={handleSendToInpainting}>
|
<ContextMenu.Item onClickCapture={handleSendToInpainting}>
|
||||||
Send to Inpainting
|
Send to Inpainting
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Item onClickCapture={handleSendToOutpainting}>
|
||||||
|
Send to Outpainting
|
||||||
|
</ContextMenu.Item>
|
||||||
<DeleteImageModal image={image}>
|
<DeleteImageModal image={image}>
|
||||||
<ContextMenu.Item data-warning>Delete Image</ContextMenu.Item>
|
<ContextMenu.Item data-warning>Delete Image</ContextMenu.Item>
|
||||||
</DeleteImageModal>
|
</DeleteImageModal>
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.image-gallery-popup {
|
.image-gallery-popup {
|
||||||
background-color: var(--tab-color);
|
background-color: var(--background-color-secondary);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -55,16 +55,16 @@
|
|||||||
column-gap: 0.5rem;
|
column-gap: 0.5rem;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
div {
|
.image-gallery-header-right-icons {
|
||||||
display: flex;
|
display: flex;
|
||||||
column-gap: 0.5rem;
|
flex-direction: row;
|
||||||
column-gap: 0.5rem;
|
column-gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-gallery-icon-btn {
|
.image-gallery-icon-btn {
|
||||||
background-color: var(--btn-load-more) !important;
|
background-color: var(--btn-load-more);
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--btn-load-more-hover) !important;
|
background-color: var(--btn-load-more-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +96,8 @@
|
|||||||
.image-gallery-container-placeholder {
|
.image-gallery-container-placeholder {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: var(--background-color-secondary);
|
row-gap: 0.5rem;
|
||||||
|
background-color: var(--background-color);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
@ -108,26 +109,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 5rem;
|
width: 4rem;
|
||||||
height: 5rem;
|
height: 4rem;
|
||||||
color: var(--svg-color);
|
color: var(--svg-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-gallery-load-more-btn {
|
.image-gallery-load-more-btn {
|
||||||
background-color: var(--btn-load-more) !important;
|
background-color: var(--btn-load-more);
|
||||||
font-size: 0.85rem !important;
|
font-size: 0.85rem;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--btn-load-more) !important;
|
background-color: var(--btn-load-more);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--btn-load-more-hover) !important;
|
background-color: var(--btn-load-more-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -135,11 +136,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.image-gallery-category-btn-group {
|
.image-gallery-category-btn-group {
|
||||||
width: 100% !important;
|
width: max-content;
|
||||||
column-gap: 0 !important;
|
column-gap: 0;
|
||||||
justify-content: stretch !important;
|
justify-content: stretch;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
background-color: var(--btn-base-color);
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--btn-base-color-hover);
|
||||||
|
}
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
&[data-selected='true'] {
|
&[data-selected='true'] {
|
||||||
background-color: var(--accent-color);
|
background-color: var(--accent-color);
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user