diff --git a/Cargo.lock b/Cargo.lock index fbfd0f483..57b0b4df5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,9 +66,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.67" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7724808837b77f4b4de9d283820f9d98bcf496d5692934b857a2399d31ff22e6" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" [[package]] name = "ar" @@ -148,7 +148,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -716,9 +716,9 @@ checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" [[package]] name = "cxx" -version = "1.0.84" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27874566aca772cb515af4c6e997b5fe2119820bca447689145e39bb734d19a0" +checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd" dependencies = [ "cc", "cxxbridge-flags", @@ -728,9 +728,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.84" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb951f2523a49533003656a72121306b225ec16a49a09dc6b0ba0d6f3ec3c0" +checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0" dependencies = [ "cc", "codespan-reporting", @@ -743,15 +743,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.84" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be778b6327031c1c7b61dd2e48124eee5361e6aa76b8de93692f011b08870ab4" +checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59" [[package]] name = "cxxbridge-macro" -version = "1.0.84" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b8a2b87662fe5a0a0b38507756ab66aff32638876a0866e5a5fc82ceb07ee49" +checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6" dependencies = [ "proc-macro2", "quote", @@ -1439,6 +1439,7 @@ name = "graph-craft" version = "0.1.0" dependencies = [ "anyhow", + "base64", "borrow_stack", "bytemuck", "dyn-any", @@ -1456,17 +1457,6 @@ dependencies = [ "vulkano", ] -[[package]] -name = "graph-proc-macros" -version = "0.1.0" -dependencies = [ - "graphene-core", - "proc-macro2", - "proc_macro_roids", - "quote", - "syn", -] - [[package]] name = "graphene-core" version = "0.1.0" @@ -1493,11 +1483,11 @@ dependencies = [ "dyn-clone", "glam", "graph-craft", - "graph-proc-macros", "graphene-core", "image", "kurbo", "log", + "node-macro", "once_cell", "proc-macro2", "quote", @@ -1701,6 +1691,15 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "html5ever" version = "0.25.2" @@ -2321,6 +2320,14 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "node-macro" +version = "0.0.0" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -2381,11 +2388,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi", + "hermit-abi 0.2.6", "libc", ] @@ -2476,9 +2483,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.44" +version = "0.10.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d971fd5722fec23977260f6e81aa67d2f22cadbdc2aa049f1022d9a3be1566" +checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" dependencies = [ "bitflags", "cfg-if", @@ -2508,9 +2515,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.79" +version = "0.9.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454462c0eced1e97f2ec09036abc8da362e66802f66fd20f86854d9d8cbcbc4" +checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" dependencies = [ "autocfg", "cc", @@ -2915,30 +2922,19 @@ dependencies = [ [[package]] name = "proc-macro-hack" -version = "0.5.19" +version = "0.5.20+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d89e5dba24725ae5678020bf8f1357a9aa7ff10736b551adbcd3f8d17d766f" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" dependencies = [ "unicode-ident", ] -[[package]] -name = "proc_macro_roids" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06675fa2c577f52bcf77fbb511123927547d154faa08097cc012c66ec3c9611a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "quick-xml" version = "0.23.1" @@ -2950,9 +2946,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556d0f47a940e895261e77dc200d5eadfc6ef644c179c6f5edfc105e3a2292c8" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] @@ -3202,7 +3198,7 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc_codegen_spirv" version = "0.4.0-alpha.17" -source = "git+https://github.com/EmbarkStudios/rust-gpu?branch=main#fabcbd9c78194ad78f1491886467f7ff59851780" +source = "git+https://github.com/EmbarkStudios/rust-gpu?branch=main#d2d6ee2f7513a51c193a65b0fd99a7d40be74ec8" dependencies = [ "ar", "either", @@ -3227,7 +3223,7 @@ dependencies = [ [[package]] name = "rustc_codegen_spirv-types" version = "0.4.0-alpha.17" -source = "git+https://github.com/EmbarkStudios/rust-gpu?branch=main#fabcbd9c78194ad78f1491886467f7ff59851780" +source = "git+https://github.com/EmbarkStudios/rust-gpu?branch=main#d2d6ee2f7513a51c193a65b0fd99a7d40be74ec8" dependencies = [ "rspirv", "serde", @@ -3248,7 +3244,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.15", + "semver 1.0.16", ] [[package]] @@ -3392,9 +3388,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bfa246f936730408c0abee392cc1a50b118ece708c7f630516defd64480c7d8" +checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" dependencies = [ "serde", ] @@ -3441,9 +3437,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.90" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8778cc0b528968fe72abec38b5db5a20a70d148116cd9325d2bc5f5180ca3faf" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" dependencies = [ "itoa 1.0.5", "ryu", @@ -3688,7 +3684,7 @@ dependencies = [ [[package]] name = "spirv-builder" version = "0.4.0-alpha.17" -source = "git+https://github.com/EmbarkStudios/rust-gpu?branch=main#fabcbd9c78194ad78f1491886467f7ff59851780" +source = "git+https://github.com/EmbarkStudios/rust-gpu?branch=main#d2d6ee2f7513a51c193a65b0fd99a7d40be74ec8" dependencies = [ "memchr", "raw-string", @@ -3701,7 +3697,7 @@ dependencies = [ [[package]] name = "spirv-std" version = "0.4.0-alpha.17" -source = "git+https://github.com/EmbarkStudios/rust-gpu#fabcbd9c78194ad78f1491886467f7ff59851780" +source = "git+https://github.com/EmbarkStudios/rust-gpu#d2d6ee2f7513a51c193a65b0fd99a7d40be74ec8" dependencies = [ "bitflags", "glam", @@ -3713,7 +3709,7 @@ dependencies = [ [[package]] name = "spirv-std-macros" version = "0.4.0-alpha.17" -source = "git+https://github.com/EmbarkStudios/rust-gpu#fabcbd9c78194ad78f1491886467f7ff59851780" +source = "git+https://github.com/EmbarkStudios/rust-gpu#d2d6ee2f7513a51c193a65b0fd99a7d40be74ec8" dependencies = [ "proc-macro2", "quote", @@ -3724,7 +3720,7 @@ dependencies = [ [[package]] name = "spirv-std-types" version = "0.4.0-alpha.17" -source = "git+https://github.com/EmbarkStudios/rust-gpu#fabcbd9c78194ad78f1491886467f7ff59851780" +source = "git+https://github.com/EmbarkStudios/rust-gpu#d2d6ee2f7513a51c193a65b0fd99a7d40be74ec8" [[package]] name = "spirv-tools" @@ -3816,9 +3812,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ee3a69cd2c7e06684677e5629b3878b253af05e4714964204279c6bc02cf0b" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", "quote", @@ -3945,7 +3941,7 @@ dependencies = [ "raw-window-handle", "regex", "rfd", - "semver 1.0.15", + "semver 1.0.16", "serde", "serde_json", "serde_repr", @@ -3977,7 +3973,7 @@ dependencies = [ "cargo_toml", "heck 0.4.0", "json-patch", - "semver 1.0.15", + "semver 1.0.16", "serde_json", "tauri-utils", "winres", @@ -3998,7 +3994,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "semver 1.0.15", + "semver 1.0.16", "serde", "serde_json", "sha2", @@ -4081,7 +4077,7 @@ dependencies = [ "phf 0.10.1", "proc-macro2", "quote", - "semver 1.0.15", + "semver 1.0.16", "serde", "serde_json", "serde_with", diff --git a/Cargo.toml b/Cargo.toml index e1685bbd3..f3db048b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "node-graph/graph-craft", "node-graph/interpreted-executor", "node-graph/borrow_stack", + "node-graph/node-macro", "libraries/dyn-any", "libraries/bezier-rs", "website/other/bezier-rs-demos/wasm", diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 24ef4c71a..36af4263a 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -41,6 +41,7 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[ MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerTreeStructure), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad), MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(BroadcastEventDiscriminant::DocumentIsDirty)), + MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::NodeGraphFrameGenerate)), ]; impl Dispatcher { diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 82dd638b1..f722e1021 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -7,8 +7,9 @@ use crate::messages::portfolio::document::utility_types::layer_panel::{JsRawBuff use crate::messages::prelude::*; use crate::messages::tool::utility_types::HintData; +use graph_craft::document::NodeId; +use graph_craft::imaginate_input::*; use graphene::color::Color; -use graphene::layers::imaginate_layer::{ImaginateBaseImage, ImaginateGenerationParameters, ImaginateMaskFillContent, ImaginateMaskPaintMode}; use graphene::layers::text_layer::Font; use graphene::LayerId; @@ -61,13 +62,13 @@ pub enum FrontendMessage { #[serde(rename = "baseImage")] base_image: Option, #[serde(rename = "maskImage")] - mask_image: Option, + mask_image: Option, #[serde(rename = "maskPaintMode")] mask_paint_mode: ImaginateMaskPaintMode, #[serde(rename = "maskBlurPx")] mask_blur_px: u32, #[serde(rename = "maskFillContent")] - mask_fill_content: ImaginateMaskFillContent, + imaginate_mask_starting_fill: ImaginateMaskStartingFill, hostname: String, #[serde(rename = "refreshFrequency")] refresh_frequency: f64, @@ -75,12 +76,16 @@ pub enum FrontendMessage { document_id: u64, #[serde(rename = "layerPath")] layer_path: Vec, + #[serde(rename = "nodePath")] + node_path: Vec, }, TriggerImaginateTerminate { #[serde(rename = "documentId")] document_id: u64, #[serde(rename = "layerPath")] layer_path: Vec, + #[serde(rename = "nodePath")] + node_path: Vec, hostname: String, }, TriggerImport, @@ -102,6 +107,8 @@ pub enum FrontendMessage { layer_path: Vec, svg: String, size: glam::DVec2, + #[serde(rename = "imaginateNode")] + imaginate_node: Option>, }, TriggerOpenDocument, TriggerPaste, diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index 155bd84e8..8b9c12e5c 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -250,6 +250,46 @@ pub enum LayoutGroup { #[serde(rename = "section")] Section { name: String, layout: SubLayout }, } +impl LayoutGroup { + /// Applies a tooltip to all widgets in this row or column without a tooltip. + pub fn with_tooltip(self, tooltip: impl Into) -> Self { + let (is_col, mut widgets) = match self { + LayoutGroup::Column { widgets } => (true, widgets), + LayoutGroup::Row { widgets } => (false, widgets), + _ => unimplemented!(), + }; + let tooltip = tooltip.into(); + for widget in &mut widgets { + let val = match &mut widget.widget { + Widget::CheckboxInput(x) => &mut x.tooltip, + Widget::ColorInput(x) => &mut x.tooltip, + Widget::DropdownInput(x) => &mut x.tooltip, + Widget::FontInput(x) => &mut x.tooltip, + Widget::IconButton(x) => &mut x.tooltip, + Widget::IconLabel(x) => &mut x.tooltip, + Widget::LayerReferenceInput(x) => &mut x.tooltip, + Widget::NumberInput(x) => &mut x.tooltip, + Widget::OptionalInput(x) => &mut x.tooltip, + Widget::ParameterExposeButton(x) => &mut x.tooltip, + Widget::PopoverButton(x) => &mut x.tooltip, + Widget::TextAreaInput(x) => &mut x.tooltip, + Widget::TextButton(x) => &mut x.tooltip, + Widget::TextInput(x) => &mut x.tooltip, + Widget::TextLabel(x) => &mut x.tooltip, + Widget::BreadcrumbTrailButtons(x) => &mut x.tooltip, + Widget::InvisibleStandinInput(_) | Widget::PivotAssist(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::SwatchPairInput(_) => continue, + }; + if val.is_empty() { + *val = tooltip.clone(); + } + } + if is_col { + Self::Column { widgets } + } else { + Self::Row { widgets } + } + } +} #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct WidgetHolder { @@ -262,18 +302,31 @@ impl WidgetHolder { pub fn new(widget: Widget) -> Self { Self { widget_id: generate_uuid(), widget } } - pub fn unrelated_seperator() -> Self { + pub fn unrelated_separator() -> Self { WidgetHolder::new(Widget::Separator(Separator { separator_type: SeparatorType::Unrelated, direction: SeparatorDirection::Horizontal, })) } + pub fn related_separator() -> Self { + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })) + } pub fn text_widget(text: impl Into) -> Self { WidgetHolder::new(Widget::TextLabel(TextLabel { value: text.into(), ..Default::default() })) } + pub fn bold_text(text: impl Into) -> Self { + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: text.into(), + bold: true, + ..Default::default() + })) + } } #[derive(Clone)] 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 194aaa924..2e62df028 100644 --- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs @@ -246,6 +246,42 @@ pub struct NumberInput { pub on_update: WidgetCallback, } +impl NumberInput { + pub fn new() -> Self { + Self::default() + } + pub fn int(mut self) -> Self { + self.is_integer = true; + self + } + pub fn min(mut self, val: f64) -> Self { + self.min = Some(val); + self.range_min = Some(val); + self + } + pub fn max(mut self, val: f64) -> Self { + self.max = Some(val); + self.range_max = Some(val); + self.mode = NumberInputMode::Range; + self + } + pub fn unit(mut self, val: impl Into) -> Self { + self.unit = val.into(); + self + } + pub fn dp(mut self, decimal_places: u32) -> Self { + self.display_decimal_places = decimal_places; + self + } + pub fn percentage(self) -> Self { + self.min(0.).max(100.).unit("%").dp(2) + } + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} + #[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq)] pub enum NumberInputIncrementBehavior { #[default] diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 558b07a9b..d0384e91a 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -3,6 +3,7 @@ use crate::messages::portfolio::document::utility_types::layer_panel::LayerMetad use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis}; use crate::messages::prelude::*; +use graph_craft::document::NodeId; use graphene::boolean_ops::BooleanOperation as BooleanOperationType; use graphene::layers::blend_mode::BlendMode; use graphene::layers::style::ViewMode; @@ -78,8 +79,6 @@ pub enum DocumentMessage { }, FrameClear, GroupSelectedLayers, - ImaginateGenerate, - ImaginateTerminate, LayerChanged { affected_layer_path: Vec, }, @@ -93,6 +92,16 @@ pub enum DocumentMessage { delta: (f64, f64), }, NodeGraphFrameGenerate, + NodeGraphFrameImaginate { + imaginate_node: Vec, + }, + NodeGraphFrameImaginateRandom { + imaginate_node: Vec, + }, + NodeGraphFrameImaginateTerminate { + layer_path: Vec, + node_path: Vec, + }, NudgeSelectedLayers { delta_x: f64, delta_y: f64, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 854825a72..dc58684ed 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -21,11 +21,11 @@ use crate::messages::portfolio::document::utility_types::vectorize_layer_metadat use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::prelude::*; +use graph_craft::document::NodeId; use graphene::color::Color; -use graphene::document::{pick_layer_safe_imaginate_resolution, Document as GrapheneDocument}; +use graphene::document::Document as GrapheneDocument; use graphene::layers::blend_mode::BlendMode; use graphene::layers::folder_layer::FolderLayer; -use graphene::layers::imaginate_layer::{ImaginateBaseImage, ImaginateGenerationParameters, ImaginateStatus}; use graphene::layers::layer_info::{LayerDataType, LayerDataTypeDiscriminant}; use graphene::layers::style::{Fill, RenderData, ViewMode}; use graphene::layers::text_layer::{Font, FontCache}; @@ -388,20 +388,17 @@ impl MessageHandler { - let mut selected_frame_layers = self - .selected_layers_with_type(LayerDataTypeDiscriminant::Imaginate) - .chain(self.selected_layers_with_type(LayerDataTypeDiscriminant::NodeGraphFrame)); - // Get what is hopefully the only selected Imaginate/NodeGraphFrame layer + let mut selected_frame_layers = self.selected_layers_with_type(LayerDataTypeDiscriminant::NodeGraphFrame); + // Get what is hopefully the only selected NodeGraphFrame layer let layer_path = selected_frame_layers.next(); - // Abort if we didn't have any Imaginate/NodeGraphFrame layer, or if there are additional ones also selected + // Abort if we didn't have any NodeGraphFrame layer, or if there are additional ones also selected if layer_path.is_none() || selected_frame_layers.next().is_some() { return; } let layer_path = layer_path.unwrap(); - let layer = self.graphene_document.layer(layer_path).expect("Clearing Imaginate/NodeGraphFrame image for invalid layer"); + let layer = self.graphene_document.layer(layer_path).expect("Clearing NodeGraphFrame image for invalid layer"); let previous_blob_url = match &layer.data { - LayerDataType::Imaginate(imaginate) => &imaginate.blob_url, LayerDataType::NodeGraphFrame(node_graph_frame) => &node_graph_frame.blob_url, x => panic!("Cannot find blob url for layer type {}", LayerDataTypeDiscriminant::from(x)), }; @@ -440,49 +437,6 @@ impl MessageHandler { - if let Some(message) = self.call_imaginate(document_id, preferences, persistent_data) { - // TODO: Eventually remove this after a message system ordering architectural change - // This message is a workaround for the fact that, when `imaginate.ts` calls... - // `editor.instance.setImaginateGeneratingStatus(layerPath, 0, true);` - // ...execution transfers from the Rust part of the call stack into the JS part of the call stack (before the Rust message queue is empty, - // and there is a Properties panel refresh queued next). Then the JS calls that line shown above and enters the Rust part of the callstack - // again, so it's gone through JS (user initiation) -> Rust (process the button press) -> JS (beginning server request) -> Rust (set - // progress percentage to 0). As that call stack returns back from the Rust and back from the JS, it returns to the Rust and finishes - // processing the queue. That's where it then processes the Properties panel refresh that sent the "Ready" or "Done" state that existed - // before pressing the Generate button causing it to show "0%". So "Ready" or "Done" immediately overwrites the "0%". This block below, - // therefore, adds a redundant call to set it to 0% progress so the message execution order ends with this as the final percentage shown - // to the user. - responses.push_back( - DocumentOperation::ImaginateSetGeneratingStatus { - path: self.selected_layers_with_type(LayerDataTypeDiscriminant::Imaginate).next().unwrap().to_vec(), - percent: Some(0.), - status: ImaginateStatus::Beginning, - } - .into(), - ); - - responses.push_back(message); - } - } - ImaginateTerminate => { - let hostname = preferences.imaginate_server_hostname.clone(); - - let layer_path = { - let mut selected_imaginate_layers = self.selected_layers_with_type(LayerDataTypeDiscriminant::Imaginate); - - // Get what is hopefully the only selected Imaginate layer - match selected_imaginate_layers.next() { - // Continue only if there are no additional Imaginate layers also selected - Some(layer_path) if selected_imaginate_layers.next().is_none() => Some(layer_path.to_owned()), - _ => None, - } - }; - - if let Some(layer_path) = layer_path { - responses.push_back(FrontendMessage::TriggerImaginateTerminate { document_id, layer_path, hostname }.into()); - } - } LayerChanged { affected_layer_path } => { if let Ok(layer_entry) = self.layer_panel_entry(affected_layer_path.clone(), &persistent_data.font_cache) { responses.push_back(FrontendMessage::UpdateDocumentLayerDetails { data: layer_entry }.into()); @@ -521,10 +475,39 @@ impl MessageHandler { - if let Some(message) = self.call_node_graph_frame(document_id, preferences, persistent_data) { + if let Some(message) = self.call_node_graph_frame(document_id, preferences, persistent_data, None) { responses.push_back(message); } } + NodeGraphFrameImaginate { imaginate_node } => { + if let Some(message) = self.call_node_graph_frame(document_id, preferences, persistent_data, Some(imaginate_node)) { + responses.push_back(message); + } + } + NodeGraphFrameImaginateRandom { imaginate_node } => { + // Set a random seed input + responses.push_back( + NodeGraphMessage::SetInputValue { + node: *imaginate_node.last().unwrap(), + input_index: 1, + value: graph_craft::document::value::TaggedValue::F64((generate_uuid() >> 1) as f64), + } + .into(), + ); + // Generate the image + responses.push_back(DocumentMessage::NodeGraphFrameImaginate { imaginate_node }.into()); + } + NodeGraphFrameImaginateTerminate { layer_path, node_path } => { + responses.push_back( + FrontendMessage::TriggerImaginateTerminate { + document_id, + layer_path, + node_path, + hostname: preferences.imaginate_server_hostname.clone(), + } + .into(), + ); + } NudgeSelectedLayers { delta_x, delta_y } => { self.backup(responses); for path in self.selected_layers().map(|path| path.to_vec()) { @@ -721,11 +704,6 @@ impl MessageHandler { - if let Some(url) = &imaginate.blob_url { - responses.push_back(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() }.into()); - } - } LayerDataType::NodeGraphFrame(node_graph_frame) => { if let Some(url) = &node_graph_frame.blob_url { responses.push_back(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() }.into()); @@ -934,96 +912,7 @@ impl MessageHandler Option { - let layer_path = { - let mut selected_imaginate_layers = self.selected_layers_with_type(LayerDataTypeDiscriminant::Imaginate); - - // Get what is hopefully the only selected Imaginate layer - match selected_imaginate_layers.next() { - // Continue only if there are no additional Imaginate layers also selected - Some(layer_path) if selected_imaginate_layers.next().is_none() => layer_path.to_owned(), - _ => return None, - } - }; - - // Prepare the Imaginate parameters and base image - - let transform = self.graphene_document.root.transform.inverse() * self.graphene_document.multiply_transforms(&layer_path).unwrap(); - let layer = self.graphene_document.layer(&layer_path).unwrap(); - let imaginate_layer = layer.as_imaginate().unwrap(); - - let parameters = ImaginateGenerationParameters { - seed: imaginate_layer.seed, - samples: imaginate_layer.samples, - sampling_method: imaginate_layer.sampling_method.api_value().to_string(), - denoising_strength: imaginate_layer.use_img2img.then_some(imaginate_layer.denoising_strength), - cfg_scale: imaginate_layer.cfg_scale, - prompt: imaginate_layer.prompt.clone(), - negative_prompt: imaginate_layer.negative_prompt.clone(), - resolution: pick_layer_safe_imaginate_resolution(layer, &persistent_data.font_cache), - restore_faces: imaginate_layer.restore_faces, - tiling: imaginate_layer.tiling, - }; - 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)); - - let mask_is_some = mask.is_some(); - 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_is_some && 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) - }; - - 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, - layer_path, - } - .into(), - ) - } - - pub fn call_node_graph_frame(&mut self, document_id: u64, _preferences: &PreferencesMessageHandler, persistent_data: &PersistentData) -> Option { + pub fn call_node_graph_frame(&mut self, document_id: u64, _preferences: &PreferencesMessageHandler, persistent_data: &PersistentData, imaginate_node: Option>) -> Option { let layer_path = { let mut selected_nodegraph_layers = self.selected_layers_with_type(LayerDataTypeDiscriminant::NodeGraphFrame); @@ -1046,11 +935,20 @@ impl DocumentMessageHandler { let svg = self.render_document(size, transform.inverse(), persistent_data, DocumentRenderMode::OnlyBelowLayerInFolder(&layer_path)); self.restore_document_transform(old_transforms); - Some(FrontendMessage::TriggerNodeGraphFrameGenerate { document_id, layer_path, svg, size }.into()) + Some( + FrontendMessage::TriggerNodeGraphFrameGenerate { + document_id, + layer_path, + svg, + size, + imaginate_node, + } + .into(), + ) } /// Remove the artwork and artboard pan/tilt/zoom to render it without the user's viewport navigation, and save it to be restored at the end - fn remove_document_transform(&mut self) -> [DAffine2; 2] { + pub(crate) fn remove_document_transform(&mut self) -> [DAffine2; 2] { let old_artwork_transform = self.graphene_document.root.transform; self.graphene_document.root.transform = DAffine2::IDENTITY; GrapheneDocument::mark_children_as_dirty(&mut self.graphene_document.root); @@ -1063,7 +961,7 @@ impl DocumentMessageHandler { } /// Transform the artwork and artboard back to their original scales - fn restore_document_transform(&mut self, [old_artwork_transform, old_artboard_transform]: [DAffine2; 2]) { + pub(crate) fn restore_document_transform(&mut self, [old_artwork_transform, old_artboard_transform]: [DAffine2; 2]) { self.graphene_document.root.transform = old_artwork_transform; GrapheneDocument::mark_children_as_dirty(&mut self.graphene_document.root); @@ -1568,15 +1466,6 @@ impl DocumentMessageHandler { image_data: image.image_data.clone(), mime: image.mime.clone(), }), - LayerDataType::Imaginate(imaginate) => { - if let Some(data) = &imaginate.image_data { - image_data.push(FrontendImageData { - path: path.clone(), - image_data: data.image_data.clone(), - mime: imaginate.mime.clone(), - }); - } - } LayerDataType::NodeGraphFrame(node_graph_frame) => { if let Some(data) = &node_graph_frame.image_data { image_data.push(FrontendImageData { diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index 5f38dc72b..6f8a0d772 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -1,5 +1,7 @@ use crate::messages::prelude::*; + use graph_craft::document::{value::TaggedValue, NodeId}; +use graphene::LayerId; #[remain::sorted] #[impl_message(Message, DocumentMessage, NodeGraph)] @@ -49,4 +51,10 @@ pub enum NodeGraphMessage { input_index: usize, value: TaggedValue, }, + SetQualifiedInputValue { + layer_path: Vec, + node_path: Vec, + input_index: usize, + value: TaggedValue, + }, } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index a816f44a1..8f77c5621 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -1,6 +1,7 @@ use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::widgets::button_widgets::BreadcrumbTrailButtons; use crate::messages::prelude::*; + use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, DocumentNodeMetadata, NodeId, NodeInput, NodeNetwork}; use graphene::document::Document; @@ -9,6 +10,7 @@ use graphene::layers::nodegraph_layer::NodeGraphFrameLayer; mod document_node_types; mod node_properties; +pub use self::document_node_types::*; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub enum FrontendGraphDataType { @@ -19,30 +21,28 @@ pub enum FrontendGraphDataType { Raster, #[serde(rename = "color")] Color, - #[serde(rename = "text")] + #[serde(rename = "number")] Text, #[serde(rename = "vector")] Subpath, #[serde(rename = "number")] Number, - #[serde(rename = "boolean")] + #[serde(rename = "number")] Boolean, - #[serde(rename = "string")] - String, #[serde(rename = "vec2")] Vector, } impl FrontendGraphDataType { pub const fn with_tagged_value(value: &TaggedValue) -> Self { match value { - TaggedValue::None => Self::General, - TaggedValue::String(_) => Self::String, + TaggedValue::String(_) => Self::Text, TaggedValue::F32(_) | TaggedValue::F64(_) | TaggedValue::U32(_) => Self::Number, TaggedValue::Bool(_) => Self::Boolean, TaggedValue::DVec2(_) => Self::Vector, TaggedValue::Image(_) => Self::Raster, TaggedValue::Color(_) => Self::Color, TaggedValue::RcSubpath(_) | TaggedValue::Subpath(_) => Self::Subpath, + _ => Self::General, } } } @@ -157,7 +157,7 @@ impl NodeGraphMessageHandler { ); } - pub fn collate_properties(&self, node_graph_frame: &NodeGraphFrameLayer) -> Vec { + pub fn collate_properties(&self, node_graph_frame: &NodeGraphFrameLayer, context: &mut NodePropertiesContext) -> Vec { let mut network = &node_graph_frame.network; for segement in &self.nested_path { network = network.nodes.get(segement).and_then(|node| node.implementation.get_network()).unwrap(); @@ -169,7 +169,7 @@ impl NodeGraphMessageHandler { continue; }; - section.push(node_properties::generate_node_properties(document_node, *node_id)); + section.push(node_properties::generate_node_properties(document_node, *node_id, context)); } section @@ -177,7 +177,6 @@ impl NodeGraphMessageHandler { fn send_graph(network: &NodeNetwork, responses: &mut VecDeque) { responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into()); - info!("Opening node graph with nodes {:?}", network.nodes); // List of links in format (link_start, link_end, link_end_input_index) let links = network @@ -227,7 +226,7 @@ impl NodeGraphMessageHandler { position: node.metadata.position, }) } - log::debug!("Nodes:\n{:#?}\n\nFrontend Nodes:\n{:#?}\n\nLinks:\n{:#?}", network.nodes, nodes, links); + log::debug!("Frontend Nodes:\n{:#?}\n\nLinks:\n{:#?}", nodes, links); responses.push_back(FrontendMessage::UpdateNodeGraph { nodes, links }.into()); } @@ -288,7 +287,6 @@ impl MessageHandler { if let Some(_old_layer_path) = self.layer_path.take() { - info!("Closing node graph"); responses.push_back(FrontendMessage::UpdateNodeGraphVisibility { visible: false }.into()); responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into()); // TODO: Close UI and clean up old node graph @@ -315,7 +313,6 @@ impl MessageHandler { + let mut network = document.layer_mut(&layer_path).ok().and_then(|layer| match &mut layer.data { + LayerDataType::NodeGraphFrame(n) => Some(&mut n.network), + _ => None, + }); + + let Some((node_id, node_path)) = node_path.split_last() else { + error!("Node path is empty"); + return + }; + for segement in node_path { + network = network.and_then(|network| network.nodes.get_mut(segement)).and_then(|node| node.implementation.get_network_mut()); + } + + if let Some(network) = network { + if let Some(node) = network.nodes.get_mut(node_id) { + // Extend number of inputs if not already large enough + if input_index >= node.inputs.len() { + node.inputs.extend(((node.inputs.len() - 1)..input_index).map(|_| NodeInput::Network)); + } + node.inputs[input_index] = NodeInput::Value { tagged_value: value, exposed: false }; + responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into()); + } + } + } } } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs index 8803829c6..143d1a3a2 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs @@ -1,13 +1,15 @@ use super::{node_properties, FrontendGraphDataType, FrontendNodeType}; -use crate::messages::layout::utility_types::layout_widget::{LayoutGroup, Widget, WidgetHolder}; -use crate::messages::layout::utility_types::widgets::label_widgets::TextLabel; +use crate::messages::layout::utility_types::layout_widget::LayoutGroup; use graph_craft::concrete; -use graph_craft::document::value::TaggedValue; +use graph_craft::document::value::*; use graph_craft::document::{DocumentNode, NodeId, NodeInput}; +use graph_craft::imaginate_input::ImaginateSamplingMethod; use graph_craft::proto::{NodeIdentifier, Type}; use graphene_core::raster::Image; +use std::collections::VecDeque; + pub struct DocumentInputType { pub name: &'static str, pub data_type: FrontendGraphDataType, @@ -30,13 +32,21 @@ impl DocumentInputType { } } +pub struct NodePropertiesContext<'a> { + pub persistent_data: &'a crate::messages::portfolio::utility_types::PersistentData, + pub document: &'a graphene::document::Document, + pub responses: &'a mut VecDeque, + pub layer_path: &'a [graphene::LayerId], + pub nested_path: &'a [NodeId], +} + pub struct DocumentNodeType { pub name: &'static str, pub category: &'static str, pub identifier: NodeIdentifier, pub inputs: &'static [DocumentInputType], pub outputs: &'static [FrontendGraphDataType], - pub properties: fn(&DocumentNode, NodeId) -> Vec, + pub properties: fn(&DocumentNode, NodeId, &mut NodePropertiesContext) -> Vec, } // TODO: Dynamic node library @@ -51,14 +61,7 @@ static DOCUMENT_NODE_TYPES: &[DocumentNodeType] = &[ default: NodeInput::Node(0), }], outputs: &[FrontendGraphDataType::General], - properties: |_document_node, _node_id| { - vec![LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "The identity node simply returns the input".to_string(), - ..Default::default() - }))], - }] - }, + properties: |_document_node, _node_id, _context| node_properties::string_properties("The identity node simply returns the input".to_string()), }, DocumentNodeType { name: "Input", @@ -70,7 +73,7 @@ static DOCUMENT_NODE_TYPES: &[DocumentNodeType] = &[ default: NodeInput::Network, }], outputs: &[FrontendGraphDataType::Raster], - properties: |_document_node, _node_id| node_properties::string_properties("The input to the graph is the bitmap under the frame".to_string()), + properties: |_document_node, _node_id, _context| node_properties::string_properties("The input to the graph is the bitmap under the frame".to_string()), }, DocumentNodeType { name: "Output", @@ -82,7 +85,7 @@ static DOCUMENT_NODE_TYPES: &[DocumentNodeType] = &[ default: NodeInput::value(TaggedValue::Image(Image::empty()), true), }], outputs: &[], - properties: |_document_node, _node_id| node_properties::string_properties("The output to the graph is rendered in the frame".to_string()), + properties: |_document_node, _node_id, _context| node_properties::string_properties("The output to the graph is rendered in the frame".to_string()), }, DocumentNodeType { name: "Grayscale", @@ -184,6 +187,7 @@ static DOCUMENT_NODE_TYPES: &[DocumentNodeType] = &[ outputs: &[FrontendGraphDataType::Raster], properties: node_properties::exposure_properties, }, + IMAGINATE_NODE, DocumentNodeType { name: "Add", category: "Math", @@ -278,6 +282,36 @@ static DOCUMENT_NODE_TYPES: &[DocumentNodeType] = &[ },*/ ]; +pub const IMAGINATE_NODE: DocumentNodeType = DocumentNodeType { + name: "Imaginate", + category: "Image Synthesis", + identifier: NodeIdentifier::new("graphene_std::raster::ImaginateNode", &[concrete!("&TypeErasedNode")]), + inputs: &[ + DocumentInputType::new("Base Image", TaggedValue::Image(Image::empty()), true), + DocumentInputType::new("Seed", TaggedValue::F64(0.), false), + DocumentInputType::new("Resolution", TaggedValue::OptionalDVec2(None), false), + DocumentInputType::new("Samples", TaggedValue::F64(30.), false), + DocumentInputType::new("Sampling Method", TaggedValue::ImaginateSamplingMethod(ImaginateSamplingMethod::EulerA), false), + DocumentInputType::new("Text Guidance", TaggedValue::F64(10.), false), + DocumentInputType::new("Text Prompt", TaggedValue::String(String::new()), false), + DocumentInputType::new("Negative Prompt", TaggedValue::String(String::new()), false), + DocumentInputType::new("Use Base Image", TaggedValue::Bool(false), false), + DocumentInputType::new("Image Creativity", TaggedValue::F64(66.), false), + DocumentInputType::new("Masking Layer", TaggedValue::LayerPath(None), false), + DocumentInputType::new("Inpaint", TaggedValue::Bool(true), false), + DocumentInputType::new("Mask Blur", TaggedValue::F64(4.), false), + DocumentInputType::new("Mask Starting Fill", TaggedValue::ImaginateMaskStartingFill(ImaginateMaskStartingFill::Fill), false), + DocumentInputType::new("Improve Faces", TaggedValue::Bool(false), false), + DocumentInputType::new("Tiling", TaggedValue::Bool(false), false), + // Non-user status (is document input the right way to do this?) + DocumentInputType::new("Cached Data", TaggedValue::RcImage(None), false), + DocumentInputType::new("Percent Complete", TaggedValue::F64(0.), false), + DocumentInputType::new("Status", TaggedValue::ImaginateStatus(ImaginateStatus::Idle), false), + ], + outputs: &[FrontendGraphDataType::Raster], + properties: node_properties::imaginate_properties, +}; + pub fn resolve_document_node_type(name: &str) -> Option<&DocumentNodeType> { DOCUMENT_NODE_TYPES.iter().find(|node| node.name == name) } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index e68efd70b..29ff78dfe 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -1,13 +1,17 @@ -use crate::messages::layout::utility_types::layout_widget::{LayoutGroup, Widget, WidgetCallback, WidgetHolder}; -use crate::messages::layout::utility_types::widgets::button_widgets::ParameterExposeButton; -use crate::messages::layout::utility_types::widgets::input_widgets::{NumberInput, NumberInputMode, TextInput}; -use crate::messages::prelude::NodeGraphMessage; +use crate::messages::layout::utility_types::layout_widget::*; +use crate::messages::layout::utility_types::widgets::{button_widgets::*, input_widgets::*}; +use crate::messages::portfolio::utility_types::ImaginateServerStatus; +use crate::messages::prelude::*; use glam::DVec2; use graph_craft::document::value::TaggedValue; -use graph_craft::document::{DocumentNode, NodeId, NodeInput}; +use graph_craft::document::{generate_uuid, DocumentNode, NodeId, NodeInput}; +use graph_craft::imaginate_input::*; +use graphene::layers::layer_info::LayerDataTypeDiscriminant; +use graphene::Operation; -use super::FrontendGraphDataType; +use super::document_node_types::NodePropertiesContext; +use super::{FrontendGraphDataType, IMAGINATE_NODE}; pub fn string_properties(text: impl Into) -> Vec { let widget = WidgetHolder::text_widget(text); @@ -15,11 +19,11 @@ pub fn string_properties(text: impl Into) -> Vec { } fn update_value TaggedValue + 'static + Send + Sync>(value: F, node_id: NodeId, input_index: usize) -> WidgetCallback { - WidgetCallback::new(move |number_input: &T| { + WidgetCallback::new(move |input_value: &T| { NodeGraphMessage::SetInputValue { node: node_id, input_index, - value: value(number_input), + value: value(input_value), } .into() }) @@ -29,7 +33,7 @@ fn expose_widget(node_id: NodeId, index: usize, data_type: FrontendGraphDataType WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton { exposed, data_type, - tooltip: "Expose input parameter in node graph".into(), + tooltip: "Expose this parameter input in node graph".into(), on_update: WidgetCallback::new(move |_parameter| { NodeGraphMessage::ExposeInput { node_id, @@ -42,14 +46,26 @@ fn expose_widget(node_id: NodeId, index: usize, data_type: FrontendGraphDataType })) } -fn text_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str) -> Vec { - let input: &NodeInput = document_node.inputs.get(index).unwrap(); - +fn start_widgets(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, data_type: FrontendGraphDataType, blank_assist: bool) -> Vec { + let input = document_node.inputs.get(index).unwrap(); let mut widgets = vec![ - expose_widget(node_id, index, FrontendGraphDataType::Number, input.is_exposed()), - WidgetHolder::unrelated_seperator(), + expose_widget(node_id, index, data_type, input.is_exposed()), + WidgetHolder::unrelated_separator(), WidgetHolder::text_widget(name), ]; + if blank_assist { + widgets.extend_from_slice(&[ + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), // TODO: This last one is the separator after the 24px assist. + ]); + } + widgets +} + +fn text_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> Vec { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Text, blank_assist); if let NodeInput::Value { tagged_value: TaggedValue::String(x), @@ -57,7 +73,7 @@ fn text_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name } = &document_node.inputs[index] { widgets.extend_from_slice(&[ - WidgetHolder::unrelated_seperator(), + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::TextInput(TextInput { value: x.clone(), on_update: update_value(|x: &TextInput| TaggedValue::String(x.value.clone()), node_id, index), @@ -68,14 +84,48 @@ fn text_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name widgets } -fn number_range_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, range_min: Option, range_max: Option, unit: String, is_integer: bool) -> Vec { - let input: &NodeInput = document_node.inputs.get(index).unwrap(); +fn text_area_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> Vec { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Text, blank_assist); - let mut widgets = vec![ - expose_widget(node_id, index, FrontendGraphDataType::Number, input.is_exposed()), - WidgetHolder::unrelated_seperator(), - WidgetHolder::text_widget(name), - ]; + if let NodeInput::Value { + tagged_value: TaggedValue::String(x), + exposed: false, + } = &document_node.inputs[index] + { + widgets.extend_from_slice(&[ + WidgetHolder::unrelated_separator(), + WidgetHolder::new(Widget::TextAreaInput(TextAreaInput { + value: x.clone(), + on_update: update_value(|x: &TextAreaInput| TaggedValue::String(x.value.clone()), node_id, index), + ..TextAreaInput::default() + })), + ]) + } + widgets +} + +fn bool_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> Vec { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Boolean, blank_assist); + + if let NodeInput::Value { + tagged_value: TaggedValue::Bool(x), + exposed: false, + } = &document_node.inputs[index] + { + widgets.extend_from_slice(&[ + WidgetHolder::unrelated_separator(), + WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { + checked: *x, + on_update: update_value(|x: &CheckboxInput| TaggedValue::Bool(x.checked), node_id, index), + ..CheckboxInput::default() + })), + ]) + } + widgets +} + +fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, number_props: NumberInput, blank_assist: bool) -> Vec { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Number, blank_assist); if let NodeInput::Value { tagged_value: TaggedValue::F64(x), @@ -83,26 +133,21 @@ fn number_range_widget(document_node: &DocumentNode, node_id: NodeId, index: usi } = document_node.inputs[index] { widgets.extend_from_slice(&[ - WidgetHolder::unrelated_seperator(), + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(x), - mode: if range_max.is_some() { NumberInputMode::Range } else { NumberInputMode::Increment }, - range_min, - range_max, - unit, - is_integer, on_update: update_value(|x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, index), - ..NumberInput::default() + ..number_props })), ]) } widgets } -pub fn adjust_hsl_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec { - let hue_shift = number_range_widget(document_node, node_id, 1, "Hue Shift", Some(-180.), Some(180.), "°".into(), false); - let saturation_shift = number_range_widget(document_node, node_id, 2, "Saturation Shift", Some(-100.), Some(100.), "%".into(), false); - let lightness_shift = number_range_widget(document_node, node_id, 3, "Lightness Shift", Some(-100.), Some(100.), "%".into(), false); +pub fn adjust_hsl_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let hue_shift = number_widget(document_node, node_id, 1, "Hue Shift", NumberInput::new().min(-180.).max(180.).unit("°"), true); + let saturation_shift = number_widget(document_node, node_id, 2, "Saturation Shift", NumberInput::new().min(-100.).max(100.).unit("%"), true); + let lightness_shift = number_widget(document_node, node_id, 3, "Lightness Shift", NumberInput::new().min(-100.).max(100.).unit("%"), true); vec![ LayoutGroup::Row { widgets: hue_shift }, @@ -111,83 +156,57 @@ pub fn adjust_hsl_properties(document_node: &DocumentNode, node_id: NodeId) -> V ] } -pub fn brighten_image_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec { - let brightness = number_range_widget(document_node, node_id, 1, "Brightness", Some(-255.), Some(255.), "".into(), false); - let contrast = number_range_widget(document_node, node_id, 2, "Contrast", Some(-255.), Some(255.), "".into(), false); +pub fn brighten_image_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let brightness = number_widget(document_node, node_id, 1, "Brightness", NumberInput::new().min(-255.).max(255.), true); + let contrast = number_widget(document_node, node_id, 2, "Contrast", NumberInput::new().min(-255.).max(255.), true); vec![LayoutGroup::Row { widgets: brightness }, LayoutGroup::Row { widgets: contrast }] } -pub fn adjust_gamma_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec { - let gamma = number_range_widget(document_node, node_id, 1, "Gamma", Some(0.01), None, "".into(), false); +pub fn adjust_gamma_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let gamma = number_widget(document_node, node_id, 1, "Gamma", NumberInput::new().min(0.01), true); vec![LayoutGroup::Row { widgets: gamma }] } -pub fn gpu_map_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec { - let map = text_widget(document_node, node_id, 1, "Map"); +pub fn gpu_map_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let map = text_widget(document_node, node_id, 1, "Map", true); vec![LayoutGroup::Row { widgets: map }] } -pub fn multiply_opacity(document_node: &DocumentNode, node_id: NodeId) -> Vec { - let gamma = number_range_widget(document_node, node_id, 1, "Factor", Some(0.), Some(1.), "".into(), false); +pub fn multiply_opacity(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let gamma = number_widget(document_node, node_id, 1, "Factor", NumberInput::new().min(0.).max(1.), true); vec![LayoutGroup::Row { widgets: gamma }] } -pub fn posterize_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec { - let value = number_range_widget(document_node, node_id, 1, "Levels", Some(2.), Some(255.), "".into(), true); +pub fn posterize_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let value = number_widget(document_node, node_id, 1, "Levels", NumberInput::new().min(2.).max(255.).int(), true); vec![LayoutGroup::Row { widgets: value }] } -pub fn exposure_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec { - let value = number_range_widget(document_node, node_id, 1, "Value", Some(-3.), Some(3.), "".into(), false); +pub fn exposure_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let value = number_widget(document_node, node_id, 1, "Value", NumberInput::new().min(-3.).max(3.), true); vec![LayoutGroup::Row { widgets: value }] } -pub fn add_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec { +pub fn add_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let operand = |name: &str, index| { - let input: &NodeInput = document_node.inputs.get(index).unwrap(); - let mut widgets = vec![ - expose_widget(node_id, index, FrontendGraphDataType::Number, input.is_exposed()), - WidgetHolder::unrelated_seperator(), - WidgetHolder::text_widget(name), - ]; - - if let NodeInput::Value { - tagged_value: TaggedValue::F64(x), - exposed: false, - } = document_node.inputs[index] - { - widgets.extend_from_slice(&[ - WidgetHolder::unrelated_seperator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(x), - mode: NumberInputMode::Increment, - on_update: update_value(|number_input: &NumberInput| TaggedValue::F64(number_input.value.unwrap()), node_id, index), - ..NumberInput::default() - })), - ]); - } + let widgets = number_widget(document_node, node_id, index, name, NumberInput::new(), true); LayoutGroup::Row { widgets } }; vec![operand("Input", 0), operand("Addend", 1)] } -pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec { +pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext, blank_assist: bool) -> Vec { let translation = { let index = 1; - let input: &NodeInput = document_node.inputs.get(index).unwrap(); - let mut widgets = vec![ - expose_widget(node_id, index, FrontendGraphDataType::Vector, input.is_exposed()), - WidgetHolder::unrelated_seperator(), - WidgetHolder::text_widget("Translation"), - ]; + let mut widgets = start_widgets(document_node, node_id, index, "Translation", FrontendGraphDataType::Vector, blank_assist); if let NodeInput::Value { tagged_value: TaggedValue::DVec2(vec2), @@ -195,7 +214,7 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId) -> V } = document_node.inputs[index] { widgets.extend_from_slice(&[ - WidgetHolder::unrelated_seperator(), + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(vec2.x), label: "X".into(), @@ -203,7 +222,7 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId) -> V on_update: update_value(move |number_input: &NumberInput| TaggedValue::DVec2(DVec2::new(number_input.value.unwrap(), vec2.y)), node_id, index), ..NumberInput::default() })), - WidgetHolder::unrelated_seperator(), + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(vec2.y), label: "Y".into(), @@ -219,13 +238,8 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId) -> V let rotation = { let index = 2; - let input: &NodeInput = document_node.inputs.get(index).unwrap(); - let mut widgets = vec![ - expose_widget(node_id, index, FrontendGraphDataType::Number, input.is_exposed()), - WidgetHolder::unrelated_seperator(), - WidgetHolder::text_widget("Rotation"), - ]; + let mut widgets = start_widgets(document_node, node_id, index, "Rotation", FrontendGraphDataType::Number, blank_assist); if let NodeInput::Value { tagged_value: TaggedValue::F64(val), @@ -233,7 +247,7 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId) -> V } = document_node.inputs[index] { widgets.extend_from_slice(&[ - WidgetHolder::unrelated_seperator(), + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(val.to_degrees()), unit: "°".into(), @@ -251,13 +265,8 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId) -> V let scale = { let index = 3; - let input: &NodeInput = document_node.inputs.get(index).unwrap(); - let mut widgets = vec![ - expose_widget(node_id, index, FrontendGraphDataType::Vector, input.is_exposed()), - WidgetHolder::unrelated_seperator(), - WidgetHolder::text_widget("Scale"), - ]; + let mut widgets = start_widgets(document_node, node_id, index, "Scale", FrontendGraphDataType::Vector, blank_assist); if let NodeInput::Value { tagged_value: TaggedValue::DVec2(vec2), @@ -265,7 +274,7 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId) -> V } = document_node.inputs[index] { widgets.extend_from_slice(&[ - WidgetHolder::unrelated_seperator(), + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(vec2.x), label: "X".into(), @@ -273,7 +282,7 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId) -> V on_update: update_value(move |number_input: &NumberInput| TaggedValue::DVec2(DVec2::new(number_input.value.unwrap(), vec2.y)), node_id, index), ..NumberInput::default() })), - WidgetHolder::unrelated_seperator(), + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(vec2.y), label: "Y".into(), @@ -289,18 +298,559 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId) -> V vec![translation, rotation, scale] } +pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { + let imaginate_node = [context.nested_path, &[node_id]].concat(); + let imaginate_node_1 = imaginate_node.clone(); + let layer_path = context.layer_path.to_vec(); + + let resolve_input = |name: &str| IMAGINATE_NODE.inputs.iter().position(|input| input.name == name).unwrap_or_else(|| panic!("Input {name} not found")); + let seed_index = resolve_input("Seed"); + let resolution_index = resolve_input("Resolution"); + let samples_index = resolve_input("Samples"); + let sampling_method_index = resolve_input("Sampling Method"); + let text_guidance_index = resolve_input("Text Guidance"); + let text_index = resolve_input("Text Prompt"); + let neg_index = resolve_input("Negative Prompt"); + let base_img_index = resolve_input("Use Base Image"); + let img_creativity_index = resolve_input("Image Creativity"); + let mask_index = resolve_input("Masking Layer"); + let inpaint_index = resolve_input("Inpaint"); + let mask_blur_index = resolve_input("Mask Blur"); + let mask_fill_index = resolve_input("Mask Starting Fill"); + let faces_index = resolve_input("Improve Faces"); + let tiling_index = resolve_input("Tiling"); + let cached_index = resolve_input("Cached Data"); + + let cached_value = &document_node.inputs[cached_index]; + let complete_value = &document_node.inputs[resolve_input("Percent Complete")]; + let status_value = &document_node.inputs[resolve_input("Status")]; + + let server_status = { + let status = match &context.persistent_data.imaginate_server_status { + ImaginateServerStatus::Unknown => { + context.responses.push_back(PortfolioMessage::ImaginateCheckServerStatus.into()); + "Checking..." + } + ImaginateServerStatus::Checking => "Checking...", + ImaginateServerStatus::Unavailable => "Unavailable", + ImaginateServerStatus::Connected => "Connected", + }; + let widgets = vec![ + WidgetHolder::text_widget("Server"), + WidgetHolder::unrelated_separator(), + WidgetHolder::new(Widget::IconButton(IconButton { + size: 24, + icon: "Settings".into(), + tooltip: "Preferences: Imaginate".into(), + on_update: WidgetCallback::new(|_| DialogMessage::RequestPreferencesDialog.into()), + ..Default::default() + })), + WidgetHolder::unrelated_separator(), + WidgetHolder::bold_text(status), + WidgetHolder::related_separator(), + 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 }.with_tooltip("Connection status to the server that computes generated images") + }; + + let &NodeInput::Value {tagged_value: TaggedValue::ImaginateStatus( imaginate_status),..} = status_value else{ + panic!("Invalid status input") + }; + let NodeInput::Value {tagged_value: TaggedValue::RcImage( cached_data),..} = cached_value else{ + panic!("Invalid cached image input") + }; + let &NodeInput::Value {tagged_value: TaggedValue::F64( percent_complete),..} = complete_value else{ + panic!("Invalid percent complete input") + }; + let use_base_image = if let &NodeInput::Value { + tagged_value: TaggedValue::Bool(use_base_image), + .. + } = &document_node.inputs[base_img_index] + { + use_base_image + } else { + true + }; + let progress = { + // 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_status; + if imaginate_status == ImaginateStatus::Idle && cached_data.is_some() && percent_complete > 0. && percent_complete < 100. { + interpreted_status = ImaginateStatus::Terminated; + } + + let status = match interpreted_status { + ImaginateStatus::Idle => match cached_data { + Some(_) => "Done".into(), + None => "Ready".into(), + }, + ImaginateStatus::Beginning => "Beginning...".into(), + ImaginateStatus::Uploading(percent) => format!("Uploading Base Image: {percent:.0}%"), + ImaginateStatus::Generating => format!("Generating: {percent_complete:.0}%"), + ImaginateStatus::Terminating => "Terminating...".into(), + ImaginateStatus::Terminated => format!("{percent_complete:.0}% (Terminated)"), + }; + let widgets = vec![ + WidgetHolder::text_widget("Progress"), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), + WidgetHolder::bold_text(status), + ]; + LayoutGroup::Row { widgets }.with_tooltip("When generating, the percentage represents how many sampling steps have so far been processed out of the target number") + }; + + let image_controls = { + let mut widgets = vec![WidgetHolder::text_widget("Image"), WidgetHolder::unrelated_separator()]; + let assist_separators = vec![ + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), + ]; + + match imaginate_status { + ImaginateStatus::Beginning | ImaginateStatus::Uploading(_) => { + widgets.extend_from_slice(&assist_separators); + widgets.push(WidgetHolder::new(Widget::TextButton(TextButton { + label: "Beginning...".into(), + tooltip: "Sending image generation request to the server".into(), + disabled: true, + ..Default::default() + }))); + } + ImaginateStatus::Generating => { + widgets.extend_from_slice(&assist_separators); + widgets.push(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(move |_| { + DocumentMessage::NodeGraphFrameImaginateTerminate { + layer_path: layer_path.clone(), + node_path: imaginate_node.clone(), + } + .into() + }), + ..Default::default() + }))); + } + ImaginateStatus::Terminating => { + widgets.extend_from_slice(&assist_separators); + widgets.push(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 => widgets.extend_from_slice(&[ + WidgetHolder::new(Widget::IconButton(IconButton { + size: 24, + icon: "Random".into(), + tooltip: "Generate with a new random seed".into(), + on_update: WidgetCallback::new(move |_| { + DocumentMessage::NodeGraphFrameImaginateRandom { + imaginate_node: imaginate_node.clone(), + } + .into() + }), + ..Default::default() + })), + WidgetHolder::unrelated_separator(), + WidgetHolder::new(Widget::TextButton(TextButton { + label: "Generate".into(), + tooltip: "Fill layer frame by generating a new image".into(), + on_update: WidgetCallback::new(move |_| { + DocumentMessage::NodeGraphFrameImaginate { + imaginate_node: imaginate_node_1.clone(), + } + .into() + }), + ..Default::default() + })), + WidgetHolder::related_separator(), + WidgetHolder::new(Widget::TextButton(TextButton { + label: "Clear".into(), + tooltip: "Remove generated image from the layer frame".into(), + disabled: cached_data.is_none(), + on_update: update_value(|_| TaggedValue::RcImage(None), node_id, cached_index), + ..Default::default() + })), + ]), + } + LayoutGroup::Row { widgets }.with_tooltip("Buttons that control the image generation process") + }; + + // Requires custom layout for the regenerate button + let seed = { + let mut widgets = start_widgets(document_node, node_id, seed_index, "Seed", FrontendGraphDataType::Number, false); + + if let &NodeInput::Value { + tagged_value: TaggedValue::F64(seed), + exposed: false, + } = &document_node.inputs[seed_index] + { + widgets.extend_from_slice(&[ + WidgetHolder::unrelated_separator(), + WidgetHolder::new(Widget::IconButton(IconButton { + size: 24, + icon: "Regenerate".into(), + tooltip: "Set a new random seed".into(), + on_update: update_value(move |_| TaggedValue::F64((generate_uuid() >> 1) as f64), node_id, seed_index), + ..Default::default() + })), + WidgetHolder::unrelated_separator(), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: Some(seed), + min: Some(0.), + is_integer: true, + on_update: update_value(move |input: &NumberInput| TaggedValue::F64(input.value.unwrap()), node_id, seed_index), + ..Default::default() + })), + ]) + } + // Note: Limited by f64. You cannot even have all the possible u64 values :) + LayoutGroup::Row { widgets }.with_tooltip("Seed determines the random outcome, enabling limitless unique variations") + }; + + let resolution = { + use graphene::document::pick_safe_imaginate_resolution; + + let mut widgets = start_widgets(document_node, node_id, resolution_index, "Resolution", FrontendGraphDataType::Vector, false); + + let round = |x: DVec2| { + let (x, y) = pick_safe_imaginate_resolution(x.into()); + Some(DVec2::new(x as f64, y as f64)) + }; + + if let &NodeInput::Value { + tagged_value: TaggedValue::OptionalDVec2(vec2), + exposed: false, + } = &document_node.inputs[resolution_index] + { + let dimensions_is_auto = vec2.is_none(); + let vec2 = vec2.unwrap_or_else(|| { + let transform = context.document.root.transform.inverse() * context.document.multiply_transforms(context.layer_path).unwrap(); + let w = transform.transform_vector2(DVec2::new(1., 0.)).length(); + let h = transform.transform_vector2(DVec2::new(0., 1.)).length(); + + let (x, y) = pick_safe_imaginate_resolution((w, h)); + + DVec2::new(x as f64, y as f64) + }); + + let layer_path = context.layer_path.to_vec(); + widgets.extend_from_slice(&[ + WidgetHolder::unrelated_separator(), + WidgetHolder::new(Widget::IconButton(IconButton { + size: 24, + icon: "Rescale".into(), + tooltip: "Set the Node Graph Frame layer dimensions to this resolution".into(), + on_update: WidgetCallback::new(move |_| { + Operation::SetLayerScaleAroundPivot { + path: layer_path.clone(), + new_scale: vec2.into(), + } + .into() + }), + ..Default::default() + })), + WidgetHolder::unrelated_separator(), + WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { + checked: !dimensions_is_auto, + icon: "Edit".into(), + tooltip: "Set a custom resolution instead of using the frame's rounded dimensions".into(), + on_update: update_value( + move |checkbox_input: &CheckboxInput| { + if checkbox_input.checked { + TaggedValue::OptionalDVec2(Some(vec2)) + } else { + TaggedValue::OptionalDVec2(None) + } + }, + node_id, + resolution_index, + ), + ..CheckboxInput::default() + })), + WidgetHolder::unrelated_separator(), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: Some(vec2.x), + label: "W".into(), + unit: " px".into(), + disabled: dimensions_is_auto, + on_update: update_value( + move |number_input: &NumberInput| TaggedValue::OptionalDVec2(round(DVec2::new(number_input.value.unwrap(), vec2.y))), + node_id, + resolution_index, + ), + ..NumberInput::default() + })), + WidgetHolder::related_separator(), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: Some(vec2.y), + label: "H".into(), + unit: " px".into(), + disabled: dimensions_is_auto, + on_update: update_value( + move |number_input: &NumberInput| TaggedValue::OptionalDVec2(round(DVec2::new(vec2.x, number_input.value.unwrap()))), + node_id, + resolution_index, + ), + ..NumberInput::default() + })), + ]) + } + LayoutGroup::Row { widgets }.with_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.\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.") + }; + + let sampling_steps = { + let widgets = number_widget(document_node, node_id, samples_index, "Sampling Steps", NumberInput::new().min(0.).max(150.).int(), true); + LayoutGroup::Row { widgets }.with_tooltip("Number of iterations to improve the image generation quality, with diminishing returns around 40 when using the Euler A sampling method") + }; + + let sampling_method = { + let mut widgets = start_widgets(document_node, node_id, sampling_method_index, "Sampling Method", FrontendGraphDataType::General, true); + + if let &NodeInput::Value { + tagged_value: TaggedValue::ImaginateSamplingMethod(sampling_method), + exposed: false, + } = &document_node.inputs[sampling_method_index] + { + 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: update_value(move |_| TaggedValue::ImaginateSamplingMethod(method), node_id, sampling_method_index), + ..DropdownEntryData::default() + }); + } + let entries = vec![entries]; + + widgets.extend_from_slice(&[ + WidgetHolder::unrelated_separator(), + WidgetHolder::new(Widget::DropdownInput(DropdownInput { + entries, + selected_index: Some(sampling_method as u32), + ..Default::default() + })), + ]); + } + LayoutGroup::Row { widgets }.with_tooltip("Algorithm used to generate the image during each sampling step") + }; + + let text_guidance = { + let widgets = number_widget(document_node, node_id, text_guidance_index, "Text Guidance", NumberInput::new().min(0.).max(30.), true); + LayoutGroup::Row { widgets }.with_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).", + ) + }; + + let text_prompt = { + let widgets = text_area_widget(document_node, node_id, text_index, "Text Prompt", true); + LayoutGroup::Row { widgets }.with_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\"", + ) + }; + let negative_prompt = { + let widgets = text_area_widget(document_node, node_id, neg_index, "Negative Prompt", true); + LayoutGroup::Row { widgets }.with_tooltip("A negative text prompt can be used to list things like objects or colors to avoid") + }; + let base_image = { + let widgets = bool_widget(document_node, node_id, base_img_index, "Use Base Image", true); + LayoutGroup::Row { widgets }.with_tooltip("Generate an image based upon some raster data") + }; + let image_creativity = { + let props = NumberInput::new().percentage().disabled(!use_base_image); + let widgets = number_widget(document_node, node_id, img_creativity_index, "Image Creativity", props, true); + LayoutGroup::Row { widgets }.with_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.", + ) + }; + + let mut layer_reference_input_layer_is_some = false; + let layer_mask = { + let mut widgets = start_widgets(document_node, node_id, mask_index, "Masking Layer", FrontendGraphDataType::General, true); + + if let NodeInput::Value { + tagged_value: TaggedValue::LayerPath(layer_path), + exposed: false, + } = &document_node.inputs[mask_index] + { + let layer_reference_input_layer = layer_path + .as_ref() + .and_then(|path| context.document.layer(path).ok()) + .map(|layer| (layer.name.clone().unwrap_or_default(), LayerDataTypeDiscriminant::from(&layer.data))); + + 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); + + widgets.extend_from_slice(&[ + WidgetHolder::unrelated_separator(), + WidgetHolder::new(Widget::LayerReferenceInput(LayerReferenceInput { + value: layer_path.clone(), + layer_name: layer_reference_input_layer_name.cloned(), + layer_type: layer_reference_input_layer_type.cloned(), + disabled: !use_base_image, + on_update: update_value(|input: &LayerReferenceInput| TaggedValue::LayerPath(input.value.clone()), node_id, mask_index), + ..Default::default() + })), + ]); + } + LayoutGroup::Row { widgets }.with_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.", + ) + }; + + let mut layout = vec![ + server_status, + progress, + image_controls, + seed, + resolution, + sampling_steps, + sampling_method, + text_guidance, + text_prompt, + negative_prompt, + base_image, + image_creativity, + layer_mask, + ]; + + if use_base_image && layer_reference_input_layer_is_some { + let in_paint = { + let mut widgets = start_widgets(document_node, node_id, inpaint_index, "Inpaint", FrontendGraphDataType::Boolean, true); + + if let &NodeInput::Value { + tagged_value: TaggedValue::Bool(in_paint), + exposed: false, + } = &document_node.inputs[inpaint_index] + { + widgets.extend_from_slice(&[ + WidgetHolder::unrelated_separator(), + WidgetHolder::new(Widget::RadioInput(RadioInput { + entries: [(true, "Inpaint"), (false, "Outpaint")] + .into_iter() + .map(|(paint, name)| RadioEntryData { + label: name.to_string(), + on_update: update_value(move |_| TaggedValue::Bool(paint), node_id, inpaint_index), + ..Default::default() + }) + .collect(), + selected_index: 1 - in_paint as u32, + ..Default::default() + })), + ]); + } + LayoutGroup::Row { widgets }.with_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.", + ) + }; + + let blur_radius = { + let widgets = number_widget(document_node, node_id, mask_blur_index, "Mask Blur", NumberInput::new().unit(" px").min(0.).max(25.).int(), true); + LayoutGroup::Row { widgets }.with_tooltip("Blur radius for the mask. Useful for softening sharp edges to blend the masked area with the rest of the image.") + }; + + let mask_starting_fill = { + let mut widgets = start_widgets(document_node, node_id, mask_fill_index, "Mask Starting Fill", FrontendGraphDataType::General, true); + + if let &NodeInput::Value { + tagged_value: TaggedValue::ImaginateMaskStartingFill(starting_fill), + exposed: false, + } = &document_node.inputs[mask_fill_index] + { + let mask_fill_content_modes = ImaginateMaskStartingFill::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: update_value(move |_| TaggedValue::ImaginateMaskStartingFill(mode), node_id, mask_fill_index), + ..DropdownEntryData::default() + }); + } + let entries = vec![entries]; + + widgets.extend_from_slice(&[ + WidgetHolder::unrelated_separator(), + WidgetHolder::new(Widget::DropdownInput(DropdownInput { + entries, + selected_index: Some(starting_fill as u32), + ..Default::default() + })), + ]); + } + LayoutGroup::Row { widgets }.with_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.", + ) + }; + layout.extend_from_slice(&[in_paint, blur_radius, mask_starting_fill]); + } + + let improve_faces = { + let widgets = bool_widget(document_node, node_id, faces_index, "Improve Faces", true); + LayoutGroup::Row { widgets }.with_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.", + ) + }; + let tiling = { + let widgets = bool_widget(document_node, node_id, tiling_index, "Tiling", true); + LayoutGroup::Row { widgets }.with_tooltip("Generate the image so its edges loop seamlessly to make repeatable patterns or textures") + }; + layout.extend_from_slice(&[improve_faces, tiling]); + + layout +} + fn unknown_node_properties(document_node: &DocumentNode) -> Vec { string_properties(format!("Node '{}' cannot be found in library", document_node.name)) } -pub fn no_properties(_document_node: &DocumentNode, _node_id: NodeId) -> Vec { +pub fn no_properties(_document_node: &DocumentNode, _node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { string_properties("Node has no properties") } -pub fn generate_node_properties(document_node: &DocumentNode, node_id: NodeId) -> LayoutGroup { +pub fn generate_node_properties(document_node: &DocumentNode, node_id: NodeId, context: &mut NodePropertiesContext) -> LayoutGroup { let name = document_node.name.clone(); let layout = match super::document_node_types::resolve_document_node_type(&name) { - Some(document_node_type) => (document_node_type.properties)(document_node, node_id), + Some(document_node_type) => (document_node_type.properties)(document_node, node_id, context), None => unknown_node_properties(document_node), }; LayoutGroup::Section { name, layout } 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 31067dd89..ff2ef43a1 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,6 @@ 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::{ImaginateMaskFillContent, ImaginateMaskPaintMode, ImaginateSamplingMethod}; use graphene::layers::style::{Fill, Stroke}; use graphene::LayerId; @@ -27,23 +26,6 @@ pub enum PropertiesPanelMessage { ModifyTransform { value: f64, transform_op: TransformOp }, ResendActiveProperties, 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 }, - SetImaginateSamples { samples: u32 }, - SetImaginateSamplingMethod { method: ImaginateSamplingMethod }, - SetImaginateScaleFromResolution, - SetImaginateSeed { seed: u64 }, - SetImaginateSeedRandomize, - SetImaginateSeedRandomizeAndGenerate, - SetImaginateTiling { tiling: bool }, - SetImaginateUseImg2Img { use_img2img: bool }, SetPivot { new_position: PivotPosition }, UpdateSelectedDocumentProperties, } 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 a2aaf068e..0e2f86155 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 @@ -1,6 +1,5 @@ use super::utility_functions::{register_artboard_layer_properties, register_artwork_layer_properties}; use super::utility_types::PropertiesPanelMessageHandlerData; -use crate::application::generate_uuid; use crate::messages::layout::utility_types::layout_widget::{Layout, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; use crate::messages::portfolio::document::properties_panel::utility_functions::apply_transform_operation; @@ -156,78 +155,6 @@ impl<'a> MessageHandler { - let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - responses.push_back(Operation::ImaginateSetPrompt { path, prompt }.into()); - } - SetImaginateNegativePrompt { negative_prompt } => { - let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - responses.push_back(Operation::ImaginateSetNegativePrompt { path, negative_prompt }.into()); - } - SetImaginateDenoisingStrength { denoising_strength } => { - 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()); - } - SetImaginateSamplingMethod { method } => { - let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - responses.push_back(Operation::SetImaginateSamplingMethod { path, method }.into()); - } - SetImaginateScaleFromResolution => { - let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - - responses.push_back(Operation::ImaginateSetScaleFromResolution { path }.into()); - } - SetImaginateSeed { seed } => { - let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - responses.push_back(Operation::ImaginateSetSeed { path, seed }.into()); - } - SetImaginateSeedRandomize => { - let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - let seed = generate_uuid(); - responses.push_back(Operation::ImaginateSetSeed { path, seed }.into()); - } - SetImaginateSeedRandomizeAndGenerate => { - let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - let seed = generate_uuid(); - responses.push_back(Operation::ImaginateSetSeed { path, seed }.into()); - responses.push_back(DocumentMessage::ImaginateGenerate.into()); - } - SetImaginateCfgScale { cfg_scale } => { - let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - responses.push_back(Operation::ImaginateSetCfgScale { path, cfg_scale }.into()); - } - SetImaginateUseImg2Img { use_img2img } => { - let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - responses.push_back(Operation::ImaginateSetUseImg2Img { path, use_img2img }.into()); - } - SetImaginateRestoreFaces { restore_faces } => { - let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - responses.push_back(Operation::ImaginateSetRestoreFaces { path, restore_faces }.into()); - } - SetImaginateTiling { tiling } => { - let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - responses.push_back(Operation::ImaginateSetTiling { path, tiling }.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 a7d8df0d1..8d4d6bdbc 100644 --- a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs +++ b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs @@ -4,16 +4,13 @@ use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, 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, 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::layout::utility_types::widgets::input_widgets::{ColorInput, FontInput, NumberInput, NumberInputMode, RadioEntryData, RadioInput, TextAreaInput, TextInput}; +use crate::messages::layout::utility_types::widgets::label_widgets::{IconLabel, TextLabel}; +use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::prelude::*; use graphene::color::Color; -use graphene::document::{pick_layer_safe_imaginate_resolution, Document}; -use graphene::layers::imaginate_layer::{ImaginateLayer, ImaginateMaskFillContent, ImaginateMaskPaintMode, ImaginateSamplingMethod, ImaginateStatus}; +use graphene::document::Document; use graphene::layers::layer_info::{Layer, LayerDataType, LayerDataTypeDiscriminant}; use graphene::layers::nodegraph_layer::NodeGraphFrameLayer; use graphene::layers::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke}; @@ -49,27 +46,18 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ tooltip: "Artboard".into(), ..Default::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::related_separator(), WidgetHolder::new(Widget::TextLabel(TextLabel { value: "Artboard".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::TextInput(TextInput { value: layer.name.clone().unwrap_or_else(|| "Untitled".to_string()), on_update: WidgetCallback::new(|text_input: &TextInput| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()), ..Default::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::related_separator(), WidgetHolder::new(Widget::PopoverButton(PopoverButton { header: "Options Bar".into(), text: "The contents of this popover menu are coming soon".into(), @@ -100,10 +88,11 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ value: "Location".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(layer.transform.x() + pivot.x), label: "X".into(), @@ -117,10 +106,7 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ }), ..NumberInput::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::related_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(layer.transform.y() + pivot.y), label: "Y".into(), @@ -142,10 +128,11 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ value: "Dimensions".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(layer.bounding_transform(&persistent_data.font_cache).scale_x()), label: "W".into(), @@ -161,10 +148,7 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ }), ..NumberInput::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::related_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(layer.bounding_transform(&persistent_data.font_cache).scale_y()), label: "H".into(), @@ -188,15 +172,16 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ value: "Background".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::ColorInput(ColorInput { value: Some(*color), on_update: WidgetCallback::new(|text_input: &ColorInput| { - let fill = if let Some(value) = text_input.value { Fill::Solid(value) } else { Fill::None }; - PropertiesPanelMessage::ModifyFill { fill }.into() + let fill = if let Some(value) = text_input.value { value } else { Color::TRANSPARENT }; + PropertiesPanelMessage::ModifyFill { fill: Fill::Solid(fill) }.into() }), no_transparency: true, ..Default::default() @@ -254,21 +239,13 @@ pub fn register_artwork_layer_properties( tooltip: "Image".into(), ..Default::default() })), - LayerDataType::Imaginate(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { - icon: "NodeImaginate".into(), - tooltip: "Imaginate".into(), - ..Default::default() - })), LayerDataType::NodeGraphFrame(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { icon: "NodeNodes".into(), tooltip: "Node Graph Frame".into(), ..Default::default() })), }, - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::related_separator(), WidgetHolder::new(Widget::TextLabel(TextLabel { value: match &layer.data { LayerDataType::NodeGraphFrame(_) => "Node Graph Frame".into(), @@ -276,19 +253,13 @@ pub fn register_artwork_layer_properties( }, ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::TextInput(TextInput { value: layer.name.clone().unwrap_or_else(|| "Untitled".to_string()), on_update: WidgetCallback::new(|text_input: &TextInput| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()), ..Default::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::related_separator(), WidgetHolder::new(Widget::PopoverButton(PopoverButton { header: "Options Bar".into(), text: "The contents of this popover menu are coming soon".into(), @@ -320,22 +291,23 @@ pub fn register_artwork_layer_properties( LayerDataType::Image(_) => { vec![node_section_transform(layer, persistent_data)] } - LayerDataType::Imaginate(imaginate) => { - 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(); let selected_nodes = &node_graph_message_handler.selected_nodes; let mut properties_sections = vec![ node_section_transform(layer, persistent_data), - node_section_node_graph_frame(layer_path, node_graph_frame, is_graph_open), + node_section_node_graph_frame(layer_path.clone(), node_graph_frame, is_graph_open), ]; if !selected_nodes.is_empty() && is_graph_open { - let parameters_sections = node_graph_message_handler.collate_properties(node_graph_frame); + let mut context = crate::messages::portfolio::document::node_graph::NodePropertiesContext { + persistent_data, + document, + responses, + nested_path: &node_graph_message_handler.nested_path, + layer_path: &layer_path, + }; + let parameters_sections = node_graph_message_handler.collate_properties(node_graph_frame, &mut context); properties_sections.extend(parameters_sections.into_iter()); } properties_sections @@ -372,19 +344,13 @@ fn node_section_transform(layer: &Layer, persistent_data: &PersistentData) -> La value: "Location".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::PivotAssist(PivotAssist { position: layer.pivot.into(), on_update: WidgetCallback::new(|pivot_assist: &PivotAssist| PropertiesPanelMessage::SetPivot { new_position: pivot_assist.position }.into()), ..Default::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(layer.transform.x() + pivot.x), label: "X".into(), @@ -398,10 +364,7 @@ fn node_section_transform(layer: &Layer, persistent_data: &PersistentData) -> La }), ..NumberInput::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::related_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(layer.transform.y() + pivot.y), label: "Y".into(), @@ -423,10 +386,11 @@ fn node_section_transform(layer: &Layer, persistent_data: &PersistentData) -> La value: "Rotation".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(layer.transform.rotation() * 180. / PI), unit: "°".into(), @@ -450,10 +414,11 @@ fn node_section_transform(layer: &Layer, persistent_data: &PersistentData) -> La value: "Scale".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(layer.transform.scale_x()), label: "X".into(), @@ -467,10 +432,7 @@ fn node_section_transform(layer: &Layer, persistent_data: &PersistentData) -> La }), ..NumberInput::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::related_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(layer.transform.scale_y()), label: "Y".into(), @@ -492,10 +454,11 @@ fn node_section_transform(layer: &Layer, persistent_data: &PersistentData) -> La value: "Dimensions".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(layer.bounding_transform(&persistent_data.font_cache).scale_x()), label: "W".into(), @@ -509,10 +472,7 @@ fn node_section_transform(layer: &Layer, persistent_data: &PersistentData) -> La }), ..NumberInput::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::related_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(layer.bounding_transform(&persistent_data.font_cache).scale_y()), label: "H".into(), @@ -532,708 +492,6 @@ fn node_section_transform(layer: &Layer, persistent_data: &PersistentData) -> La } } -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))); - - 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(); - - 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(); - - 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 = " - 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 { - 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 { LayoutGroup::Section { name: "Node Graph Frame".into(), @@ -1245,10 +503,11 @@ fn node_section_node_graph_frame(layer_path: Vec, node_graph_ tooltip: "Button to edit the node graph network for this layer".into(), ..Default::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::TextButton(TextButton { label: if open_graph { "Close Node Graph".into() } else { "Open Node Graph".into() }, tooltip: format!("{} the node graph associated with this layer", if open_graph { "Close" } else { "Open" }), @@ -1271,20 +530,18 @@ fn node_section_node_graph_frame(layer_path: Vec, node_graph_ tooltip: "Buttons to render the node graph and clear the last rendered image".into(), ..Default::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::TextButton(TextButton { label: "Render".into(), tooltip: "Fill layer frame by rendering the node graph".into(), on_update: WidgetCallback::new(|_| DocumentMessage::NodeGraphFrameGenerate.into()), ..Default::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::related_separator(), WidgetHolder::new(Widget::TextButton(TextButton { label: "Clear".into(), tooltip: "Remove rendered node graph from the layer frame".into(), @@ -1310,10 +567,11 @@ fn node_section_font(layer: &TextLayer) -> LayoutGroup { value: "Text".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::TextAreaInput(TextAreaInput { value: layer.text.clone(), on_update: WidgetCallback::new(|text_area: &TextAreaInput| PropertiesPanelMessage::ModifyText { new_text: text_area.value.clone() }.into()), @@ -1327,10 +585,11 @@ fn node_section_font(layer: &TextLayer) -> LayoutGroup { value: "Font".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::FontInput(FontInput { is_style_picker: false, font_family: layer.font.font_family.clone(), @@ -1353,10 +612,11 @@ fn node_section_font(layer: &TextLayer) -> LayoutGroup { value: "Style".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::FontInput(FontInput { is_style_picker: true, font_family: layer.font.font_family.clone(), @@ -1379,10 +639,11 @@ fn node_section_font(layer: &TextLayer) -> LayoutGroup { value: "Size".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(layer.size), min: Some(1.), @@ -1418,10 +679,11 @@ fn node_gradient_type(gradient: &Gradient) -> LayoutGroup { value: "Gradient Type".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::RadioInput(RadioInput { selected_index, entries: vec![ @@ -1463,24 +725,27 @@ fn node_gradient_color(gradient: &Gradient, position: usize) -> LayoutGroup { let send_fill_message = move |new_gradient: Gradient| PropertiesPanelMessage::ModifyFill { fill: Fill::Gradient(new_gradient) }.into(); let value = format!("Gradient: {:.0}%", gradient_clone.positions[position].0 * 100.); - let mut widgets = vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value, - tooltip: "Adjustable by dragging the gradient stops in the viewport with the Gradient tool active".into(), - ..TextLabel::default() - }))]; - widgets.push(WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - }))); - widgets.push(WidgetHolder::new(Widget::ColorInput(ColorInput { - value: gradient_clone.positions[position].1, - on_update: WidgetCallback::new(move |text_input: &ColorInput| { - let mut new_gradient = (*gradient_clone).clone(); - new_gradient.positions[position].1 = text_input.value; - send_fill_message(new_gradient) - }), - ..ColorInput::default() - }))); + let mut widgets = vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value, + tooltip: "Adjustable by dragging the gradient stops in the viewport with the Gradient tool active".into(), + ..TextLabel::default() + })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), + WidgetHolder::new(Widget::ColorInput(ColorInput { + value: gradient_clone.positions[position].1, + on_update: WidgetCallback::new(move |text_input: &ColorInput| { + let mut new_gradient = (*gradient_clone).clone(); + new_gradient.positions[position].1 = text_input.value; + send_fill_message(new_gradient) + }), + ..ColorInput::default() + })), + ]; let mut skip_separator = false; // Remove button @@ -1492,10 +757,7 @@ fn node_gradient_color(gradient: &Gradient, position: usize) -> LayoutGroup { }); skip_separator = true; - widgets.push(WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - }))); + widgets.push(WidgetHolder::related_separator()); widgets.push(WidgetHolder::new(Widget::IconButton(IconButton { icon: "Remove".to_string(), tooltip: "Remove this gradient stop".to_string(), @@ -1524,10 +786,7 @@ fn node_gradient_color(gradient: &Gradient, position: usize) -> LayoutGroup { }); if !skip_separator { - widgets.push(WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - }))); + widgets.push(WidgetHolder::related_separator()); } widgets.push(WidgetHolder::new(Widget::IconButton(IconButton { icon: "Add".to_string(), @@ -1553,10 +812,11 @@ fn node_section_fill(fill: &Fill) -> Option { value: "Color".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::ColorInput(ColorInput { value: if let Fill::Solid(color) = fill { Some(*color) } else { None }, on_update: WidgetCallback::new(|text_input: &ColorInput| { @@ -1573,10 +833,11 @@ fn node_section_fill(fill: &Fill) -> Option { value: "".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::TextButton(TextButton { label: "Use Gradient".into(), tooltip: "Change this fill from a solid color to a gradient".into(), @@ -1617,10 +878,11 @@ fn node_section_fill(fill: &Fill) -> Option { value: "".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::TextButton(TextButton { label: "Invert".into(), icon: Some("Swap".into()), @@ -1641,10 +903,11 @@ fn node_section_fill(fill: &Fill) -> Option { value: "".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::TextButton(TextButton { label: "Use Solid Color".into(), tooltip: "Change this fill from a gradient to a solid color, keeping the 0% stop color".into(), @@ -1687,10 +950,11 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup { value: "Color".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::ColorInput(ColorInput { value: stroke.color(), on_update: WidgetCallback::new(move |text_input: &ColorInput| { @@ -1709,10 +973,11 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup { value: "Weight".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(stroke.weight()), is_integer: false, @@ -1734,10 +999,11 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup { value: "Dash Lengths".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::TextInput(TextInput { value: stroke.dash_lengths(), centered: true, @@ -1757,10 +1023,11 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup { value: "Dash Offset".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(stroke.dash_offset()), is_integer: true, @@ -1782,10 +1049,11 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup { value: "Line Cap".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::RadioInput(RadioInput { selected_index: stroke.line_cap_index(), entries: vec![ @@ -1830,10 +1098,11 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup { value: "Line Join".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::RadioInput(RadioInput { selected_index: stroke.line_join_index(), entries: vec![ @@ -1879,10 +1148,11 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup { value: "Miter Limit".into(), ..TextLabel::default() })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(stroke.line_join_miter_limit() as f64), is_integer: true, diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index cd44a9483..2290eb071 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -2,7 +2,9 @@ use super::utility_types::ImaginateServerStatus; use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; use crate::messages::prelude::*; -use graphene::layers::{imaginate_layer::ImaginateStatus, text_layer::Font}; +use graph_craft::document::NodeId; +use graph_craft::imaginate_input::ImaginateStatus; +use graphene::layers::text_layer::Font; use graphene::LayerId; use serde::{Deserialize, Serialize}; @@ -52,22 +54,20 @@ pub enum PortfolioMessage { is_default: bool, }, ImaginateCheckServerStatus, - ImaginateSetBlobUrl { - document_id: u64, - layer_path: Vec, - blob_url: String, - resolution: (f64, f64), - }, ImaginateSetGeneratingStatus { document_id: u64, - path: Vec, + layer_path: Vec, + node_path: Vec, percent: Option, status: ImaginateStatus, }, ImaginateSetImageData { document_id: u64, layer_path: Vec, + node_path: Vec, image_data: Vec, + width: u32, + height: u32, }, ImaginateSetServerStatus { status: ImaginateServerStatus, @@ -114,6 +114,7 @@ pub enum PortfolioMessage { layer_path: Vec, image_data: Vec, size: (u32, u32), + imaginate_node: Option>, }, SelectDocument { document_id: u64, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index ef8672d52..451c16dc2 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -5,13 +5,23 @@ use crate::messages::dialog::simple_dialogs; use crate::messages::frontend::utility_types::{FrontendDocumentDetails, FrontendImageData}; use crate::messages::layout::utility_types::layout_widget::PropertyHolder; use crate::messages::layout::utility_types::misc::LayoutTarget; +use crate::messages::portfolio::document::node_graph::IMAGINATE_NODE; use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT}; +use crate::messages::portfolio::document::utility_types::misc::DocumentRenderMode; use crate::messages::portfolio::utility_types::ImaginateServerStatus; use crate::messages::prelude::*; +use graph_craft::document::value::TaggedValue; +use graph_craft::document::NodeId; +use graph_craft::document::{NodeInput, NodeNetwork}; +use graphene::document::pick_safe_imaginate_resolution; use graphene::layers::layer_info::{LayerDataType, LayerDataTypeDiscriminant}; use graphene::layers::text_layer::Font; use graphene::{LayerId, Operation as DocumentOperation}; +use graphene_core::raster::Image; + +use glam::DVec2; +use std::borrow::Cow; #[derive(Debug, Clone, Default)] pub struct PortfolioMessageHandler { @@ -199,32 +209,85 @@ impl MessageHandler { - if let Some(document) = self.documents.get_mut(&document_id) { - if let Ok(layer) = document.graphene_document.layer(&layer_path) { - let previous_blob_url = &layer.as_imaginate().unwrap().blob_url; - - if let Some(url) = previous_blob_url { - responses.push_back(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() }.into()); + let get = |name: &str| IMAGINATE_NODE.inputs.iter().position(|input| input.name == name).unwrap_or_else(|| panic!("Input {name} not found")); + if let Some(percentage) = percent { + responses.push_back( + PortfolioMessage::DocumentPassMessage { + document_id, + message: NodeGraphMessage::SetQualifiedInputValue { + layer_path: layer_path.clone(), + node_path: node_path.clone(), + input_index: get("Percent Complete"), + value: TaggedValue::F64(percentage), + } + .into(), } - - let message = DocumentOperation::SetLayerBlobUrl { layer_path, blob_url, resolution }.into(); - responses.push_back(PortfolioMessage::DocumentPassMessage { document_id, message }.into()); - } + .into(), + ); } + + if status == graph_craft::imaginate_input::ImaginateStatus::Generating { + responses.push_back( + PortfolioMessage::DocumentPassMessage { + document_id, + message: NodeGraphMessage::SetQualifiedInputValue { + layer_path: layer_path.clone(), + node_path: node_path.clone(), + input_index: get("Cached Data"), + value: TaggedValue::RcImage(None), + } + .into(), + } + .into(), + ); + } + + responses.push_back( + PortfolioMessage::DocumentPassMessage { + document_id, + message: NodeGraphMessage::SetQualifiedInputValue { + layer_path: layer_path.clone(), + node_path: node_path.clone(), + input_index: get("Status"), + value: TaggedValue::ImaginateStatus(status), + } + .into(), + } + .into(), + ); } - PortfolioMessage::ImaginateSetGeneratingStatus { document_id, path, percent, status } => { - let message = DocumentOperation::ImaginateSetGeneratingStatus { path, percent, status }.into(); - responses.push_back(PortfolioMessage::DocumentPassMessage { document_id, message }.into()); - } - PortfolioMessage::ImaginateSetImageData { document_id, layer_path, image_data } => { - let message = DocumentOperation::ImaginateSetImageData { layer_path, image_data }.into(); - responses.push_back(PortfolioMessage::DocumentPassMessage { document_id, message }.into()); + PortfolioMessage::ImaginateSetImageData { + document_id, + layer_path, + node_path, + image_data, + width, + height, + } => { + let get = |name: &str| IMAGINATE_NODE.inputs.iter().position(|input| input.name == name).unwrap_or_else(|| panic!("Input {name} not found")); + + let data = image_data.chunks_exact(4).map(|v| graphene_core::raster::color::Color::from_rgba8(v[0], v[1], v[2], v[3])).collect(); + let image = Image { width, height, data }; + responses.push_back( + PortfolioMessage::DocumentPassMessage { + document_id, + message: NodeGraphMessage::SetQualifiedInputValue { + layer_path: layer_path.clone(), + node_path: node_path.clone(), + input_index: get("Cached Data"), + value: TaggedValue::RcImage(Some(std::sync::Arc::new(image))), + } + .into(), + } + .into(), + ); } PortfolioMessage::ImaginateSetServerStatus { status } => { self.persistent_data.imaginate_server_status = status; @@ -418,88 +481,16 @@ impl MessageHandler { - fn read_image(document: Option<&DocumentMessageHandler>, layer_path: &[LayerId], image_data: Vec, (width, height): (u32, u32)) -> Result, String> { - use graphene_core::raster::Image; - use image::{ImageBuffer, Rgba}; - use std::io::Cursor; - - let data = image_data.chunks_exact(4).map(|v| graphene_core::raster::color::Color::from_rgba8(v[0], v[1], v[2], v[3])).collect(); - let image = graphene_core::raster::Image { width, height, data }; - - let document = document.ok_or_else(|| "Invalid document".to_string())?; - let layer = document.graphene_document.layer(layer_path).map_err(|e| format!("No layer: {e:?}"))?; - let node_graph_frame = match &layer.data { - LayerDataType::NodeGraphFrame(frame) => Ok(frame), - _ => Err("Invalid layer type".to_string()), - }?; - - // Execute the node graph - - let mut network = node_graph_frame.network.clone(); - info!("Executing network {network:#?}"); - - let stack = borrow_stack::FixedSizeStack::new(256); - for node_id in node_graph_frame.network.nodes.keys() { - network.flatten(*node_id); - } - - let mut proto_network = network.into_proto_network(); - proto_network.reorder_ids(); - - info!("proto_network with reordered ids: {proto_network:#?}"); - - assert_ne!(proto_network.nodes.len(), 0, "No protonodes exist?"); - for (_id, node) in proto_network.nodes { - info!("Inserting proto node {:?}", node); - interpreted_executor::node_registry::push_node(node, &stack); - } - - use borrow_stack::BorrowStack; - use dyn_any::IntoDynAny; - use graphene_core::Node; - - let result = unsafe { stack.get().last().unwrap().eval(image.into_dyn()) }; - let result = *dyn_any::downcast::(result).unwrap(); - - let mut bytes: Vec = Vec::new(); - let [result_width, result_height] = [result.width, result.height]; - let size_estimate = (result_width * result_height * 4) as usize; - - let mut result_bytes = Vec::with_capacity(size_estimate); - result_bytes.extend(result.data.into_iter().flat_map(|colour| colour.to_rgba8())); - let output: ImageBuffer, _> = image::ImageBuffer::from_raw(result_width, result_height, result_bytes).ok_or_else(|| "Invalid image size".to_string())?; - output.write_to(&mut Cursor::new(&mut bytes), image::ImageOutputFormat::Bmp).map_err(|e| e.to_string())?; - - Ok(bytes) - } - - match read_image(self.documents.get(&document_id), &layer_path, image_data, size) { - Ok(image_data) => { - responses.push_back( - DocumentOperation::SetNodeGraphFrameImageData { - layer_path: layer_path.clone(), - image_data: image_data.clone(), - } - .into(), - ); - let mime = "image/bmp".to_string(); - let image_data = std::sync::Arc::new(image_data); - responses.push_back( - FrontendMessage::UpdateImageData { - document_id, - image_data: vec![FrontendImageData { path: layer_path, image_data, mime }], - } - .into(), - ); - } - Err(description) => responses.push_back( + if let Err(description) = self.evaluate_node_graph(document_id, layer_path, (image_data, size), imaginate_node, preferences, responses) { + responses.push_back( DialogMessage::DisplayDialogError { - title: "Failed to update node graph".to_string(), + title: "Unable to update node graph".to_string(), description, } .into(), - ), + ); } } PortfolioMessage::SelectDocument { document_id } => { @@ -681,4 +672,238 @@ impl PortfolioMessageHandler { fn document_index(&self, document_id: u64) -> usize { self.document_ids.iter().position(|id| id == &document_id).expect("Active document is missing from document ids") } + + /// Computes an input for a node in the graph + fn compute_input(old_network: &NodeNetwork, node_path: &[NodeId], mut input_index: usize, image: Cow) -> Result { + let mut network = old_network.clone(); + // Adjust the output of the graph so we find the relevant output + 'outer: for end in (0..node_path.len()).rev() { + let mut inner_network = &mut network; + for index in 0..end { + let node_id = node_path[index]; + inner_network.output = node_id; + + let Some(new_inner) = inner_network.nodes.get_mut(&node_id).and_then(|node| node.implementation.get_network_mut()) else{ + return Err("Failed to find network".to_string()); + }; + inner_network = new_inner; + } + match &inner_network.nodes.get(&node_path[end]).unwrap().inputs[input_index] { + // If the input is from a parent network then adjust the input index and continue iteration + NodeInput::Network => { + input_index = inner_network + .inputs + .iter() + .enumerate() + .filter(|&(_index, &id)| id == node_path[end]) + .nth(input_index) + .ok_or_else(|| "Invalid network input".to_string())? + .0; + } + // If the input is just a value, return that value + NodeInput::Value { tagged_value, .. } => { + return dyn_any::downcast::(tagged_value.clone().to_value().up_box()) + .map(|v| *v) + .ok_or_else(|| "Incorrectly typed value".to_string()) + } + // If the input is from a node, set the node to be the output (so that is what is evaluated) + NodeInput::Node(n) => { + inner_network.output = *n; + break 'outer; + } + } + } + + let stack = borrow_stack::FixedSizeStack::new(256); + for node_id in old_network.nodes.keys() { + network.flatten(*node_id); + } + + let mut proto_network = network.into_proto_network(); + proto_network.reorder_ids(); + + assert_ne!(proto_network.nodes.len(), 0, "No protonodes exist?"); + for (_id, node) in proto_network.nodes { + interpreted_executor::node_registry::push_node(node, &stack); + } + + use borrow_stack::BorrowStack; + use dyn_any::IntoDynAny; + use graphene_core::Node; + + let boxed = unsafe { stack.get().last().unwrap().eval(image.into_owned().into_dyn()) }; + + dyn_any::downcast::(boxed).map(|v| *v).ok_or_else(|| "Incorrectly typed output".to_string()) + } + + /// Encodes an image into a format using the image crate + fn encode_img(image: Image, resize: Option, format: image::ImageOutputFormat) -> Result<(Vec, (u32, u32)), String> { + use image::{ImageBuffer, Rgba}; + use std::io::Cursor; + + let mut image_data: Vec = Vec::new(); + let [image_width, image_height] = [image.width, image.height]; + let size_estimate = (image_width * image_height * 4) as usize; + + let mut result_bytes = Vec::with_capacity(size_estimate); + result_bytes.extend(image.data.into_iter().flat_map(|colour| colour.to_rgba8())); + let mut output: ImageBuffer, _> = image::ImageBuffer::from_raw(image_width, image_height, result_bytes).ok_or_else(|| "Invalid image size".to_string())?; + if let Some(size) = resize { + let size = size.as_uvec2(); + if size.x > 0 && size.y > 0 { + output = image::imageops::resize(&output, size.x, size.y, image::imageops::Triangle); + } + } + let size = output.dimensions(); + output.write_to(&mut Cursor::new(&mut image_data), format).map_err(|e| e.to_string())?; + Ok::<_, String>((image_data, size)) + } + + /// Evaluates a node graph, computing either the imaginate node or the entire graph + fn evaluate_node_graph( + &mut self, + document_id: u64, + layer_path: Vec, + (image_data, size): (Vec, (u32, u32)), + imaginate_node: Option>, + preferences: &PreferencesMessageHandler, + responses: &mut VecDeque, + ) -> Result<(), String> { + // Reformat the input image data into an f32 image + let data = image_data.chunks_exact(4).map(|v| graphene_core::raster::color::Color::from_rgba8(v[0], v[1], v[2], v[3])).collect(); + let (width, height) = size; + let image = graphene_core::raster::Image { width, height, data }; + + // Get the node graph layer + let document = self.documents.get_mut(&document_id).ok_or_else(|| "Invalid document".to_string())?; + let layer = document.graphene_document.layer(&layer_path).map_err(|e| format!("No layer: {e:?}"))?; + let node_graph_frame = match &layer.data { + LayerDataType::NodeGraphFrame(frame) => Ok(frame), + _ => Err("Invalid layer type".to_string()), + }?; + let network = node_graph_frame.network.clone(); + + // Execute the node graph + if let Some(imaginate_node) = imaginate_node { + use graph_craft::imaginate_input::*; + + let get = |name: &str| IMAGINATE_NODE.inputs.iter().position(|input| input.name == name).unwrap_or_else(|| panic!("Input {name} not found")); + + let resolution: Option = Self::compute_input(&network, &imaginate_node, get("Resolution"), Cow::Borrowed(&image))?; + let resolution = resolution.unwrap_or_else(|| { + let transform = document.graphene_document.root.transform.inverse() * document.graphene_document.multiply_transforms(&layer_path).unwrap(); + let (x, y) = pick_safe_imaginate_resolution((transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length())); + DVec2::new(x as f64, y as f64) + }); + + let transform = document.graphene_document.root.transform.inverse() * document.graphene_document.multiply_transforms(&layer_path).unwrap(); + let parameters = ImaginateGenerationParameters { + seed: Self::compute_input::(&network, &imaginate_node, get("Seed"), Cow::Borrowed(&image))? as u64, + resolution: resolution.as_uvec2().into(), + samples: Self::compute_input::(&network, &imaginate_node, get("Samples"), Cow::Borrowed(&image))? as u32, + sampling_method: Self::compute_input::(&network, &imaginate_node, get("Sampling Method"), Cow::Borrowed(&image))? + .api_value() + .to_string(), + text_guidance: Self::compute_input(&network, &imaginate_node, get("Text Guidance"), Cow::Borrowed(&image))?, + text_prompt: Self::compute_input(&network, &imaginate_node, get("Text Prompt"), Cow::Borrowed(&image))?, + negative_prompt: Self::compute_input(&network, &imaginate_node, get("Negative Prompt"), Cow::Borrowed(&image))?, + image_creativity: Some(Self::compute_input::(&network, &imaginate_node, get("Image Creativity"), Cow::Borrowed(&image))? / 100.), + restore_faces: Self::compute_input(&network, &imaginate_node, get("Improve Faces"), Cow::Borrowed(&image))?, + tiling: Self::compute_input(&network, &imaginate_node, get("Tiling"), Cow::Borrowed(&image))?, + }; + let use_base_image = Self::compute_input::(&network, &imaginate_node, get("Use Base Image"), Cow::Borrowed(&image))?; + + let base_image = if use_base_image { + let image: Image = Self::compute_input(&network, &imaginate_node, get("Base Image"), Cow::Borrowed(&image))?; + // Only use if has size + if image.width > 0 && image.height > 0 { + let (image_data, size) = Self::encode_img(image, Some(resolution), image::ImageOutputFormat::Png)?; + let size = DVec2::new(size.0 as f64, size.1 as f64); + let mime = "image/png".to_string(); + Some(ImaginateBaseImage { image_data, size, mime }) + } else { + None + } + } else { + None + }; + + let mask_image = + if base_image.is_some() { + let mask_path: Option> = Self::compute_input(&network, &imaginate_node, get("Masking Layer"), Cow::Borrowed(&image))?; + + // Calculate the size of the node graph frame + let size = DVec2::new(transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length()); + + // Render the masking layer within the node graph frame + let old_transforms = document.remove_document_transform(); + let mask_is_some = mask_path.is_some(); + let mask_image = mask_path.filter(|mask_layer_path| document.graphene_document.layer(mask_layer_path).is_ok()).map(|mask_layer_path| { + let render_mode = DocumentRenderMode::LayerCutout(&mask_layer_path, graphene::color::Color::WHITE); + let svg = document.render_document(size, transform.inverse(), &self.persistent_data, render_mode); + + ImaginateMaskImage { svg, size } + }); + + if mask_is_some && mask_image.is_none() { + return Err("Imagination masking layer is missing.\nIt may have been deleted or moved. Please drag a new layer reference\ninto the 'Masking Layer' parameter input, then generate again.".to_string()); + } + + document.restore_document_transform(old_transforms); + mask_image + } else { + None + }; + + responses.push_back( + FrontendMessage::TriggerImaginateGenerate { + parameters, + base_image, + mask_image, + mask_paint_mode: if Self::compute_input::(&network, &imaginate_node, get("Inpaint"), Cow::Borrowed(&image))? { + ImaginateMaskPaintMode::Inpaint + } else { + ImaginateMaskPaintMode::Outpaint + }, + mask_blur_px: Self::compute_input::(&network, &imaginate_node, get("Mask Blur"), Cow::Borrowed(&image))? as u32, + imaginate_mask_starting_fill: Self::compute_input(&network, &imaginate_node, get("Mask Starting Fill"), Cow::Borrowed(&image))?, + hostname: preferences.imaginate_server_hostname.clone(), + refresh_frequency: preferences.imaginate_refresh_frequency, + document_id, + layer_path, + node_path: imaginate_node, + } + .into(), + ); + } else { + let mut image: Image = Self::compute_input(&network, &[1], 0, Cow::Owned(image))?; + + // If no image was generated, use the input image + if image.width == 0 || image.height == 0 { + let data = image_data.chunks_exact(4).map(|v| graphene_core::raster::color::Color::from_rgba8(v[0], v[1], v[2], v[3])).collect(); + image = graphene_core::raster::Image { width, height, data }; + } + + let (image_data, _size) = Self::encode_img(image, None, image::ImageOutputFormat::Bmp)?; + + responses.push_back( + DocumentOperation::SetNodeGraphFrameImageData { + layer_path: layer_path.clone(), + image_data: image_data.clone(), + } + .into(), + ); + let mime = "image/bmp".to_string(); + let image_data = std::sync::Arc::new(image_data); + responses.push_back( + FrontendMessage::UpdateImageData { + document_id, + image_data: vec![FrontendImageData { path: layer_path, image_data, mime }], + } + .into(), + ); + } + + Ok(()) + } } diff --git a/editor/src/messages/tool/tool_messages/imaginate_tool.rs b/editor/src/messages/tool/tool_messages/imaginate_tool.rs index 78be8e9c8..9d0341bcf 100644 --- a/editor/src/messages/tool/tool_messages/imaginate_tool.rs +++ b/editor/src/messages/tool/tool_messages/imaginate_tool.rs @@ -2,6 +2,7 @@ use crate::consts::DRAG_THRESHOLD; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion}; use crate::messages::layout::utility_types::layout_widget::PropertyHolder; +use crate::messages::portfolio::document::node_graph::IMAGINATE_NODE; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::resize::Resize; use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; @@ -137,11 +138,73 @@ impl Fsm for ImaginateToolFsmState { shape_data.path = Some(document.get_path_for_new_layer()); responses.push_back(DocumentMessage::DeselectAllLayers.into()); + use graph_craft::{document::*, generic, proto::*}; + + let imaginate_node_type = IMAGINATE_NODE; + let num_inputs = imaginate_node_type.inputs.len(); + + let imaginate_inner_network = NodeNetwork { + inputs: (0..num_inputs).map(|_| 0).collect(), + output: 0, + nodes: [( + 0, + DocumentNode { + name: format!("{}_impl", imaginate_node_type.name), + // TODO: Allow inserting nodes that contain other nodes. + implementation: DocumentNodeImplementation::Unresolved(imaginate_node_type.identifier.clone()), + inputs: (0..num_inputs).map(|_| NodeInput::Network).collect(), + metadata: DocumentNodeMetadata::default(), + }, + )] + .into_iter() + .collect(), + }; + let mut imaginate_inputs: Vec = imaginate_node_type.inputs.iter().map(|input| input.default.clone()).collect(); + imaginate_inputs[0] = NodeInput::Node(0); + + let network = NodeNetwork { + inputs: vec![0], + output: 1, + nodes: [ + ( + 0, + DocumentNode { + name: "Input".into(), + inputs: vec![NodeInput::Network], + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")])), + metadata: DocumentNodeMetadata { position: (8, 4) }, + }, + ), + ( + 1, + DocumentNode { + name: "Output".into(), + inputs: vec![NodeInput::Node(2)], + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")])), + metadata: DocumentNodeMetadata { position: (32, 4) }, + }, + ), + ( + 2, + DocumentNode { + name: imaginate_node_type.name.to_string(), + inputs: imaginate_inputs, + // TODO: Allow inserting nodes that contain other nodes. + implementation: DocumentNodeImplementation::Network(imaginate_inner_network), + metadata: graph_craft::document::DocumentNodeMetadata { position: (20, 4) }, + }, + ), + ] + .into_iter() + .collect(), + }; + responses.push_back( - Operation::AddImaginateFrame { + Operation::AddNodeGraphFrame { path: shape_data.path.clone().unwrap(), insert_index: -1, transform: DAffine2::ZERO.to_cols_array(), + network, } .into(), ); diff --git a/editor/src/messages/tool/tool_messages/node_graph_frame_tool.rs b/editor/src/messages/tool/tool_messages/node_graph_frame_tool.rs index b1edaab07..23e4c0a5a 100644 --- a/editor/src/messages/tool/tool_messages/node_graph_frame_tool.rs +++ b/editor/src/messages/tool/tool_messages/node_graph_frame_tool.rs @@ -137,11 +137,40 @@ impl Fsm for NodeGraphToolFsmState { shape_data.path = Some(document.get_path_for_new_layer()); responses.push_back(DocumentMessage::DeselectAllLayers.into()); + use graph_craft::{document::*, generic, proto::*}; + let network = NodeNetwork { + inputs: vec![0], + output: 1, + nodes: [ + ( + 0, + DocumentNode { + name: "Input".into(), + inputs: vec![NodeInput::Network], + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")])), + metadata: DocumentNodeMetadata { position: (8, 4) }, + }, + ), + ( + 1, + DocumentNode { + name: "Output".into(), + inputs: vec![NodeInput::Node(0)], + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")])), + metadata: DocumentNodeMetadata { position: (20, 4) }, + }, + ), + ] + .into_iter() + .collect(), + }; + responses.push_back( Operation::AddNodeGraphFrame { path: shape_data.path.clone().unwrap(), insert_index: -1, transform: DAffine2::ZERO.to_cols_array(), + network, } .into(), ); diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 130e46bdb..1e5ea8759 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -1,6 +1,6 @@ use crate::application::generate_uuid; use crate::consts::{ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE}; -use crate::messages::frontend::utility_types::{FrontendImageData, MouseCursorIcon}; +use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion}; use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; @@ -375,7 +375,7 @@ impl Fsm for SelectToolFsmState { self, event: ToolMessage, tool_data: &mut Self::ToolData, - (document, document_id, _global_tool_data, input, font_cache): ToolActionHandlerData, + (document, _document_id, _global_tool_data, input, font_cache): ToolActionHandlerData, _tool_options: &Self::ToolOptions, responses: &mut VecDeque, ) -> Self { @@ -572,7 +572,7 @@ impl Fsm for SelectToolFsmState { tool_data.drag_current = mouse_position + closest_move; if input.keyboard.get(duplicate as usize) && tool_data.not_duplicated_layers.is_none() { - tool_data.start_duplicates(document, document_id, responses); + tool_data.start_duplicates(document, responses); } else if !input.keyboard.get(duplicate as usize) && tool_data.not_duplicated_layers.is_some() { tool_data.stop_duplicates(responses); } @@ -929,7 +929,7 @@ impl Fsm for SelectToolFsmState { impl SelectToolData { /// Duplicates the currently dragging layers. Called when Alt is pressed and the layers have not yet been duplicated. - fn start_duplicates(&mut self, document: &DocumentMessageHandler, document_id: u64, responses: &mut VecDeque) { + fn start_duplicates(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque) { responses.push_back(DocumentMessage::DeselectAllLayers.into()); self.not_duplicated_layers = Some(self.layers_dragging.clone()); @@ -947,7 +947,7 @@ impl SelectToolData { // Copy the layers. // Not using the Copy message allows us to retrieve the ids of the new layers to initialize the drag. - let mut layer = match document.graphene_document.layer(layer_path) { + let layer = match document.graphene_document.layer(layer_path) { Ok(layer) => layer.clone(), Err(e) => { warn!("Could not access selected layer {:?}: {:?}", layer_path, e); @@ -958,20 +958,6 @@ impl SelectToolData { let layer_metadata = *document.layer_metadata(layer_path); *layer_path.last_mut().unwrap() = generate_uuid(); - let image_data = if let LayerDataType::Imaginate(imaginate) = &mut layer.data { - imaginate.blob_url = None; - - imaginate.image_data.as_ref().map(|data| { - vec![FrontendImageData { - path: layer_path.clone(), - image_data: data.image_data.clone(), - mime: imaginate.mime.clone(), - }] - }) - } else { - None - }; - responses.push_back( Operation::InsertLayer { layer: Box::new(layer), @@ -988,10 +974,6 @@ impl SelectToolData { } .into(), ); - - if let Some(image_data) = image_data { - responses.push_back(FrontendMessage::UpdateImageData { image_data, document_id }.into()); - } } } diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index 5b119931b..5b127fb18 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -315,8 +315,8 @@ fn list_tools_in_groups() -> Vec> { ], vec![ // Raster tool group - ToolAvailability::Available(Box::::default()), ToolAvailability::Available(Box::::default()), + ToolAvailability::Available(Box::::default()), ToolAvailability::ComingSoon(ToolEntry { tool_type: ToolType::Brush, icon_name: "RasterBrushTool".into(), diff --git a/frontend/assets/icon-12px-solid/edit.svg b/frontend/assets/icon-12px-solid/edit.svg new file mode 100644 index 000000000..6483577ce --- /dev/null +++ b/frontend/assets/icon-12px-solid/edit.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 5b5d92712..472b9f3c4 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -83,6 +83,13 @@ body, user-select: none; } +// The default value of `auto` from the CSS spec is a footgun with flexbox layouts: +// https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size +* { + min-width: 0; + min-height: 0; +} + html, body, input, diff --git a/frontend/src/components/layout/FloatingMenu.vue b/frontend/src/components/layout/FloatingMenu.vue index 5065a141d..f1b188bd8 100644 --- a/frontend/src/components/layout/FloatingMenu.vue +++ b/frontend/src/components/layout/FloatingMenu.vue @@ -267,6 +267,7 @@ export default defineComponent({ if (!inParentFloatingMenu) { // Required to correctly position content when scrolled (it has a `position: fixed` to prevent clipping) + // We use `.style` on a ref (instead of a `:style` Vue binding) because the binding causes the `updated()` hook to call the function we're in recursively forever const tailOffset = this.type === "Popover" ? 10 : 0; if (this.direction === "Bottom") floatingMenuContent.style.top = `${tailOffset + this.floatingMenuBounds.top}px`; if (this.direction === "Top") floatingMenuContent.style.bottom = `${tailOffset + this.floatingMenuBounds.bottom}px`; @@ -274,7 +275,7 @@ export default defineComponent({ if (this.direction === "Left") floatingMenuContent.style.right = `${tailOffset + this.floatingMenuBounds.right}px`; // Required to correctly position tail when scrolled (it has a `position: fixed` to prevent clipping) - // We use a ref here, instead of a `:style` binding, because that causes the `updated()` hook to call the function we're in recursively forever + // We use `.style` on a ref (instead of a `:style` Vue binding) because the binding causes the `updated()` hook to call the function we're in recursively forever const tail = this.$refs.tail as HTMLElement; if (tail && this.direction === "Bottom") tail.style.top = `${this.floatingMenuBounds.top}px`; if (tail && this.direction === "Top") tail.style.bottom = `${this.floatingMenuBounds.bottom}px`; @@ -289,6 +290,7 @@ export default defineComponent({ if (this.direction === "Top" || this.direction === "Bottom") { zeroedBorderVertical = this.direction === "Top" ? "Bottom" : "Top"; + // We use `.style` on a ref (instead of a `:style` Vue binding) because the binding causes the `updated()` hook to call the function we're in recursively forever if (this.floatingMenuContentBounds.left - this.windowEdgeMargin <= this.workspaceBounds.left) { floatingMenuContent.style.left = `${this.windowEdgeMargin}px`; if (this.workspaceBounds.left + floatingMenuContainerBounds.left === 12) zeroedBorderHorizontal = "Left"; @@ -301,6 +303,7 @@ export default defineComponent({ if (this.direction === "Left" || this.direction === "Right") { zeroedBorderHorizontal = this.direction === "Left" ? "Right" : "Left"; + // We use `.style` on a ref (instead of a `:style` Vue binding) because the binding causes the `updated()` hook to call the function we're in recursively forever if (this.floatingMenuContentBounds.top - this.windowEdgeMargin <= this.workspaceBounds.top) { floatingMenuContent.style.top = `${this.windowEdgeMargin}px`; if (this.workspaceBounds.top + floatingMenuContainerBounds.top === 12) zeroedBorderVertical = "Top"; @@ -313,6 +316,7 @@ export default defineComponent({ // Remove the rounded corner from the content where the tail perfectly meets the corner if (this.type === "Popover" && this.windowEdgeMargin === 6 && zeroedBorderVertical && zeroedBorderHorizontal) { + // We use `.style` on a ref (instead of a `:style` Vue binding) because the binding causes the `updated()` hook to call the function we're in recursively forever switch (`${zeroedBorderVertical}${zeroedBorderHorizontal}`) { case "TopLeft": floatingMenuContent.style.borderTopLeftRadius = "0"; diff --git a/frontend/src/components/layout/LayoutCol.vue b/frontend/src/components/layout/LayoutCol.vue index 071ec2541..699c192d3 100644 --- a/frontend/src/components/layout/LayoutCol.vue +++ b/frontend/src/components/layout/LayoutCol.vue @@ -15,8 +15,6 @@ display: flex; flex-direction: column; flex-grow: 1; - min-width: 0; - min-height: 0; .spacer { flex: 1 1 100%; diff --git a/frontend/src/components/layout/LayoutRow.vue b/frontend/src/components/layout/LayoutRow.vue index e886683e9..ad500abfc 100644 --- a/frontend/src/components/layout/LayoutRow.vue +++ b/frontend/src/components/layout/LayoutRow.vue @@ -15,8 +15,6 @@ display: flex; flex-direction: row; flex-grow: 1; - min-width: 0; - min-height: 0; .spacer { flex: 1 1 100%; diff --git a/frontend/src/components/panels/Document.vue b/frontend/src/components/panels/Document.vue index 6c7cc5ae6..b2b9dfa87 100644 --- a/frontend/src/components/panels/Document.vue +++ b/frontend/src/components/panels/Document.vue @@ -133,8 +133,6 @@ flex: 0 0 auto; .widget-row { - min-height: 0; - .swatch-pair { margin: 0; } diff --git a/frontend/src/components/panels/LayerTree.vue b/frontend/src/components/panels/LayerTree.vue index 2a19725e8..511766881 100644 --- a/frontend/src/components/panels/LayerTree.vue +++ b/frontend/src/components/panels/LayerTree.vue @@ -77,8 +77,6 @@