Add inpainting and outpainting to Imaginate (#864)

* Do not select layer immediatly on drag

* Add LayerReferenceInput MVP widget

* Properties Panel

* Fix dragging marker flicker

* Change mask shape to outline

* Add mask rendering

* Simplify select code

* Remove colours

* Fix inpaint/outpaint and rearrage widget UX

* Add mask blur and mask starting fill parameters

* Guard for the case when the layer is missing

* Add icon to LayerReferenceInput to finalize its UI

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube 2022-11-21 07:00:38 +00:00 committed by Keavon Chambers
parent 5bf7b9fdf8
commit 9d80defa14
26 changed files with 1211 additions and 544 deletions

View file

@ -8,7 +8,7 @@ use crate::messages::prelude::*;
use crate::messages::tool::utility_types::HintData;
use graphene::color::Color;
use graphene::layers::imaginate_layer::{ImaginateBaseImage, ImaginateGenerationParameters};
use graphene::layers::imaginate_layer::{ImaginateBaseImage, ImaginateGenerationParameters, ImaginateMaskFillContent, ImaginateMaskPaintMode};
use graphene::layers::text_layer::Font;
use graphene::LayerId;
@ -60,6 +60,14 @@ pub enum FrontendMessage {
parameters: ImaginateGenerationParameters,
#[serde(rename = "baseImage")]
base_image: Option<ImaginateBaseImage>,
#[serde(rename = "maskImage")]
mask_image: Option<ImaginateBaseImage>,
#[serde(rename = "maskPaintMode")]
mask_paint_mode: ImaginateMaskPaintMode,
#[serde(rename = "maskBlurPx")]
mask_blur_px: u32,
#[serde(rename = "maskFillContent")]
mask_fill_content: ImaginateMaskFillContent,
hostname: String,
#[serde(rename = "refreshFrequency")]
refresh_frequency: f64,

View file

@ -6,8 +6,10 @@ use crate::messages::prelude::*;
use graphene::color::Color;
use graphene::layers::text_layer::Font;
use graphene::LayerId;
use serde_json::Value;
use std::ops::Not;
#[derive(Debug, Clone, Default)]
pub struct LayoutMessageHandler {
@ -117,6 +119,19 @@ impl<F: Fn(&MessageDiscriminant) -> Vec<KeysGroup>> MessageHandler<LayoutMessage
let callback_message = (invisible.on_update.callback)(&());
responses.push_back(callback_message);
}
Widget::LayerReferenceInput(layer_reference_input) => {
let update_value = value.is_null().not().then(|| {
value
.as_str()
.expect("LayerReferenceInput update was not of type: string")
.split(',')
.map(|id| id.parse::<LayerId>().unwrap())
.collect::<Vec<_>>()
});
layer_reference_input.value = update_value;
let callback_message = (layer_reference_input.on_update.callback)(layer_reference_input);
responses.push_back(callback_message);
}
Widget::NumberInput(number_input) => match value {
Value::Number(num) => {
let update_value = num.as_f64().unwrap();

View file

@ -61,11 +61,12 @@ impl Layout {
let mut tooltip_shortcut = match &mut widget_holder.widget {
Widget::CheckboxInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::ColorInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::IconButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::OptionalInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::DropdownInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::FontInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::IconButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::LayerReferenceInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::NumberInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::OptionalInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::PopoverButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::IconLabel(_)
@ -290,6 +291,7 @@ pub enum Widget {
IconButton(IconButton),
IconLabel(IconLabel),
InvisibleStandinInput(InvisibleStandinInput),
LayerReferenceInput(LayerReferenceInput),
NumberInput(NumberInput),
OptionalInput(OptionalInput),
PivotAssist(PivotAssist),

View file

@ -1,7 +1,7 @@
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
use crate::messages::layout::utility_types::layout_widget::WidgetCallback;
use graphene::color::Color;
use graphene::{color::Color, layers::layer_info::LayerDataTypeDiscriminant, LayerId};
use derivative::*;
use serde::{Deserialize, Serialize};
@ -150,6 +150,34 @@ pub struct InvisibleStandinInput {
pub on_update: WidgetCallback<()>,
}
#[derive(Clone, Serialize, Deserialize, Derivative)]
#[derivative(Debug, PartialEq, Default)]
pub struct LayerReferenceInput {
pub value: Option<Vec<LayerId>>,
#[serde(rename = "layerName")]
pub layer_name: Option<String>,
#[serde(rename = "layerType")]
pub layer_type: Option<LayerDataTypeDiscriminant>,
pub disabled: bool,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Styling
#[serde(rename = "minWidth")]
pub min_width: u32,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<LayerReferenceInput>,
}
#[derive(Clone, Serialize, Deserialize, Derivative)]
#[derivative(Debug, PartialEq, Default)]
pub struct NumberInput {

View file

@ -963,23 +963,55 @@ impl DocumentMessageHandler {
restore_faces: imaginate_layer.restore_faces,
tiling: imaginate_layer.tiling,
};
let base_image = if imaginate_layer.use_img2img {
let mask_paint_mode = imaginate_layer.mask_paint_mode;
let mask_blur_px = imaginate_layer.mask_blur_px;
let mask_fill_content = imaginate_layer.mask_fill_content;
let (base_image, mask_image) = if imaginate_layer.use_img2img {
let mask = imaginate_layer.mask_layer_ref.clone();
// Calculate the size of the region to be exported
let size = DVec2::new(transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length());
let old_transforms = self.remove_document_transform();
let svg = self.render_document(size, transform.inverse(), persistent_data, DocumentRenderMode::OnlyBelowLayerInFolder(&layer_path));
self.restore_document_transform(old_transforms);
let mask_image = mask.and_then(|mask_layer_path| match self.graphene_document.layer(&mask_layer_path) {
Ok(_) => {
let svg = self.render_document(size, transform.inverse(), persistent_data, DocumentRenderMode::LayerCutout(&mask_layer_path, Color::WHITE));
Some(ImaginateBaseImage { svg, size })
}
Err(_) => None,
});
if mask_image.is_none() {
return Some(
DialogMessage::DisplayDialogError {
title: "Masking layer is missing".into(),
description: "
It may have been deleted or moved. Please drag a new layer reference\n\
into the 'Masking Layer' parameter input, then generate again."
.trim()
.into(),
}
.into(),
);
}
self.restore_document_transform(old_transforms);
(Some(ImaginateBaseImage { svg, size }), mask_image)
} else {
None
(None, None)
};
Some(
FrontendMessage::TriggerImaginateGenerate {
parameters,
base_image,
mask_image,
mask_paint_mode,
mask_blur_px,
mask_fill_content,
hostname: preferences.imaginate_server_hostname.clone(),
refresh_frequency: preferences.imaginate_refresh_frequency,
document_id,
@ -1042,13 +1074,17 @@ impl DocumentMessageHandler {
let render_data = RenderData::new(ViewMode::Normal, &persistent_data.font_cache, None);
let artwork = match render_mode {
DocumentRenderMode::Root => self.graphene_document.render_root(render_data),
DocumentRenderMode::OnlyBelowLayerInFolder(below_layer_path) => self.graphene_document.render_layers_below(below_layer_path, render_data).unwrap(),
let (artwork, outside) = match render_mode {
DocumentRenderMode::Root => (self.graphene_document.render_root(render_data), None),
DocumentRenderMode::OnlyBelowLayerInFolder(below_layer_path) => (self.graphene_document.render_layers_below(below_layer_path, render_data).unwrap(), None),
DocumentRenderMode::LayerCutout(layer_path, background) => (self.graphene_document.render_layer(layer_path, render_data).unwrap(), Some(background)),
};
let artboards = self.artboard_message_handler.artboards_graphene_document.render_root(render_data);
let outside_artboards_color = if self.artboard_message_handler.artboard_ids.is_empty() { "#ffffff" } else { "#222222" };
let outside_artboards = format!(r#"<rect x="0" y="0" width="100%" height="100%" fill="{}" />"#, outside_artboards_color);
let outside_artboards_color = outside.map_or_else(
|| if self.artboard_message_handler.artboard_ids.is_empty() { "ffffff" } else { "222222" }.to_string(),
|col| col.rgba_hex(),
);
let outside_artboards = format!(r##"<rect x="0" y="0" width="100%" height="100%" fill="#{}" />"##, outside_artboards_color);
let matrix = transform
.to_cols_array()
.iter()

View file

@ -3,7 +3,7 @@ use crate::messages::layout::utility_types::widgets::assist_widgets::PivotPositi
use crate::messages::portfolio::document::utility_types::misc::TargetDocument;
use crate::messages::prelude::*;
use graphene::layers::imaginate_layer::ImaginateSamplingMethod;
use graphene::layers::imaginate_layer::{ImaginateMaskFillContent, ImaginateMaskPaintMode, ImaginateSamplingMethod};
use graphene::layers::style::{Fill, Stroke};
use graphene::LayerId;
@ -29,6 +29,10 @@ pub enum PropertiesPanelMessage {
SetActiveLayers { paths: Vec<Vec<LayerId>>, document: TargetDocument },
SetImaginateCfgScale { cfg_scale: f64 },
SetImaginateDenoisingStrength { denoising_strength: f64 },
SetImaginateLayerPath { layer_path: Option<Vec<LayerId>> },
SetImaginateMaskBlurPx { mask_blur_px: u32 },
SetImaginateMaskFillContent { mode: ImaginateMaskFillContent },
SetImaginateMaskPaintMode { paint: ImaginateMaskPaintMode },
SetImaginateNegativePrompt { negative_prompt: String },
SetImaginatePrompt { prompt: String },
SetImaginateRestoreFaces { restore_faces: bool },

View file

@ -140,10 +140,11 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
}
ResendActiveProperties => {
if let Some((path, target_document)) = self.active_selection.clone() {
let layer = get_document(target_document).layer(&path).unwrap();
let document = get_document(target_document);
let layer = document.layer(&path).unwrap();
match target_document {
TargetDocument::Artboard => register_artboard_layer_properties(layer, responses, persistent_data),
TargetDocument::Artwork => register_artwork_layer_properties(path, layer, responses, persistent_data, node_graph_message_handler),
TargetDocument::Artwork => register_artwork_layer_properties(document, path, layer, responses, persistent_data, node_graph_message_handler),
}
}
}
@ -166,6 +167,22 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetDenoisingStrength { path, denoising_strength }.into());
}
SetImaginateLayerPath { layer_path } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetLayerPath { path, layer_path }.into());
}
SetImaginateMaskBlurPx { mask_blur_px } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetMaskBlurPx { path, mask_blur_px }.into());
}
SetImaginateMaskFillContent { mode } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetMaskFillContent { path, mode }.into());
}
SetImaginateMaskPaintMode { paint } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetMaskPaintMode { path, paint }.into());
}
SetImaginateSamples { samples } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetSamples { path, samples }.into());

View file

@ -5,15 +5,15 @@ use crate::messages::layout::utility_types::misc::LayoutTarget;
use crate::messages::layout::utility_types::widgets::assist_widgets::PivotAssist;
use crate::messages::layout::utility_types::widgets::button_widgets::{IconButton, PopoverButton, TextButton};
use crate::messages::layout::utility_types::widgets::input_widgets::{
CheckboxInput, ColorInput, DropdownEntryData, DropdownInput, FontInput, NumberInput, NumberInputMode, RadioEntryData, RadioInput, TextAreaInput, TextInput,
CheckboxInput, ColorInput, DropdownEntryData, DropdownInput, FontInput, LayerReferenceInput, NumberInput, NumberInputMode, RadioEntryData, RadioInput, TextAreaInput, TextInput,
};
use crate::messages::layout::utility_types::widgets::label_widgets::{IconLabel, Separator, SeparatorDirection, SeparatorType, TextLabel};
use crate::messages::portfolio::utility_types::{ImaginateServerStatus, PersistentData};
use crate::messages::prelude::*;
use graphene::color::Color;
use graphene::document::pick_layer_safe_imaginate_resolution;
use graphene::layers::imaginate_layer::{ImaginateLayer, ImaginateSamplingMethod, ImaginateStatus};
use graphene::document::{pick_layer_safe_imaginate_resolution, Document};
use graphene::layers::imaginate_layer::{ImaginateLayer, ImaginateMaskFillContent, ImaginateMaskPaintMode, ImaginateSamplingMethod, ImaginateStatus};
use graphene::layers::layer_info::{Layer, LayerDataType, LayerDataTypeDiscriminant};
use graphene::layers::nodegraph_layer::NodeGraphFrameLayer;
use graphene::layers::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke};
@ -224,6 +224,7 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ
}
pub fn register_artwork_layer_properties(
document: &Document,
layer_path: Vec<graphene::LayerId>,
layer: &Layer,
responses: &mut VecDeque<Message>,
@ -320,7 +321,10 @@ pub fn register_artwork_layer_properties(
vec![node_section_transform(layer, persistent_data)]
}
LayerDataType::Imaginate(imaginate) => {
vec![node_section_transform(layer, persistent_data), node_section_imaginate(imaginate, layer, persistent_data, responses)]
vec![
node_section_transform(layer, persistent_data),
node_section_imaginate(imaginate, layer, document, persistent_data, responses),
]
}
LayerDataType::NodeGraphFrame(node_graph_frame) => {
let is_graph_open = node_graph_message_handler.layer_path.as_ref().filter(|node_graph| *node_graph == &layer_path).is_some();
@ -528,10 +532,19 @@ fn node_section_transform(layer: &Layer, persistent_data: &PersistentData) -> La
}
}
fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persistent_data: &PersistentData, responses: &mut VecDeque<Message>) -> LayoutGroup {
LayoutGroup::Section {
name: "Imaginate".into(),
layout: vec![
fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, document: &Document, persistent_data: &PersistentData, responses: &mut VecDeque<Message>) -> LayoutGroup {
let layer_reference_input_layer = imaginate_layer
.mask_layer_ref
.as_ref()
.and_then(|path| document.layer(path).ok())
.map(|layer| (layer.name.clone().unwrap_or_default(), LayerDataTypeDiscriminant::from(&layer.data)));
let layer_reference_input_layer_is_some = layer_reference_input_layer.is_some();
let layer_reference_input_layer_name = layer_reference_input_layer.as_ref().map(|(layer_name, _)| layer_name);
let layer_reference_input_layer_type = layer_reference_input_layer.as_ref().map(|(_, layer_type)| layer_type);
let mut layout = vec![
LayoutGroup::Row {
widgets: {
let tooltip = "Connection status to the server that computes generated images".to_string();
@ -605,10 +618,7 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi
value: {
// Since we don't serialize the status, we need to derive from other state whether the Idle state is actually supposed to be the Terminated state
let mut interpreted_status = imaginate_layer.status.clone();
if imaginate_layer.status == ImaginateStatus::Idle
&& imaginate_layer.blob_url.is_some()
&& imaginate_layer.percent_complete > 0.
&& imaginate_layer.percent_complete < 100.
if imaginate_layer.status == ImaginateStatus::Idle && imaginate_layer.blob_url.is_some() && imaginate_layer.percent_complete > 0. && imaginate_layer.percent_complete < 100.
{
interpreted_status = ImaginateStatus::Terminated;
}
@ -669,7 +679,7 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi
WidgetHolder::new(Widget::IconButton(IconButton {
size: 24,
icon: "Random".into(),
tooltip: "Generate with a random seed".into(),
tooltip: "Generate with a new random seed".into(),
on_update: WidgetCallback::new(|_| PropertiesPanelMessage::SetImaginateSeedRandomizeAndGenerate.into()),
..Default::default()
})),
@ -850,71 +860,6 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi
]
},
},
LayoutGroup::Row {
widgets: {
let tooltip = "Generate an image based upon the artwork beneath this frame in the containing folder".to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Use Base Image".into(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::CheckboxInput(CheckboxInput {
checked: imaginate_layer.use_img2img,
tooltip,
on_update: WidgetCallback::new(move |checkbox_input: &CheckboxInput| PropertiesPanelMessage::SetImaginateUseImg2Img { use_img2img: checkbox_input.checked }.into()),
..Default::default()
})),
]
},
},
LayoutGroup::Row {
widgets: {
let tooltip = "
Strength of the artistic liberties allowing changes from the base image. The image is unchanged at 0% and completely different at 100%.\n\
\n\
This parameter is otherwise known as denoising strength.
"
.trim()
.to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Image Creativity".into(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(imaginate_layer.denoising_strength * 100.),
unit: "%".into(),
mode: NumberInputMode::Range,
range_min: Some(0.),
range_max: Some(100.),
min: Some(0.),
max: Some(100.),
display_decimal_places: 2,
disabled: !imaginate_layer.use_img2img,
tooltip,
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
PropertiesPanelMessage::SetImaginateDenoisingStrength {
denoising_strength: number_input.value.unwrap() / 100.,
}
.into()
}),
..Default::default()
})),
]
},
},
LayoutGroup::Row {
widgets: {
let tooltip = "
@ -922,14 +867,14 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi
\n\
Lower values are more creative and exploratory. Higher values are more literal and uninspired, but may be lower quality.\n\
\n\
This parameter is otherwise known as CFG (classifier-free guidance) scale.
This parameter is otherwise known as CFG (classifier-free guidance).
"
.trim()
.to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Text Literalness".into(),
value: "Text Guidance".into(),
tooltip: tooltip.to_string(),
..Default::default()
})),
@ -1013,7 +958,229 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi
},
LayoutGroup::Row {
widgets: {
let tooltip = "Postprocess human (or human-like) faces to look subtly less distorted".to_string();
let tooltip = "Generate an image based upon the artwork beneath this frame in the containing folder".to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Use Base Image".into(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::CheckboxInput(CheckboxInput {
checked: imaginate_layer.use_img2img,
tooltip,
on_update: WidgetCallback::new(move |checkbox_input: &CheckboxInput| PropertiesPanelMessage::SetImaginateUseImg2Img { use_img2img: checkbox_input.checked }.into()),
..Default::default()
})),
]
},
},
LayoutGroup::Row {
widgets: {
let tooltip = "
Strength of the artistic liberties allowing changes from the base image. The image is unchanged at 0% and completely different at 100%.\n\
\n\
This parameter is otherwise known as denoising strength.
"
.trim()
.to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Image Creativity".into(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(imaginate_layer.denoising_strength * 100.),
unit: "%".into(),
mode: NumberInputMode::Range,
range_min: Some(0.),
range_max: Some(100.),
min: Some(0.),
max: Some(100.),
display_decimal_places: 2,
disabled: !imaginate_layer.use_img2img,
tooltip,
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
PropertiesPanelMessage::SetImaginateDenoisingStrength {
denoising_strength: number_input.value.unwrap() / 100.,
}
.into()
}),
..Default::default()
})),
]
},
},
LayoutGroup::Row {
widgets: {
let tooltip = "
Reference to a layer or folder which masks parts of the base image. Image generation is constrained to masked areas.\n\
\n\
Black shapes represent the masked regions. Lighter shades of gray act as a partial mask, and colors become grayscale.
"
.trim()
.to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Masking Layer".into(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::LayerReferenceInput(LayerReferenceInput {
value: imaginate_layer.mask_layer_ref.clone(),
tooltip,
layer_name: layer_reference_input_layer_name.cloned(),
layer_type: layer_reference_input_layer_type.cloned(),
disabled: !imaginate_layer.use_img2img,
on_update: WidgetCallback::new(move |val: &LayerReferenceInput| PropertiesPanelMessage::SetImaginateLayerPath { layer_path: val.value.clone() }.into()),
..Default::default()
})),
]
},
},
];
if imaginate_layer.use_img2img && imaginate_layer.mask_layer_ref.is_some() && layer_reference_input_layer_is_some {
layout.extend(vec![
LayoutGroup::Row {
widgets: {
let tooltip = "
Constrain image generation to the interior (inpaint) or exterior (outpaint) of the mask, while referencing the other unchanged parts as context imagery.\n\
\n\
An unwanted part of an image can be replaced by drawing around it with a black shape and inpainting with that mask layer.\n\
\n\
An image can be uncropped by resizing the Imaginate layer to the target bounds and outpainting with a black rectangle mask matching the original image bounds.
"
.trim()
.to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Mask Direction".to_string(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::RadioInput(RadioInput {
entries: [(ImaginateMaskPaintMode::Inpaint, "Inpaint"), (ImaginateMaskPaintMode::Outpaint, "Outpaint")]
.into_iter()
.map(|(paint, name)| RadioEntryData {
label: name.to_string(),
on_update: WidgetCallback::new(move |_| PropertiesPanelMessage::SetImaginateMaskPaintMode { paint }.into()),
tooltip: tooltip.clone(),
..Default::default()
})
.collect(),
selected_index: imaginate_layer.mask_paint_mode as u32,
..Default::default()
})),
]
},
},
LayoutGroup::Row {
widgets: {
let tooltip = "Blur radius for the mask. Useful for softening sharp edges to blend the masked area with the rest of the image.".to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Mask Blur".to_string(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(imaginate_layer.mask_blur_px as f64),
unit: " px".into(),
mode: NumberInputMode::Range,
range_min: Some(0.),
range_max: Some(25.),
min: Some(0.),
is_integer: true,
tooltip,
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
PropertiesPanelMessage::SetImaginateMaskBlurPx {
mask_blur_px: number_input.value.unwrap() as u32,
}
.into()
}),
..Default::default()
})),
]
},
},
LayoutGroup::Row {
widgets: {
let tooltip = "
Begin in/outpainting the masked areas using this fill content as the starting base image.\n\
\n\
Each option can be visualized by generating with 'Sampling Steps' set to 0.
"
.trim()
.to_string();
let mask_fill_content_modes = ImaginateMaskFillContent::list();
let mut entries = Vec::with_capacity(mask_fill_content_modes.len());
for mode in mask_fill_content_modes {
entries.push(DropdownEntryData {
label: mode.to_string(),
on_update: WidgetCallback::new(move |_| PropertiesPanelMessage::SetImaginateMaskFillContent { mode }.into()),
..DropdownEntryData::default()
});
}
let entries = vec![entries];
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Mask Starting Fill".to_string(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::DropdownInput(DropdownInput {
entries,
selected_index: Some(imaginate_layer.mask_fill_content as u32),
tooltip,
..Default::default()
})),
]
},
},
]);
}
layout.extend(vec![
LayoutGroup::Row {
widgets: {
let tooltip = "
Postprocess human (or human-like) faces to look subtly less distorted.\n\
\n\
This filter can be used on its own by enabling 'Use Base Image' and setting 'Sampling Steps' to 0.
"
.to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
@ -1062,8 +1229,9 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi
]
},
},
],
}
]);
LayoutGroup::Section { name: "Imaginate".into(), layout }
}
fn node_section_node_graph_frame(layer_path: Vec<graphene::LayerId>, node_graph_frame: &NodeGraphFrameLayer, open_graph: bool) -> LayoutGroup {

View file

@ -61,12 +61,12 @@ impl LayerPanelEntry {
pub fn new(layer_metadata: &LayerMetadata, transform: DAffine2, layer: &Layer, path: Vec<LayerId>, font_cache: &FontCache) -> Self {
let name = layer.name.clone().unwrap_or_else(|| String::from(""));
let tooltip = if cfg!(debug_assertions) {
let joined = &path.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(" / ");
name.clone() + "\nLayer Path: " + joined.as_str()
} else {
name.clone()
};
let mut tooltip = name.clone();
if cfg!(debug_assertions) {
tooltip += "\nLayer Path: ";
tooltip += &path.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(" / ");
tooltip = tooltip.trim().to_string();
}
let arr = layer.data.bounding_box(transform, font_cache).unwrap_or([DVec2::ZERO, DVec2::ZERO]);
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();

View file

@ -1,5 +1,6 @@
pub use super::layer_panel::{LayerMetadata, LayerPanelEntry};
use graphene::color::Color;
use graphene::document::Document as GrapheneDocument;
use graphene::LayerId;
@ -65,4 +66,5 @@ impl DocumentMode {
pub enum DocumentRenderMode<'a> {
Root,
OnlyBelowLayerInFolder(&'a [LayerId]),
LayerCutout(&'a [LayerId], Color),
}

View file

@ -233,6 +233,7 @@ img {
import { defineComponent } from "vue";
import { createClipboardManager } from "@/io-managers/clipboard";
import { createDragManager } from "@/io-managers/drag";
import { createHyperlinkManager } from "@/io-managers/hyperlinks";
import { createInputManager } from "@/io-managers/input";
import { createLocalizationManager } from "@/io-managers/localization";
@ -252,6 +253,7 @@ import MainWindow from "@/components/window/MainWindow.vue";
const managerDestructors: {
createClipboardManager?: () => void;
createDragManager?: () => void;
createHyperlinkManager?: () => void;
createInputManager?: () => void;
createLocalizationManager?: () => void;
@ -302,6 +304,7 @@ export default defineComponent({
// Initialize managers, which are isolated systems that subscribe to backend messages to link them to browser API functionality (like JS events, IndexedDB, etc.)
Object.assign(managerDestructors, {
createClipboardManager: createClipboardManager(this.editor),
createDragManager: createDragManager(),
createHyperlinkManager: createHyperlinkManager(this.editor),
createInputManager: createInputManager(this.editor, this.$el.parentElement, this.dialog, this.portfolio, this.fullscreen),
createLocalizationManager: createLocalizationManager(this.editor),

View file

@ -1,5 +1,5 @@
<template>
<LayoutCol class="layer-tree">
<LayoutCol class="layer-tree" @dragleave="dragInPanel = false">
<LayoutRow class="options-bar" :scrollableX="true">
<WidgetLayout :layout="layerTreeOptionsLayout" />
</LayoutRow>
@ -31,7 +31,8 @@
></button>
<LayoutRow
class="layer"
:class="{ selected: listing.entry.layerMetadata.selected }"
:class="{ selected: fakeHighlight ? fakeHighlight.includes(listing.entry.path) : listing.entry.layerMetadata.selected }"
:data-layer="String(listing.entry.path)"
:data-index="index"
:title="listing.entry.tooltip"
:draggable="draggable"
@ -65,7 +66,11 @@
</LayoutRow>
</LayoutRow>
</LayoutCol>
<div class="insert-mark" v-if="draggingData && !draggingData.highlightFolder" :style="{ left: markIndent(draggingData.insertFolder), top: markTopOffset(draggingData.markerHeight) }"></div>
<div
class="insert-mark"
v-if="draggingData && !draggingData.highlightFolder && dragInPanel"
:style="{ left: markIndent(draggingData.insertFolder), top: markTopOffset(draggingData.markerHeight) }"
></div>
</LayoutRow>
</LayoutCol>
</template>
@ -258,6 +263,7 @@
margin-top: -2px;
height: 5px;
z-index: 1;
pointer-events: none;
}
}
}
@ -266,6 +272,7 @@
<script lang="ts">
import { defineComponent, nextTick } from "vue";
import { beginDraggingElement } from "@/io-managers/drag";
import { platformIsMac } from "@/utility-functions/platform";
import {
type LayerType,
@ -291,7 +298,7 @@ const LAYER_INDENT = 16;
const INSERT_MARK_MARGIN_LEFT = 4 + 32 + LAYER_INDENT;
const INSERT_MARK_OFFSET = 2;
type DraggingData = { insertFolder: BigUint64Array; insertIndex: number; highlightFolder: boolean; markerHeight: number };
type DraggingData = { select?: () => void; insertFolder: BigUint64Array; insertIndex: number; highlightFolder: boolean; markerHeight: number };
export default defineComponent({
inject: ["editor"],
@ -304,6 +311,8 @@ export default defineComponent({
// Interactive dragging
draggable: true,
draggingData: undefined as undefined | DraggingData,
fakeHighlight: undefined as undefined | BigUint64Array[],
dragInPanel: false,
// Layouts
layerTreeOptionsLayout: defaultWidgetLayout(),
@ -371,7 +380,7 @@ export default defineComponent({
async deselectAllLayers() {
this.editor.instance.deselectAllLayers();
},
calculateDragIndex(tree: HTMLDivElement, clientY: number): DraggingData {
calculateDragIndex(tree: HTMLDivElement, clientY: number, select?: () => void): DraggingData {
const treeChildren = tree.children;
const treeOffset = tree.getBoundingClientRect().top;
@ -430,11 +439,21 @@ export default defineComponent({
markerHeight -= treeOffset;
return { insertFolder, insertIndex, highlightFolder, markerHeight };
return { select, insertFolder, insertIndex, highlightFolder, markerHeight };
},
async dragStart(event: DragEvent, listing: LayerListingInfo) {
const layer = listing.entry;
if (!layer.layerMetadata.selected) this.selectLayer(event.ctrlKey, event.metaKey, event.shiftKey, listing, event);
this.dragInPanel = true;
if (!layer.layerMetadata.selected) {
this.fakeHighlight = [layer.path];
}
const select = (): void => {
if (!layer.layerMetadata.selected) this.selectLayer(false, false, false, listing, event);
};
const target = (event.target || undefined) as HTMLElement | undefined;
const draggingELement = (target?.closest("[data-layer]") || undefined) as HTMLElement | undefined;
if (draggingELement) beginDraggingElement(draggingELement);
// Set style of cursor for drag
if (event.dataTransfer) {
@ -443,23 +462,26 @@ export default defineComponent({
}
const tree: HTMLDivElement | undefined = (this.$refs.list as typeof LayoutCol | undefined)?.$el;
if (tree) this.draggingData = this.calculateDragIndex(tree, event.clientY);
if (tree) this.draggingData = this.calculateDragIndex(tree, event.clientY, select);
},
updateInsertLine(event: DragEvent) {
// Stop the drag from being shown as cancelled
event.preventDefault();
this.dragInPanel = true;
const tree: HTMLDivElement | undefined = (this.$refs.list as typeof LayoutCol | undefined)?.$el;
if (tree) this.draggingData = this.calculateDragIndex(tree, event.clientY);
if (tree) this.draggingData = this.calculateDragIndex(tree, event.clientY, this.draggingData?.select);
},
async drop() {
if (this.draggingData) {
const { insertFolder, insertIndex } = this.draggingData;
if (this.draggingData && this.dragInPanel) {
const { select, insertFolder, insertIndex } = this.draggingData;
select?.();
this.editor.instance.moveLayerInTree(insertFolder, insertIndex);
this.draggingData = undefined;
}
this.draggingData = undefined;
this.fakeHighlight = undefined;
this.dragInPanel = false;
},
rebuildLayerTree(updateDocumentLayerTreeStructure: UpdateDocumentLayerTreeStructure) {
const layerWithNameBeingEdited = this.layers.find((layer: LayerListingInfo) => layer.editingName);
@ -494,7 +516,7 @@ export default defineComponent({
recurse(updateDocumentLayerTreeStructure, this.layers, this.layerCache);
},
layerTypeData(layerType: LayerType): LayerTypeData {
return layerTypeData(layerType) || { name: "Error", icon: "NodeText" };
return layerTypeData(layerType) || { name: "Error", icon: "Info" };
},
},
mounted() {

View file

@ -28,6 +28,7 @@
/>
<IconButton v-if="component.props.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widgetId, undefined)" :sharpRightCorners="nextIsSuffix" />
<IconLabel v-if="component.props.kind === 'IconLabel'" v-bind="component.props" />
<LayerReferenceInput v-if="component.props.kind === 'LayerReferenceInput'" v-bind="component.props" @update:value="(value: BigUint64Array) => updateLayout(component.widgetId, value)" />
<NumberInput
v-if="component.props.kind === 'NumberInput'"
v-bind="component.props"
@ -109,6 +110,7 @@ import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
import ColorInput from "@/components/widgets/inputs/ColorInput.vue";
import DropdownInput from "@/components/widgets/inputs/DropdownInput.vue";
import FontInput from "@/components/widgets/inputs/FontInput.vue";
import LayerReferenceInput from "@/components/widgets/inputs/LayerReferenceInput.vue";
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
@ -170,6 +172,7 @@ export default defineComponent({
FontInput,
IconButton,
IconLabel,
LayerReferenceInput,
NumberInput,
OptionalInput,
PivotAssist,

View file

@ -0,0 +1,160 @@
<template>
<LayoutRow
class="layer-reference-input"
:class="{ disabled, droppable, 'sharp-right-corners': sharpRightCorners }"
:title="tooltip"
@dragover="(e: DragEvent) => !disabled && dragOver(e)"
@dragleave="() => !disabled && dragLeave()"
@drop="(e: DragEvent) => !disabled && drop(e)"
>
<template v-if="value === undefined || droppable">
<LayoutRow class="drop-zone"></LayoutRow>
<TextLabel :italic="true">{{ droppable ? "Drop" : "Drag" }} Layer Here</TextLabel>
</template>
<template v-if="value !== undefined && !droppable">
<IconLabel v-if="layerName !== undefined && layerType" :icon="layerTypeData(layerType).icon" class="layer-icon" />
<TextLabel v-if="layerName !== undefined && layerType" :italic="layerName === ''" class="layer-name">{{ layerName || layerTypeData(layerType).name }}</TextLabel>
<TextLabel :bold="true" :italic="true" v-else class="missing">Layer Missing</TextLabel>
</template>
<IconButton v-if="value !== undefined && !droppable" :icon="'CloseX'" :size="16" :disabled="disabled" :action="() => clearLayer()" />
</LayoutRow>
</template>
<style lang="scss">
.layer-reference-input {
position: relative;
flex: 1 0 auto;
height: 24px;
border-radius: 2px;
background: var(--color-1-nearblack);
.drop-zone {
pointer-events: none;
border: 1px dashed var(--color-5-dullgray);
border-radius: 1px;
position: absolute;
top: 2px;
bottom: 2px;
left: 2px;
right: 2px;
}
&.droppable .drop-zone {
border: 1px dashed var(--color-e-nearwhite);
}
.layer-icon {
margin: 4px 8px;
+ .text-label {
padding-left: 0;
}
}
.text-label {
line-height: 18px;
padding: 3px calc(8px + 2px);
width: 100%;
text-align: center;
&.missing {
color: var(--color-data-unused1);
}
&.layer-name {
text-align: left;
}
}
.icon-button {
margin: 4px;
margin-left: 0;
}
&.disabled {
background: var(--color-2-mildblack);
.drop-zone {
border: 1px dashed var(--color-4-dimgray);
}
.text-label {
color: var(--color-8-uppergray);
}
.icon-label svg {
fill: var(--color-8-uppergray);
}
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { currentDraggingElement } from "@/io-managers/drag";
import type { LayerType, LayerTypeData } from "@/wasm-communication/messages";
import { layerTypeData } from "@/wasm-communication/messages";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
export default defineComponent({
emits: ["update:value"],
props: {
value: { type: String as PropType<string | undefined>, required: false },
layerName: { type: String as PropType<string | undefined>, required: false },
layerType: { type: String as PropType<LayerType | undefined>, required: false },
disabled: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
},
data() {
return {
hoveringDrop: false,
};
},
computed: {
droppable() {
return this.hoveringDrop && currentDraggingElement();
},
},
methods: {
dragOver(e: DragEvent): void {
this.hoveringDrop = true;
e.preventDefault();
},
dragLeave(): void {
this.hoveringDrop = false;
},
drop(e: DragEvent): void {
this.hoveringDrop = false;
const element = currentDraggingElement();
const layerPath = element?.getAttribute("data-layer") || undefined;
if (layerPath) {
e.preventDefault();
this.$emit("update:value", layerPath);
}
},
clearLayer(): void {
this.$emit("update:value", undefined);
},
layerTypeData(layerType: LayerType): LayerTypeData {
return layerTypeData(layerType) || { name: "Error", icon: "Info" };
},
},
components: {
IconButton,
IconLabel,
LayoutRow,
TextLabel,
},
});
</script>

View file

@ -1,5 +1,5 @@
<template>
<LayoutRow class="optional-input">
<LayoutRow class="optional-input" :class="disabled">
<CheckboxInput :checked="checked" :disabled="disabled" @input="(e: Event) => $emit('update:checked', (e.target as HTMLInputElement).checked)" :icon="icon" :tooltip="tooltip" />
</LayoutRow>
</template>
@ -18,6 +18,10 @@
border-radius: 2px 0 0 2px;
box-sizing: border-box;
}
&.disabled label {
border: 1px solid var(--color-4-dimgray);
}
}
</style>

View file

@ -0,0 +1,26 @@
let draggingElement: HTMLElement | undefined;
export function createDragManager(): () => void {
const clearDraggingElement = (): void => {
draggingElement = undefined;
};
// Add the event listener
document.addEventListener("drop", clearDraggingElement);
// Return the destructor
return () => {
// We use setTimeout to sequence this drop after any potential users in the current call stack progression, since this will begin in an entirely new call stack later
setTimeout(() => {
document.removeEventListener("drop", clearDraggingElement);
}, 0);
};
}
export function beginDraggingElement(element: HTMLElement): void {
draggingElement = element;
}
export function currentDraggingElement(): HTMLElement | undefined {
return draggingElement;
}

View file

@ -4,6 +4,7 @@ import { type IconName } from "@/utility-functions/icons";
import { browserVersion, operatingSystem } from "@/utility-functions/platform";
import { stripIndents } from "@/utility-functions/strip-indents";
import { type Editor } from "@/wasm-communication/editor";
import type { TextLabel } from "@/wasm-communication/messages";
import { type TextButtonWidget, type WidgetLayout, Widget, DisplayDialogPanic } from "@/wasm-communication/messages";
export function createPanicManager(editor: Editor, dialogState: DialogState): void {
@ -24,11 +25,11 @@ export function createPanicManager(editor: Editor, dialogState: DialogState): vo
}
function preparePanicDialog(header: string, details: string, panicDetails: string): [IconName, WidgetLayout, TextButtonWidget[]] {
const headerLabel: TextLabel = { kind: "TextLabel", value: header, disabled: false, bold: true, italic: false, tableAlign: false, minWidth: 0, multiline: false, tooltip: "" };
const detailsLabel: TextLabel = { kind: "TextLabel", value: details, disabled: false, bold: false, italic: false, tableAlign: false, minWidth: 0, multiline: true, tooltip: "" };
const widgets: WidgetLayout = {
layout: [
{ rowWidgets: [new Widget({ kind: "TextLabel", value: header, disabled: false, bold: true, italic: false, tableAlign: false, minWidth: 0, multiline: false, tooltip: "" }, 0n)] },
{ rowWidgets: [new Widget({ kind: "TextLabel", value: details, disabled: false, bold: false, italic: false, tableAlign: false, minWidth: 0, multiline: true, tooltip: "" }, 1n)] },
],
layout: [{ rowWidgets: [new Widget(headerLabel, 0n)] }, { rowWidgets: [new Widget(detailsLabel, 1n)] }],
layoutTarget: undefined,
};

View file

@ -15,7 +15,6 @@ export function createNodeGraphState(editor: Editor) {
editor.subscriptions.subscribeJsMessage(UpdateNodeGraph, (updateNodeGraph) => {
state.nodes = updateNodeGraph.nodes;
state.links = updateNodeGraph.links;
console.info("Recieved updated nodes", state.nodes);
});
editor.subscriptions.subscribeJsMessage(UpdateNodeTypes, (updateNodeTypes) => {
state.nodeTypes = updateNodeTypes.nodeTypes;

View file

@ -68,7 +68,7 @@ export function createPortfolioState(editor: Editor) {
imaginateCheckConnection(hostname, editor);
});
editor.subscriptions.subscribeJsMessage(TriggerImaginateGenerate, async (triggerImaginateGenerate) => {
const { documentId, layerPath, hostname, refreshFrequency, baseImage, parameters } = triggerImaginateGenerate;
const { documentId, layerPath, hostname, refreshFrequency, baseImage, maskImage, maskPaintMode, maskBlurPx, maskFillContent, parameters } = triggerImaginateGenerate;
// Handle img2img mode
let image: Blob | undefined;
@ -79,7 +79,14 @@ export function createPortfolioState(editor: Editor) {
preloadAndSetImaginateBlobURL(editor, image, documentId, layerPath, baseImage.size[0], baseImage.size[1]);
}
imaginateGenerate(parameters, image, hostname, refreshFrequency, documentId, layerPath, editor);
// Handle layer mask
let mask: Blob | undefined;
if (maskImage !== undefined) {
// Rasterize the SVG to an image file
mask = await rasterizeSVG(maskImage.svg, maskImage.size[0], maskImage.size[1], "image/png");
}
imaginateGenerate(parameters, image, mask, maskPaintMode, maskBlurPx, maskFillContent, hostname, refreshFrequency, documentId, layerPath, editor);
});
editor.subscriptions.subscribeJsMessage(TriggerImaginateTerminate, async (triggerImaginateTerminate) => {
const { documentId, layerPath, hostname } = triggerImaginateTerminate;

View file

@ -23,6 +23,10 @@ let statusAbortController = new AbortController();
export async function imaginateGenerate(
parameters: ImaginateGenerationParameters,
image: Blob | undefined,
mask: Blob | undefined,
maskPaintMode: string,
maskBlurPx: number,
maskFillContent: string,
hostname: string,
refreshFrequency: number,
documentId: bigint,
@ -41,7 +45,7 @@ export async function imaginateGenerate(
const discloseUploadingProgress = (progress: number): void => {
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, progress * 100, "Uploading");
};
const { uploaded, result, xhr } = await generate(discloseUploadingProgress, hostname, image, parameters);
const { uploaded, result, xhr } = await generate(discloseUploadingProgress, hostname, image, mask, maskPaintMode, maskBlurPx, maskFillContent, parameters);
generatingAbortRequest = xhr;
try {
@ -211,6 +215,10 @@ async function generate(
discloseUploadingProgress: (progress: number) => void,
hostname: string,
image: Blob | undefined,
mask: Blob | undefined,
maskPaintMode: string,
maskBlurPx: number,
maskFillContent: string,
parameters: ImaginateGenerationParameters
): Promise<{
uploaded: Promise<void>;
@ -255,6 +263,13 @@ async function generate(
};
} else {
const sourceImageBase64 = await blobToBase64(image);
const maskImageBase64 = mask ? await blobToBase64(mask) : "";
const maskFillContentIndexes = ["Fill", "Original", "LatentNoise", "LatentNothing"];
const maskFillContentIndexFound = maskFillContentIndexes.indexOf(maskFillContent);
const maskFillContentIndex = maskFillContentIndexFound === -1 ? undefined : maskFillContentIndexFound;
const maskInvert = maskPaintMode === "Inpaint" ? 1 : 0;
endpoint = `${hostname}sdapi/v1/img2img`;
@ -262,12 +277,12 @@ async function generate(
init_images: [sourceImageBase64],
// resize_mode: 0,
denoising_strength: parameters.denoisingStrength,
// mask: "",
// mask_blur: 4,
// inpainting_fill: 0,
// inpaint_full_res: true,
mask: mask && maskImageBase64,
mask_blur: mask && maskBlurPx,
inpainting_fill: mask && maskFillContentIndex,
inpaint_full_res: mask && false,
// inpaint_full_res_padding: 0,
// inpainting_mask_invert: 0,
inpainting_mask_invert: mask && maskInvert,
prompt: parameters.prompt,
// styles: [],
seed: Number(parameters.seed),
@ -291,6 +306,7 @@ async function generate(
// s_noise: 1,
override_settings: {
show_progress_every_n_steps: PROGRESS_EVERY_N_STEPS,
img2img_fix_steps: true,
},
sampler_index: parameters.samplingMethod,
// include_init_images: false,

View file

@ -509,6 +509,15 @@ export class TriggerImaginateGenerate extends JsMessage {
@Type(() => ImaginateBaseImage)
readonly baseImage!: ImaginateBaseImage | undefined;
@Type(() => ImaginateBaseImage)
readonly maskImage: ImaginateBaseImage | undefined;
readonly maskPaintMode!: string;
readonly maskBlurPx!: number;
readonly maskFillContent!: string;
readonly hostname!: string;
readonly refreshFrequency!: number;
@ -670,7 +679,7 @@ export class UpdateDocumentLayerDetails extends JsMessage {
export class LayerPanelEntry {
name!: string;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
visible!: boolean;
@ -764,7 +773,7 @@ export class CheckboxInput extends WidgetProps {
icon!: IconName;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -778,7 +787,7 @@ export class ColorInput extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -818,7 +827,7 @@ export class DropdownInput extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -831,7 +840,7 @@ export class FontInput extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -844,7 +853,7 @@ export class IconButton extends WidgetProps {
active!: boolean;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -853,10 +862,28 @@ export class IconLabel extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
export class LayerReferenceInput extends WidgetProps {
@Transform(({ value }: { value: BigUint64Array | undefined }) => (value ? String(value) : undefined))
value!: string | undefined;
layerName!: string | undefined;
layerType!: LayerType | undefined;
disabled!: boolean;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
// Styling
minWidth!: number;
}
export type NumberInputIncrementBehavior = "Add" | "Multiply" | "Callback" | "None";
export type NumberInputMode = "Increment" | "Range";
@ -865,7 +892,7 @@ export class NumberInput extends WidgetProps {
label!: string | undefined;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
// Disabled
@ -914,7 +941,7 @@ export class OptionalInput extends WidgetProps {
icon!: IconName;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -928,7 +955,7 @@ export class PopoverButton extends WidgetProps {
text!: string;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -975,7 +1002,7 @@ export class TextAreaInput extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -990,7 +1017,7 @@ export class TextButton extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -1021,7 +1048,7 @@ export class TextInput extends WidgetProps {
minWidth!: number;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -1042,7 +1069,7 @@ export class TextLabel extends WidgetProps {
multiline!: boolean;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -1063,6 +1090,7 @@ const widgetSubTypes = [
{ value: FontInput, name: "FontInput" },
{ value: IconButton, name: "IconButton" },
{ value: IconLabel, name: "IconLabel" },
{ value: LayerReferenceInput, name: "LayerReferenceInput" },
{ value: NumberInput, name: "NumberInput" },
{ value: OptionalInput, name: "OptionalInput" },
{ value: PopoverButton, name: "PopoverButton" },

View file

@ -179,7 +179,7 @@ impl JsEditorHandle {
self.dispatch(message);
Ok(())
}
_ => Err(Error::new("Could not update UI").into()),
(target, val) => Err(Error::new(&format!("Could not update UI\nDetails:\nTarget: {:?}\nValue: {:?}", target, val)).into()),
}
}

View file

@ -82,6 +82,22 @@ impl Document {
}
}
/// Renders a layer and its children
pub fn render_layer(&mut self, layer_path: &[LayerId], render_data: RenderData) -> Option<String> {
// Note: it is bad practice to directly clone and modify the Graphene document structure, this is a temporary hack until this whole system is replaced by the node graph
let mut temp_clone = self.layer_mut(layer_path).ok()?.clone();
// Render and append to the defs section
let mut svg_defs = String::from("<defs>");
temp_clone.render(&mut vec![], &mut svg_defs, render_data);
svg_defs.push_str("</defs>");
// Append the cached rendered SVG
svg_defs.push_str(&temp_clone.cache);
Some(svg_defs)
}
pub fn current_state_identifier(&self) -> u64 {
self.state_identifier.finish()
}
@ -895,6 +911,36 @@ impl Document {
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetMaskBlurPx { path, mask_blur_px } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate mask blur for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.mask_blur_px = mask_blur_px;
} else {
panic!("Incorrectly trying to set the mask blur for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetMaskFillContent { path, mode } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate mask fill content for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.mask_fill_content = mode;
} else {
panic!("Incorrectly trying to set the mask fill content for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetMaskPaintMode { path, paint } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate mask paint mode for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.mask_paint_mode = paint;
} else {
panic!("Incorrectly trying to set the mask paint mode for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetCfgScale { path, cfg_scale } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate CFG scale for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
@ -915,6 +961,16 @@ impl Document {
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetLayerPath { path, layer_path } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate layer path strength for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.mask_layer_ref = layer_path;
} else {
panic!("Incorrectly trying to set the layer path for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetSamples { path, samples } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate samples for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {

View file

@ -18,6 +18,10 @@ pub struct ImaginateLayer {
pub sampling_method: ImaginateSamplingMethod,
pub use_img2img: bool,
pub denoising_strength: f64,
pub mask_layer_ref: Option<Vec<LayerId>>,
pub mask_paint_mode: ImaginateMaskPaintMode,
pub mask_blur_px: u32,
pub mask_fill_content: ImaginateMaskFillContent,
pub cfg_scale: f64,
pub prompt: String,
pub negative_prompt: String,
@ -62,6 +66,44 @@ pub struct ImaginateBaseImage {
pub size: DVec2,
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)]
pub enum ImaginateMaskPaintMode {
#[default]
Inpaint,
Outpaint,
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)]
pub enum ImaginateMaskFillContent {
#[default]
Fill,
Original,
LatentNoise,
LatentNothing,
}
impl ImaginateMaskFillContent {
pub fn list() -> [ImaginateMaskFillContent; 4] {
[
ImaginateMaskFillContent::Fill,
ImaginateMaskFillContent::Original,
ImaginateMaskFillContent::LatentNoise,
ImaginateMaskFillContent::LatentNothing,
]
}
}
impl std::fmt::Display for ImaginateMaskFillContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImaginateMaskFillContent::Fill => write!(f, "Smeared Surroundings"),
ImaginateMaskFillContent::Original => write!(f, "Original Base Image"),
ImaginateMaskFillContent::LatentNoise => write!(f, "Randomness (Latent Noise)"),
ImaginateMaskFillContent::LatentNothing => write!(f, "Neutral (Latent Nothing)"),
}
}
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Deserialize, Serialize)]
pub enum ImaginateSamplingMethod {
#[default]
@ -182,6 +224,10 @@ impl Default for ImaginateLayer {
sampling_method: Default::default(),
use_img2img: false,
denoising_strength: 0.66,
mask_paint_mode: ImaginateMaskPaintMode::default(),
mask_layer_ref: None,
mask_blur_px: 4,
mask_fill_content: ImaginateMaskFillContent::default(),
cfg_scale: 10.,
prompt: "".into(),
negative_prompt: "".into(),

View file

@ -76,7 +76,7 @@ impl fmt::Display for LayerDataTypeDiscriminant {
LayerDataTypeDiscriminant::Text => write!(f, "Text"),
LayerDataTypeDiscriminant::Image => write!(f, "Image"),
LayerDataTypeDiscriminant::Imaginate => write!(f, "Imaginate"),
LayerDataTypeDiscriminant::NodeGraphFrame => write!(f, "NodeGraphFrame"),
LayerDataTypeDiscriminant::NodeGraphFrame => write!(f, "Node Graph Frame"),
}
}
}

View file

@ -1,6 +1,6 @@
use crate::boolean_ops::BooleanOperation as BooleanOperationType;
use crate::layers::blend_mode::BlendMode;
use crate::layers::imaginate_layer::{ImaginateSamplingMethod, ImaginateStatus};
use crate::layers::imaginate_layer::{ImaginateMaskFillContent, ImaginateMaskPaintMode, ImaginateSamplingMethod, ImaginateStatus};
use crate::layers::layer_info::Layer;
use crate::layers::style::{self, Stroke};
use crate::layers::vector::consts::ManipulatorType;
@ -95,6 +95,18 @@ pub enum Operation {
path: Vec<LayerId>,
prompt: String,
},
ImaginateSetMaskBlurPx {
path: Vec<LayerId>,
mask_blur_px: u32,
},
ImaginateSetMaskFillContent {
path: Vec<LayerId>,
mode: ImaginateMaskFillContent,
},
ImaginateSetMaskPaintMode {
path: Vec<LayerId>,
paint: ImaginateMaskPaintMode,
},
ImaginateSetCfgScale {
path: Vec<LayerId>,
cfg_scale: f64,
@ -118,6 +130,10 @@ pub enum Operation {
path: Vec<LayerId>,
denoising_strength: f64,
},
ImaginateSetLayerPath {
path: Vec<LayerId>,
layer_path: Option<Vec<LayerId>>,
},
ImaginateSetUseImg2Img {
path: Vec<LayerId>,
use_img2img: bool,