From 9d80defa149381e1acbdc73e9fbccf56a5af44b7 Mon Sep 17 00:00:00 2001 From: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Mon, 21 Nov 2022 07:00:38 +0000 Subject: [PATCH] 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 --- .../src/messages/frontend/frontend_message.rs | 10 +- .../messages/layout/layout_message_handler.rs | 15 + .../layout/utility_types/layout_widget.rs | 6 +- .../utility_types/widgets/input_widgets.rs | 30 +- .../document/document_message_handler.rs | 54 +- .../properties_panel_message.rs | 6 +- .../properties_panel_message_handler.rs | 21 +- .../properties_panel/utility_functions.rs | 1122 ++++++++++------- .../document/utility_types/layer_panel.rs | 12 +- .../portfolio/document/utility_types/misc.rs | 2 + frontend/src/App.vue | 3 + frontend/src/components/panels/LayerTree.vue | 50 +- frontend/src/components/widgets/WidgetRow.vue | 3 + .../widgets/inputs/LayerReferenceInput.vue | 160 +++ .../widgets/inputs/OptionalInput.vue | 6 +- frontend/src/io-managers/drag.ts | 26 + frontend/src/io-managers/panic.ts | 9 +- frontend/src/state-providers/node-graph.ts | 1 - frontend/src/state-providers/portfolio.ts | 11 +- frontend/src/utility-functions/imaginate.ts | 28 +- frontend/src/wasm-communication/messages.ts | 56 +- frontend/wasm/src/editor_api.rs | 2 +- graphene/src/document.rs | 56 + graphene/src/layers/imaginate_layer.rs | 46 + graphene/src/layers/layer_info.rs | 2 +- graphene/src/operation.rs | 18 +- 26 files changed, 1211 insertions(+), 544 deletions(-) create mode 100644 frontend/src/components/widgets/inputs/LayerReferenceInput.vue create mode 100644 frontend/src/io-managers/drag.ts diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index d0fed912..f1e1c2af 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -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, + #[serde(rename = "maskImage")] + mask_image: Option, + #[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, diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index e6dfc51f..830a6595 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -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 Vec> MessageHandler { + let update_value = value.is_null().not().then(|| { + value + .as_str() + .expect("LayerReferenceInput update was not of type: string") + .split(',') + .map(|id| id.parse::().unwrap()) + .collect::>() + }); + 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(); diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index 089e29b4..2442a940 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -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), diff --git a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs index 6f87a7c4..194aaa92 100644 --- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs @@ -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>, + + #[serde(rename = "layerName")] + pub layer_name: Option, + + #[serde(rename = "layerType")] + pub layer_type: Option, + + pub disabled: bool, + + pub tooltip: String, + + #[serde(skip)] + pub tooltip_shortcut: Option, + + // Styling + #[serde(rename = "minWidth")] + pub min_width: u32, + + // Callbacks + #[serde(skip)] + #[derivative(Debug = "ignore", PartialEq = "ignore")] + pub on_update: WidgetCallback, +} + #[derive(Clone, Serialize, Deserialize, Derivative)] #[derivative(Debug, PartialEq, Default)] pub struct NumberInput { diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 789d38e1..7c901999 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -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); - Some(ImaginateBaseImage { svg, size }) + 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#""#, 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##""##, outside_artboards_color); let matrix = transform .to_cols_array() .iter() diff --git a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message.rs b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message.rs index 4611a34a..31067dd8 100644 --- a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message.rs +++ b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message.rs @@ -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>, document: TargetDocument }, SetImaginateCfgScale { cfg_scale: f64 }, SetImaginateDenoisingStrength { denoising_strength: f64 }, + SetImaginateLayerPath { layer_path: Option> }, + SetImaginateMaskBlurPx { mask_blur_px: u32 }, + SetImaginateMaskFillContent { mode: ImaginateMaskFillContent }, + SetImaginateMaskPaintMode { paint: ImaginateMaskPaintMode }, SetImaginateNegativePrompt { negative_prompt: String }, SetImaginatePrompt { prompt: String }, SetImaginateRestoreFaces { restore_faces: bool }, diff --git a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs index 061b0060..ca03c638 100644 --- a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs @@ -140,10 +140,11 @@ impl<'a> MessageHandler { 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 { + 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()); diff --git a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs index 1fea4e16..ab3e323a 100644 --- a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs +++ b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs @@ -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, layer: &Layer, responses: &mut VecDeque, @@ -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,185 +532,546 @@ 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) -> LayoutGroup { - LayoutGroup::Section { - name: "Imaginate".into(), - layout: vec![ - LayoutGroup::Row { - widgets: { - let tooltip = "Connection status to the server that computes generated images".to_string(); +fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, document: &Document, persistent_data: &PersistentData, responses: &mut VecDeque) -> 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))); - vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Server".into(), - tooltip: tooltip.clone(), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::IconButton(IconButton { - size: 24, - icon: "Settings".into(), - tooltip: "Preferences: Imaginate".into(), - on_update: WidgetCallback::new(|_| DialogMessage::RequestPreferencesDialog.into()), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: { - match &persistent_data.imaginate_server_status { - ImaginateServerStatus::Unknown => { - responses.push_back(PortfolioMessage::ImaginateCheckServerStatus.into()); - "Checking...".into() - } - ImaginateServerStatus::Checking => "Checking...".into(), - ImaginateServerStatus::Unavailable => "Unavailable".into(), - ImaginateServerStatus::Connected => "Connected".into(), - } - }, - bold: true, - tooltip, - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::IconButton(IconButton { - size: 24, - icon: "Reload".into(), - tooltip: "Refresh connection status".into(), - on_update: WidgetCallback::new(|_| PortfolioMessage::ImaginateCheckServerStatus.into()), - ..Default::default() - })), - ] - }, - }, - LayoutGroup::Row { - widgets: { - let tooltip = "When generating, the percentage represents how many sampling steps have so far been processed out of the target number".to_string(); + let layer_reference_input_layer_is_some = layer_reference_input_layer.is_some(); - vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Progress".into(), - tooltip: tooltip.clone(), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::TextLabel(TextLabel { - 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. - { - interpreted_status = ImaginateStatus::Terminated; - } + 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); - match interpreted_status { - ImaginateStatus::Idle => match imaginate_layer.blob_url { - Some(_) => "Done".into(), - None => "Ready".into(), - }, - ImaginateStatus::Beginning => "Beginning...".into(), - ImaginateStatus::Uploading(percent) => format!("Uploading Base Image: {:.0}%", percent), - ImaginateStatus::Generating => format!("Generating: {:.0}%", imaginate_layer.percent_complete), - ImaginateStatus::Terminating => "Terminating...".into(), - ImaginateStatus::Terminated => format!("{:.0}% (Terminated)", imaginate_layer.percent_complete), + let mut layout = vec![ + LayoutGroup::Row { + widgets: { + let tooltip = "Connection status to the server that computes generated images".to_string(); + + vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Server".into(), + tooltip: tooltip.clone(), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::IconButton(IconButton { + size: 24, + icon: "Settings".into(), + tooltip: "Preferences: Imaginate".into(), + on_update: WidgetCallback::new(|_| DialogMessage::RequestPreferencesDialog.into()), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: { + match &persistent_data.imaginate_server_status { + ImaginateServerStatus::Unknown => { + responses.push_back(PortfolioMessage::ImaginateCheckServerStatus.into()); + "Checking...".into() } - }, - bold: true, - tooltip, - ..Default::default() - })), - ] - }, - }, - LayoutGroup::Row { - widgets: [ - vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Image".into(), - tooltip: "Buttons that control the image generation process".into(), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - ], - { - match imaginate_layer.status { - ImaginateStatus::Beginning | ImaginateStatus::Uploading(_) => vec![WidgetHolder::new(Widget::TextButton(TextButton { - label: "Beginning...".into(), - tooltip: "Sending image generation request to the server".into(), - disabled: true, - ..Default::default() - }))], - ImaginateStatus::Generating => vec![WidgetHolder::new(Widget::TextButton(TextButton { - label: "Terminate".into(), - tooltip: "Cancel the in-progress image generation and keep the latest progress".into(), - on_update: WidgetCallback::new(|_| DocumentMessage::ImaginateTerminate.into()), - ..Default::default() - }))], - ImaginateStatus::Terminating => vec![WidgetHolder::new(Widget::TextButton(TextButton { - label: "Terminating...".into(), - tooltip: "Waiting on the final image generated after termination".into(), - disabled: true, - ..Default::default() - }))], - ImaginateStatus::Idle | ImaginateStatus::Terminated => vec![ - WidgetHolder::new(Widget::IconButton(IconButton { - size: 24, - icon: "Random".into(), - tooltip: "Generate with a random seed".into(), - on_update: WidgetCallback::new(|_| PropertiesPanelMessage::SetImaginateSeedRandomizeAndGenerate.into()), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::TextButton(TextButton { - label: "Generate".into(), - tooltip: "Fill layer frame by generating a new image".into(), - on_update: WidgetCallback::new(|_| DocumentMessage::ImaginateGenerate.into()), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::TextButton(TextButton { - label: "Clear".into(), - tooltip: "Remove generated image from the layer frame".into(), - disabled: imaginate_layer.blob_url.is_none(), - on_update: WidgetCallback::new(|_| DocumentMessage::FrameClear.into()), - ..Default::default() - })), - ], - } - }, + ImaginateServerStatus::Checking => "Checking...".into(), + ImaginateServerStatus::Unavailable => "Unavailable".into(), + ImaginateServerStatus::Connected => "Connected".into(), + } + }, + bold: true, + tooltip, + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::IconButton(IconButton { + size: 24, + icon: "Reload".into(), + tooltip: "Refresh connection status".into(), + on_update: WidgetCallback::new(|_| PortfolioMessage::ImaginateCheckServerStatus.into()), + ..Default::default() + })), ] - .concat(), }, + }, + LayoutGroup::Row { + widgets: { + let tooltip = "When generating, the percentage represents how many sampling steps have so far been processed out of the target number".to_string(); + + vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Progress".into(), + tooltip: tooltip.clone(), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextLabel(TextLabel { + 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. + { + interpreted_status = ImaginateStatus::Terminated; + } + + match interpreted_status { + ImaginateStatus::Idle => match imaginate_layer.blob_url { + Some(_) => "Done".into(), + None => "Ready".into(), + }, + ImaginateStatus::Beginning => "Beginning...".into(), + ImaginateStatus::Uploading(percent) => format!("Uploading Base Image: {:.0}%", percent), + ImaginateStatus::Generating => format!("Generating: {:.0}%", imaginate_layer.percent_complete), + ImaginateStatus::Terminating => "Terminating...".into(), + ImaginateStatus::Terminated => format!("{:.0}% (Terminated)", imaginate_layer.percent_complete), + } + }, + bold: true, + tooltip, + ..Default::default() + })), + ] + }, + }, + LayoutGroup::Row { + widgets: [ + vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Image".into(), + tooltip: "Buttons that control the image generation process".into(), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + ], + { + match imaginate_layer.status { + ImaginateStatus::Beginning | ImaginateStatus::Uploading(_) => vec![WidgetHolder::new(Widget::TextButton(TextButton { + label: "Beginning...".into(), + tooltip: "Sending image generation request to the server".into(), + disabled: true, + ..Default::default() + }))], + ImaginateStatus::Generating => vec![WidgetHolder::new(Widget::TextButton(TextButton { + label: "Terminate".into(), + tooltip: "Cancel the in-progress image generation and keep the latest progress".into(), + on_update: WidgetCallback::new(|_| DocumentMessage::ImaginateTerminate.into()), + ..Default::default() + }))], + ImaginateStatus::Terminating => vec![WidgetHolder::new(Widget::TextButton(TextButton { + label: "Terminating...".into(), + tooltip: "Waiting on the final image generated after termination".into(), + disabled: true, + ..Default::default() + }))], + ImaginateStatus::Idle | ImaginateStatus::Terminated => vec![ + WidgetHolder::new(Widget::IconButton(IconButton { + size: 24, + icon: "Random".into(), + tooltip: "Generate with a new random seed".into(), + on_update: WidgetCallback::new(|_| PropertiesPanelMessage::SetImaginateSeedRandomizeAndGenerate.into()), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextButton(TextButton { + label: "Generate".into(), + tooltip: "Fill layer frame by generating a new image".into(), + on_update: WidgetCallback::new(|_| DocumentMessage::ImaginateGenerate.into()), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextButton(TextButton { + label: "Clear".into(), + tooltip: "Remove generated image from the layer frame".into(), + disabled: imaginate_layer.blob_url.is_none(), + on_update: WidgetCallback::new(|_| DocumentMessage::FrameClear.into()), + ..Default::default() + })), + ], + } + }, + ] + .concat(), + }, + LayoutGroup::Row { + widgets: { + let tooltip = "Seed determines the random outcome, enabling limitless unique variations".to_string(); + + vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Seed".into(), + tooltip: tooltip.clone(), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::IconButton(IconButton { + size: 24, + icon: "Regenerate".into(), + tooltip: "Set a new random seed".into(), + on_update: WidgetCallback::new(|_| PropertiesPanelMessage::SetImaginateSeedRandomize.into()), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: Some(imaginate_layer.seed as f64), + min: Some(-1.), + tooltip, + on_update: WidgetCallback::new(move |number_input: &NumberInput| { + PropertiesPanelMessage::SetImaginateSeed { + seed: number_input.value.unwrap().round() as u64, + } + .into() + }), + ..Default::default() + })), + ] + }, + }, + LayoutGroup::Row { + widgets: { + let tooltip = " + Width and height of the image that will be generated. Larger resolutions take longer to compute.\n\ + \n\ + 512x512 yields optimal results because the AI is trained to understand that scale best. Larger sizes may tend to integrate the prompt's subject more than once. Small sizes are often incoherent. Put the layer in a folder and resize that to keep resolution unchanged.\n\ + \n\ + Dimensions must be a multiple of 64, so these are set by rounding the layer dimensions. A resolution exceeding 1 megapixel is reduced below that limit because larger sizes may exceed available GPU memory on the server. + ".trim().to_string(); + + vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Resolution".into(), + tooltip: tooltip.clone(), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::IconButton(IconButton { + size: 24, + icon: "Rescale".into(), + tooltip: "Set the layer scale to this resolution".into(), + on_update: WidgetCallback::new(|_| PropertiesPanelMessage::SetImaginateScaleFromResolution.into()), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: { + let (width, height) = pick_layer_safe_imaginate_resolution(layer, &persistent_data.font_cache); + format!("{} W x {} H", width, height) + }, + tooltip, + bold: true, + ..Default::default() + })), + ] + }, + }, + LayoutGroup::Row { + widgets: { + let tooltip = "Number of iterations to improve the image generation quality, with diminishing returns around 40 when using the Euler A sampling method".to_string(); + vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Sampling Steps".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.samples.into()), + mode: NumberInputMode::Range, + range_min: Some(0.), + range_max: Some(150.), + is_integer: true, + min: Some(0.), + max: Some(150.), + tooltip, + on_update: WidgetCallback::new(move |number_input: &NumberInput| { + PropertiesPanelMessage::SetImaginateSamples { + samples: number_input.value.unwrap().round() as u32, + } + .into() + }), + ..Default::default() + })), + ] + }, + }, + LayoutGroup::Row { + widgets: { + let tooltip = "Algorithm used to generate the image during each sampling step".to_string(); + + let sampling_methods = ImaginateSamplingMethod::list(); + let mut entries = Vec::with_capacity(sampling_methods.len()); + for method in sampling_methods { + entries.push(DropdownEntryData { + label: method.to_string(), + on_update: WidgetCallback::new(move |_| PropertiesPanelMessage::SetImaginateSamplingMethod { method }.into()), + ..DropdownEntryData::default() + }); + } + let entries = vec![entries]; + + vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Sampling Method".into(), + 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.sampling_method as u32), + tooltip, + ..Default::default() + })), + ] + }, + }, + LayoutGroup::Row { + widgets: { + let tooltip = " + Amplification of the text prompt's influence over the outcome. At 0, the prompt is entirely ignored.\n\ + \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). + " + .trim() + .to_string(); + + vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Text Guidance".into(), + tooltip: tooltip.to_string(), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: Some(imaginate_layer.cfg_scale), + mode: NumberInputMode::Range, + range_min: Some(0.), + range_max: Some(30.), + min: Some(0.), + max: Some(30.), + tooltip, + on_update: WidgetCallback::new(move |number_input: &NumberInput| { + PropertiesPanelMessage::SetImaginateCfgScale { + cfg_scale: number_input.value.unwrap(), + } + .into() + }), + ..Default::default() + })), + ] + }, + }, + LayoutGroup::Row { + widgets: vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Text Prompt".into(), + tooltip: " + Description of the desired image subject and style.\n\ + \n\ + Include an artist name like \"Rembrandt\" or art medium like \"watercolor\" or \"photography\" to influence the look. List multiple to meld styles.\n\ + \n\ + To boost (or lessen) the importance of a word or phrase, wrap it in parentheses ending with a colon and a multiplier, for example:\n\ + \"Colorless green ideas (sleep:1.3) furiously\" + " + .trim() + .into(), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextAreaInput(TextAreaInput { + value: imaginate_layer.prompt.clone(), + on_update: WidgetCallback::new(move |text_area_input: &TextAreaInput| { + PropertiesPanelMessage::SetImaginatePrompt { + prompt: text_area_input.value.clone(), + } + .into() + }), + ..Default::default() + })), + ], + }, + LayoutGroup::Row { + widgets: vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Neg. Prompt".into(), + tooltip: "A negative text prompt can be used to list things like objects or colors to avoid".into(), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextAreaInput(TextAreaInput { + value: imaginate_layer.negative_prompt.clone(), + on_update: WidgetCallback::new(move |text_area_input: &TextAreaInput| { + PropertiesPanelMessage::SetImaginateNegativePrompt { + negative_prompt: text_area_input.value.clone(), + } + .into() + }), + ..Default::default() + })), + ], + }, + 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 = " + 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 = "Seed determines the random outcome, enabling limitless unique variations".to_string(); + 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: "Seed".into(), + value: "Mask Direction".to_string(), tooltip: tooltip.clone(), ..Default::default() })), @@ -714,24 +1079,48 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi separator_type: SeparatorType::Unrelated, direction: SeparatorDirection::Horizontal, })), - WidgetHolder::new(Widget::IconButton(IconButton { - size: 24, - icon: "Regenerate".into(), - tooltip: "Set a new random seed".into(), - on_update: WidgetCallback::new(|_| PropertiesPanelMessage::SetImaginateSeedRandomize.into()), + 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::Related, + separator_type: SeparatorType::Unrelated, direction: SeparatorDirection::Horizontal, })), WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(imaginate_layer.seed as f64), - min: Some(-1.), + 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::SetImaginateSeed { - seed: number_input.value.unwrap().round() as u64, + PropertiesPanelMessage::SetImaginateMaskBlurPx { + mask_blur_px: number_input.value.unwrap() as u32, } .into() }), @@ -743,89 +1132,19 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi LayoutGroup::Row { widgets: { let tooltip = " - Width and height of the image that will be generated. Larger resolutions take longer to compute.\n\ + Begin in/outpainting the masked areas using this fill content as the starting base image.\n\ \n\ - 512x512 yields optimal results because the AI is trained to understand that scale best. Larger sizes may tend to integrate the prompt's subject more than once. Small sizes are often incoherent. Put the layer in a folder and resize that to keep resolution unchanged.\n\ - \n\ - Dimensions must be a multiple of 64, so these are set by rounding the layer dimensions. A resolution exceeding 1 megapixel is reduced below that limit because larger sizes may exceed available GPU memory on the server. - ".trim().to_string(); + Each option can be visualized by generating with 'Sampling Steps' set to 0. + " + .trim() + .to_string(); - vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Resolution".into(), - tooltip: tooltip.clone(), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::IconButton(IconButton { - size: 24, - icon: "Rescale".into(), - tooltip: "Set the layer scale to this resolution".into(), - on_update: WidgetCallback::new(|_| PropertiesPanelMessage::SetImaginateScaleFromResolution.into()), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: { - let (width, height) = pick_layer_safe_imaginate_resolution(layer, &persistent_data.font_cache); - format!("{} W x {} H", width, height) - }, - tooltip, - bold: true, - ..Default::default() - })), - ] - }, - }, - LayoutGroup::Row { - widgets: { - let tooltip = "Number of iterations to improve the image generation quality, with diminishing returns around 40 when using the Euler A sampling method".to_string(); - vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Sampling Steps".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.samples.into()), - mode: NumberInputMode::Range, - range_min: Some(0.), - range_max: Some(150.), - is_integer: true, - min: Some(0.), - max: Some(150.), - tooltip, - on_update: WidgetCallback::new(move |number_input: &NumberInput| { - PropertiesPanelMessage::SetImaginateSamples { - samples: number_input.value.unwrap().round() as u32, - } - .into() - }), - ..Default::default() - })), - ] - }, - }, - LayoutGroup::Row { - widgets: { - let tooltip = "Algorithm used to generate the image during each sampling step".to_string(); - - let sampling_methods = ImaginateSamplingMethod::list(); - let mut entries = Vec::with_capacity(sampling_methods.len()); - for method in sampling_methods { + 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: method.to_string(), - on_update: WidgetCallback::new(move |_| PropertiesPanelMessage::SetImaginateSamplingMethod { method }.into()), + label: mode.to_string(), + on_update: WidgetCallback::new(move |_| PropertiesPanelMessage::SetImaginateMaskFillContent { mode }.into()), ..DropdownEntryData::default() }); } @@ -833,7 +1152,7 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi vec![ WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Sampling Method".into(), + value: "Mask Starting Fill".to_string(), tooltip: tooltip.clone(), ..Default::default() })), @@ -843,227 +1162,76 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi })), WidgetHolder::new(Widget::DropdownInput(DropdownInput { entries, - selected_index: Some(imaginate_layer.sampling_method as u32), + selected_index: Some(imaginate_layer.mask_fill_content as u32), tooltip, ..Default::default() })), ] }, }, - 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 = " - Amplification of the text prompt's influence over the outcome. At 0, the prompt is entirely ignored.\n\ - \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. - " - .trim() - .to_string(); - - vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Text Literalness".into(), - tooltip: tooltip.to_string(), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(imaginate_layer.cfg_scale), - mode: NumberInputMode::Range, - range_min: Some(0.), - range_max: Some(30.), - min: Some(0.), - max: Some(30.), - tooltip, - on_update: WidgetCallback::new(move |number_input: &NumberInput| { - PropertiesPanelMessage::SetImaginateCfgScale { - cfg_scale: number_input.value.unwrap(), - } - .into() - }), - ..Default::default() - })), - ] - }, - }, - LayoutGroup::Row { - widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Text Prompt".into(), - tooltip: " - Description of the desired image subject and style.\n\ - \n\ - Include an artist name like \"Rembrandt\" or art medium like \"watercolor\" or \"photography\" to influence the look. List multiple to meld styles.\n\ - \n\ - To boost (or lessen) the importance of a word or phrase, wrap it in parentheses ending with a colon and a multiplier, for example:\n\ - \"Colorless green ideas (sleep:1.3) furiously\" - " - .trim() - .into(), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::TextAreaInput(TextAreaInput { - value: imaginate_layer.prompt.clone(), - on_update: WidgetCallback::new(move |text_area_input: &TextAreaInput| { - PropertiesPanelMessage::SetImaginatePrompt { - prompt: text_area_input.value.clone(), - } - .into() - }), - ..Default::default() - })), - ], - }, - LayoutGroup::Row { - widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Neg. Prompt".into(), - tooltip: "A negative text prompt can be used to list things like objects or colors to avoid".into(), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::TextAreaInput(TextAreaInput { - value: imaginate_layer.negative_prompt.clone(), - on_update: WidgetCallback::new(move |text_area_input: &TextAreaInput| { - PropertiesPanelMessage::SetImaginateNegativePrompt { - negative_prompt: text_area_input.value.clone(), - } - .into() - }), - ..Default::default() - })), - ], - }, - LayoutGroup::Row { - widgets: { - let tooltip = "Postprocess human (or human-like) faces to look subtly less distorted".to_string(); - - vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Improve Faces".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.restore_faces, - tooltip, - on_update: WidgetCallback::new(move |checkbox_input: &CheckboxInput| { - PropertiesPanelMessage::SetImaginateRestoreFaces { - restore_faces: checkbox_input.checked, - } - .into() - }), - ..Default::default() - })), - ] - }, - }, - LayoutGroup::Row { - widgets: { - let tooltip = "Generate the image so its edges loop seamlessly to make repeatable patterns or textures".to_string(); - - vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Tiling".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.tiling, - tooltip, - on_update: WidgetCallback::new(move |checkbox_input: &CheckboxInput| PropertiesPanelMessage::SetImaginateTiling { tiling: checkbox_input.checked }.into()), - ..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 { + value: "Improve Faces".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.restore_faces, + tooltip, + on_update: WidgetCallback::new(move |checkbox_input: &CheckboxInput| { + PropertiesPanelMessage::SetImaginateRestoreFaces { + restore_faces: checkbox_input.checked, + } + .into() + }), + ..Default::default() + })), + ] + }, + }, + LayoutGroup::Row { + widgets: { + let tooltip = "Generate the image so its edges loop seamlessly to make repeatable patterns or textures".to_string(); + + vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Tiling".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.tiling, + tooltip, + on_update: WidgetCallback::new(move |checkbox_input: &CheckboxInput| PropertiesPanelMessage::SetImaginateTiling { tiling: checkbox_input.checked }.into()), + ..Default::default() + })), + ] + }, + }, + ]); + + LayoutGroup::Section { name: "Imaginate".into(), layout } } fn node_section_node_graph_frame(layer_path: Vec, node_graph_frame: &NodeGraphFrameLayer, open_graph: bool) -> LayoutGroup { diff --git a/editor/src/messages/portfolio/document/utility_types/layer_panel.rs b/editor/src/messages/portfolio/document/utility_types/layer_panel.rs index 263d62df..919a361c 100644 --- a/editor/src/messages/portfolio/document/utility_types/layer_panel.rs +++ b/editor/src/messages/portfolio/document/utility_types/layer_panel.rs @@ -61,12 +61,12 @@ impl LayerPanelEntry { pub fn new(layer_metadata: &LayerMetadata, transform: DAffine2, layer: &Layer, path: Vec, 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::>().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::>().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::>(); diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 6e18edb4..294afe3d 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -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), } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 87a3c87a..1ea4005f 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -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), diff --git a/frontend/src/components/panels/LayerTree.vue b/frontend/src/components/panels/LayerTree.vue index 6206c9d0..078f1eb4 100644 --- a/frontend/src/components/panels/LayerTree.vue +++ b/frontend/src/components/panels/LayerTree.vue @@ -1,5 +1,5 @@ @@ -258,6 +263,7 @@ margin-top: -2px; height: 5px; z-index: 1; + pointer-events: none; } } } @@ -266,6 +272,7 @@ diff --git a/frontend/src/components/widgets/inputs/OptionalInput.vue b/frontend/src/components/widgets/inputs/OptionalInput.vue index a7bb878f..96542e9d 100644 --- a/frontend/src/components/widgets/inputs/OptionalInput.vue +++ b/frontend/src/components/widgets/inputs/OptionalInput.vue @@ -1,5 +1,5 @@ @@ -18,6 +18,10 @@ border-radius: 2px 0 0 2px; box-sizing: border-box; } + + &.disabled label { + border: 1px solid var(--color-4-dimgray); + } } diff --git a/frontend/src/io-managers/drag.ts b/frontend/src/io-managers/drag.ts new file mode 100644 index 00000000..25af14c7 --- /dev/null +++ b/frontend/src/io-managers/drag.ts @@ -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; +} diff --git a/frontend/src/io-managers/panic.ts b/frontend/src/io-managers/panic.ts index 5d90f3d0..dbaecd38 100644 --- a/frontend/src/io-managers/panic.ts +++ b/frontend/src/io-managers/panic.ts @@ -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, }; diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index 5e916b9e..7dcb9372 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -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; diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index 4bc1f87d..d1920002 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -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; diff --git a/frontend/src/utility-functions/imaginate.ts b/frontend/src/utility-functions/imaginate.ts index 84bcc657..fae0fe5b 100644 --- a/frontend/src/utility-functions/imaginate.ts +++ b/frontend/src/utility-functions/imaginate.ts @@ -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; @@ -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, diff --git a/frontend/src/wasm-communication/messages.ts b/frontend/src/wasm-communication/messages.ts index 93a3f7c4..e550ec30 100644 --- a/frontend/src/wasm-communication/messages.ts +++ b/frontend/src/wasm-communication/messages.ts @@ -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" }, diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index f105f91f..33ca925a 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -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()), } } diff --git a/graphene/src/document.rs b/graphene/src/document.rs index a3203caf..5cf0cbff 100644 --- a/graphene/src/document.rs +++ b/graphene/src/document.rs @@ -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 { + // 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(""); + temp_clone.render(&mut vec![], &mut svg_defs, render_data); + svg_defs.push_str(""); + + // 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 { diff --git a/graphene/src/layers/imaginate_layer.rs b/graphene/src/layers/imaginate_layer.rs index 9bb68133..d7a7c8f6 100644 --- a/graphene/src/layers/imaginate_layer.rs +++ b/graphene/src/layers/imaginate_layer.rs @@ -18,6 +18,10 @@ pub struct ImaginateLayer { pub sampling_method: ImaginateSamplingMethod, pub use_img2img: bool, pub denoising_strength: f64, + pub mask_layer_ref: Option>, + 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(), diff --git a/graphene/src/layers/layer_info.rs b/graphene/src/layers/layer_info.rs index a0bbee01..13307625 100644 --- a/graphene/src/layers/layer_info.rs +++ b/graphene/src/layers/layer_info.rs @@ -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"), } } } diff --git a/graphene/src/operation.rs b/graphene/src/operation.rs index bfca4dfd..6ab19b37 100644 --- a/graphene/src/operation.rs +++ b/graphene/src/operation.rs @@ -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, prompt: String, }, + ImaginateSetMaskBlurPx { + path: Vec, + mask_blur_px: u32, + }, + ImaginateSetMaskFillContent { + path: Vec, + mode: ImaginateMaskFillContent, + }, + ImaginateSetMaskPaintMode { + path: Vec, + paint: ImaginateMaskPaintMode, + }, ImaginateSetCfgScale { path: Vec, cfg_scale: f64, @@ -118,6 +130,10 @@ pub enum Operation { path: Vec, denoising_strength: f64, }, + ImaginateSetLayerPath { + path: Vec, + layer_path: Option>, + }, ImaginateSetUseImg2Img { path: Vec, use_img2img: bool,