Make Imaginate into a node (#878)

* Simplify document node input defenitions

* Remove imaginate layer

* Imaginate node properties

* Fix serde feature gate

* Add Proc Macro for Protonode implementation

* Fix incorrect type

* Add cargo.toml metadata

* Send imaginate params to frontend

* Fix image_creativity range

* Finish imaginate implementation

* Fix the imaginate draw tool

* Remove node-graph/rpco-macro

* Cargo fmt

* Fix missing workspace member

* Changes to the resolution

* Add checkbox for Imaginate auto resolution; improve Properties panel layouts

And fix bugs in panel resizing

* Implement the Rescale button

* Reorder imports

* Update Rust deps

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube 2022-12-20 22:51:38 +00:00 committed by Keavon Chambers
parent 2f2daa25e9
commit 2732492307
61 changed files with 2249 additions and 2596 deletions

124
Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -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<ImaginateBaseImage>,
#[serde(rename = "maskImage")]
mask_image: Option<ImaginateBaseImage>,
mask_image: Option<ImaginateMaskImage>,
#[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<LayerId>,
#[serde(rename = "nodePath")]
node_path: Vec<NodeId>,
},
TriggerImaginateTerminate {
#[serde(rename = "documentId")]
document_id: u64,
#[serde(rename = "layerPath")]
layer_path: Vec<LayerId>,
#[serde(rename = "nodePath")]
node_path: Vec<NodeId>,
hostname: String,
},
TriggerImport,
@ -102,6 +107,8 @@ pub enum FrontendMessage {
layer_path: Vec<LayerId>,
svg: String,
size: glam::DVec2,
#[serde(rename = "imaginateNode")]
imaginate_node: Option<Vec<NodeId>>,
},
TriggerOpenDocument,
TriggerPaste,

View file

@ -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<String>) -> 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<String>) -> Self {
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: text.into(),
..Default::default()
}))
}
pub fn bold_text(text: impl Into<String>) -> Self {
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: text.into(),
bold: true,
..Default::default()
}))
}
}
#[derive(Clone)]

View file

@ -246,6 +246,42 @@ pub struct NumberInput {
pub on_update: WidgetCallback<NumberInput>,
}
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<String>) -> 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]

View file

@ -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<LayerId>,
},
@ -93,6 +92,16 @@ pub enum DocumentMessage {
delta: (f64, f64),
},
NodeGraphFrameGenerate,
NodeGraphFrameImaginate {
imaginate_node: Vec<NodeId>,
},
NodeGraphFrameImaginateRandom {
imaginate_node: Vec<NodeId>,
},
NodeGraphFrameImaginateTerminate {
layer_path: Vec<LayerId>,
node_path: Vec<NodeId>,
},
NudgeSelectedLayers {
delta_x: f64,
delta_y: f64,

View file

@ -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<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
responses.extend([LayerChanged { affected_layer_path }.into(), DocumentStructureChanged.into()]);
}
FrameClear => {
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<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
.into(),
);
}
ImaginateGenerate => {
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<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
}
}
NodeGraphFrameGenerate => {
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<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
// Revoke the old blob URL
match &layer.data {
LayerDataType::Imaginate(imaginate) => {
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<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
}
impl DocumentMessageHandler {
pub fn call_imaginate(&mut self, document_id: u64, preferences: &PreferencesMessageHandler, persistent_data: &PersistentData) -> Option<Message> {
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<Message> {
pub fn call_node_graph_frame(&mut self, document_id: u64, _preferences: &PreferencesMessageHandler, persistent_data: &PersistentData, imaginate_node: Option<Vec<NodeId>>) -> Option<Message> {
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 {

View file

@ -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<LayerId>,
node_path: Vec<NodeId>,
input_index: usize,
value: TaggedValue,
},
}

View file

@ -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<LayoutGroup> {
pub fn collate_properties(&self, node_graph_frame: &NodeGraphFrameLayer, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
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<Message>) {
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<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
match message {
NodeGraphMessage::CloseNodeGraph => {
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<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
};
input_node.inputs[actual_index] = NodeInput::Node(output_node);
info!("Inputs: {:?}", input_node.inputs);
Self::send_graph(network, responses);
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
}
@ -474,6 +471,36 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
}
}
}
NodeGraphMessage::SetQualifiedInputValue {
layer_path,
node_path,
input_index,
value,
} => {
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());
}
}
}
}
}

View file

@ -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<crate::messages::prelude::Message>,
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<LayoutGroup>,
pub properties: fn(&DocumentNode, NodeId, &mut NodePropertiesContext) -> Vec<LayoutGroup>,
}
// 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)
}

View file

@ -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<String>) -> Vec<LayoutGroup> {
let widget = WidgetHolder::text_widget(text);
@ -15,11 +19,11 @@ pub fn string_properties(text: impl Into<String>) -> Vec<LayoutGroup> {
}
fn update_value<T, F: Fn(&T) -> TaggedValue + 'static + Send + Sync>(value: F, node_id: NodeId, input_index: usize) -> WidgetCallback<T> {
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<WidgetHolder> {
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<WidgetHolder> {
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<WidgetHolder> {
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<f64>, range_max: Option<f64>, unit: String, is_integer: bool) -> Vec<WidgetHolder> {
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<WidgetHolder> {
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<WidgetHolder> {
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<WidgetHolder> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
pub fn add_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
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<LayoutGroup> {
pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext, blank_assist: bool) -> Vec<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
string_properties(format!("Node '{}' cannot be found in library", document_node.name))
}
pub fn no_properties(_document_node: &DocumentNode, _node_id: NodeId) -> Vec<LayoutGroup> {
pub fn no_properties(_document_node: &DocumentNode, _node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
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 }

View file

@ -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<Vec<LayerId>>, document: TargetDocument },
SetImaginateCfgScale { cfg_scale: f64 },
SetImaginateDenoisingStrength { denoising_strength: f64 },
SetImaginateLayerPath { layer_path: Option<Vec<LayerId>> },
SetImaginateMaskBlurPx { mask_blur_px: u32 },
SetImaginateMaskFillContent { mode: ImaginateMaskFillContent },
SetImaginateMaskPaintMode { paint: ImaginateMaskPaintMode },
SetImaginateNegativePrompt { negative_prompt: String },
SetImaginatePrompt { prompt: String },
SetImaginateRestoreFaces { restore_faces: bool },
SetImaginateSamples { samples: u32 },
SetImaginateSamplingMethod { method: ImaginateSamplingMethod },
SetImaginateScaleFromResolution,
SetImaginateSeed { seed: u64 },
SetImaginateSeedRandomize,
SetImaginateSeedRandomizeAndGenerate,
SetImaginateTiling { tiling: bool },
SetImaginateUseImg2Img { use_img2img: bool },
SetPivot { new_position: PivotPosition },
UpdateSelectedDocumentProperties,
}

View file

@ -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<PropertiesPanelMessage, (&PersistentData, PropertiesPane
}
.into(),
),
SetImaginatePrompt { prompt } => {
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());
}
}
}

View file

@ -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<LayerId>,
blob_url: String,
resolution: (f64, f64),
},
ImaginateSetGeneratingStatus {
document_id: u64,
path: Vec<LayerId>,
layer_path: Vec<LayerId>,
node_path: Vec<NodeId>,
percent: Option<f64>,
status: ImaginateStatus,
},
ImaginateSetImageData {
document_id: u64,
layer_path: Vec<LayerId>,
node_path: Vec<NodeId>,
image_data: Vec<u8>,
width: u32,
height: u32,
},
ImaginateSetServerStatus {
status: ImaginateServerStatus,
@ -114,6 +114,7 @@ pub enum PortfolioMessage {
layer_path: Vec<LayerId>,
image_data: Vec<u8>,
size: (u32, u32),
imaginate_node: Option<Vec<NodeId>>,
},
SelectDocument {
document_id: u64,

View file

@ -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<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
);
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into());
}
PortfolioMessage::ImaginateSetBlobUrl {
PortfolioMessage::ImaginateSetGeneratingStatus {
document_id,
layer_path,
blob_url,
resolution,
node_path,
percent,
status,
} => {
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(),
}
.into(),
);
}
let message = DocumentOperation::SetLayerBlobUrl { layer_path, blob_url, resolution }.into();
responses.push_back(PortfolioMessage::DocumentPassMessage { document_id, message }.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(),
);
}
PortfolioMessage::ImaginateSetGeneratingStatus { document_id, path, percent, status } => {
let message = DocumentOperation::ImaginateSetGeneratingStatus { path, percent, status }.into();
responses.push_back(PortfolioMessage::DocumentPassMessage { document_id, message }.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),
}
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());
.into(),
}
.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<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
layer_path,
image_data,
size,
imaginate_node,
} => {
fn read_image(document: Option<&DocumentMessageHandler>, layer_path: &[LayerId], image_data: Vec<u8>, (width, height): (u32, u32)) -> Result<Vec<u8>, 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::<Image>(result).unwrap();
let mut bytes: Vec<u8> = 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<Rgba<u8>, _> = 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) => {
if let Err(description) = self.evaluate_node_graph(document_id, layer_path, (image_data, size), imaginate_node, preferences, responses) {
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(
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<T: dyn_any::StaticType>(old_network: &NodeNetwork, node_path: &[NodeId], mut input_index: usize, image: Cow<Image>) -> Result<T, String> {
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::<T>(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::<T>(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<DVec2>, format: image::ImageOutputFormat) -> Result<(Vec<u8>, (u32, u32)), String> {
use image::{ImageBuffer, Rgba};
use std::io::Cursor;
let mut image_data: Vec<u8> = 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<Rgba<u8>, _> = 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<LayerId>,
(image_data, size): (Vec<u8>, (u32, u32)),
imaginate_node: Option<Vec<NodeId>>,
preferences: &PreferencesMessageHandler,
responses: &mut VecDeque<Message>,
) -> 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<glam::DVec2> = 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::<f64>(&network, &imaginate_node, get("Seed"), Cow::Borrowed(&image))? as u64,
resolution: resolution.as_uvec2().into(),
samples: Self::compute_input::<f64>(&network, &imaginate_node, get("Samples"), Cow::Borrowed(&image))? as u32,
sampling_method: Self::compute_input::<ImaginateSamplingMethod>(&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::<f64>(&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::<bool>(&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<Vec<LayerId>> = 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::<bool>(&network, &imaginate_node, get("Inpaint"), Cow::Borrowed(&image))? {
ImaginateMaskPaintMode::Inpaint
} else {
ImaginateMaskPaintMode::Outpaint
},
mask_blur_px: Self::compute_input::<f64>(&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(())
}
}

View file

@ -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<NodeInput> = 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(),
);

View file

@ -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(),
);

View file

@ -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<Message>,
) -> 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<Message>) {
fn start_duplicates(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
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());
}
}
}

View file

@ -315,8 +315,8 @@ fn list_tools_in_groups() -> Vec<Vec<ToolAvailability>> {
],
vec![
// Raster tool group
ToolAvailability::Available(Box::<imaginate_tool::ImaginateTool>::default()),
ToolAvailability::Available(Box::<node_graph_frame_tool::NodeGraphFrameTool>::default()),
ToolAvailability::Available(Box::<imaginate_tool::ImaginateTool>::default()),
ToolAvailability::ComingSoon(ToolEntry {
tool_type: ToolType::Brush,
icon_name: "RasterBrushTool".into(),

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<!-- TODO: Replace this with a proper icon design -->
<g transform="scale(0.5)">
<path d="M18.5,5.5c-1-1-2.5-1.5-3-1L5,15c0.39,0.24,1.23,0.1,1.59,0.38c0.36,0.28,0.3,1.06,0.63,1.4c0.33,0.33,1.11,0.28,1.4,0.63C8.9,17.77,8.76,18.61,9,19L19.5,8.5C20,8,19.5,6.5,18.5,5.5z M16.5,7.5l-7,7c-0.14,0.14-0.32,0.21-0.5,0.21s-0.36-0.07-0.5-0.21c-0.28-0.28-0.28-0.72,0-1l7-7c0.28-0.28,0.72-0.28,1,0C16.78,6.78,16.78,7.22,16.5,7.5z" />
<path d="M7.35,18.47l-2.39,1.71c-0.1-0.27-0.22-0.51-0.42-0.71c-0.2-0.2-0.44-0.32-0.71-0.42l1.71-2.39C5.14,16.61,4.5,16.42,4,16l-3,7l7-3C7.58,19.5,7.39,18.86,7.35,18.47z" />
<path d="M21.99,2.01C21,1,19.5,0.5,19,1l-2,2c2,0,4,2,4,4l2-2C23.5,4.5,22.98,3.02,21.99,2.01z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 773 B

View file

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

View file

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

View file

@ -15,8 +15,6 @@
display: flex;
flex-direction: column;
flex-grow: 1;
min-width: 0;
min-height: 0;
.spacer {
flex: 1 1 100%;

View file

@ -15,8 +15,6 @@
display: flex;
flex-direction: row;
flex-grow: 1;
min-width: 0;
min-height: 0;
.spacer {
flex: 1 1 100%;

View file

@ -133,8 +133,6 @@
flex: 0 0 auto;
.widget-row {
min-height: 0;
.swatch-pair {
margin: 0;
}

View file

@ -77,8 +77,6 @@
<style lang="scss">
.layer-tree {
min-height: 0;
// Options bar
.options-bar {
height: 32px;

View file

@ -409,6 +409,7 @@ export default defineComponent({
nodeIcon(nodeName: string): IconName {
const iconMap: Record<string, IconName> = {
Output: "NodeOutput",
Imaginate: "NodeImaginate",
"Hue Shift Image": "NodeColorCorrection",
"Brighten Image": "NodeColorCorrection",
"Grayscale Image": "NodeColorCorrection",

View file

@ -76,6 +76,7 @@
.widget-row {
flex: 0 0 auto;
display: flex;
overflow: hidden;
min-height: 32px;
> * {

View file

@ -9,6 +9,7 @@
display: flex;
align-items: center;
flex: 0 0 auto;
max-height: 24px;
button {
flex: 0 0 auto;

View file

@ -96,12 +96,12 @@
}
> .text-label:first-of-type {
flex: 0 0 30%;
text-align: right;
flex: 0 0 25%;
margin-left: 16px;
}
> .parameter-expose-button ~ .text-label:first-of-type {
text-align: left;
margin-left: 0;
}
> .text-button {

View file

@ -113,7 +113,7 @@
}
textarea {
min-height: calc(18px * 4);
min-height: calc(18px * 2);
margin: 3px;
padding: 0 5px;
box-sizing: border-box;

View file

@ -168,7 +168,6 @@
background: var(--color-3-darkgray);
flex: 1 1 100%;
flex-direction: column;
min-height: 0;
.empty-panel {
background: var(--color-2-mildblack);

View file

@ -1,8 +1,8 @@
<template>
<LayoutRow class="workspace" data-workspace>
<LayoutRow class="workspace-grid-subdivision">
<LayoutCol class="workspace-grid-subdivision">
<LayoutRow class="workspace-grid-subdivision">
<LayoutRow class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['root'] }" data-subdivision-name="root">
<LayoutCol class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['content'] }" data-subdivision-name="content">
<LayoutRow class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['document'] }" data-subdivision-name="document">
<Panel
:panelType="portfolio.state.documents.length > 0 ? 'Document' : undefined"
:tabCloseButtons="true"
@ -15,17 +15,17 @@
/>
</LayoutRow>
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical @pointerdown="(e: PointerEvent) => resizePanel(e)" v-if="nodeGraphVisible"></LayoutRow>
<LayoutRow class="workspace-grid-subdivision" v-if="nodeGraphVisible">
<LayoutRow class="workspace-grid-subdivision" v-if="nodeGraphVisible" :style="{ 'flex-grow': panelSizes['graph'] }" data-subdivision-name="graph">
<Panel :panelType="'NodeGraph'" :tabLabels="[{ name: 'Node Graph' }]" :tabActiveIndex="0" />
</LayoutRow>
</LayoutCol>
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutCol>
<LayoutCol class="workspace-grid-subdivision" style="flex-grow: 0.2">
<LayoutRow class="workspace-grid-subdivision" style="flex-grow: 402">
<LayoutCol class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['details'] }" data-subdivision-name="details">
<LayoutRow class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['properties'] }" data-subdivision-name="properties">
<Panel :panelType="'Properties'" :tabLabels="[{ name: 'Properties' }]" :tabActiveIndex="0" />
</LayoutRow>
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutRow>
<LayoutRow class="workspace-grid-subdivision" style="flex-grow: 590">
<LayoutRow class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['layers'] }" data-subdivision-name="layers">
<Panel :panelType="'LayerTree'" :tabLabels="[{ name: 'Layer Tree' }]" :tabActiveIndex="0" />
</LayoutRow>
</LayoutCol>
@ -72,9 +72,23 @@ import LayoutRow from "@/components/layout/LayoutRow.vue";
import Panel from "@/components/window/workspace/Panel.vue";
const MIN_PANEL_SIZE = 100;
const PANEL_SIZES = {
/**/ root: 100,
/* ├── */ content: 80,
/* │ ├── */ document: 60,
/* │ └── */ graph: 40,
/* └── */ details: 20,
/* ├── */ properties: 45,
/* └── */ layers: 55,
};
export default defineComponent({
inject: ["workspace", "portfolio", "dialog", "editor"],
data() {
return {
panelSizes: PANEL_SIZES,
};
},
computed: {
activeDocumentIndex() {
return this.portfolio.state.activeDocumentIndex;
@ -97,16 +111,26 @@ export default defineComponent({
resizePanel(event: PointerEvent) {
const gutter = (event.target || undefined) as HTMLDivElement | undefined;
const nextSibling = (gutter?.nextElementSibling || undefined) as HTMLDivElement | undefined;
const previousSibling = (gutter?.previousElementSibling || undefined) as HTMLDivElement | undefined;
const prevSibling = (gutter?.previousElementSibling || undefined) as HTMLDivElement | undefined;
const parentElement = (gutter?.parentElement || undefined) as HTMLDivElement | undefined;
if (!gutter || !nextSibling || !previousSibling) return;
const nextSiblingName = (nextSibling?.getAttribute("data-subdivision-name") || undefined) as keyof typeof PANEL_SIZES;
const prevSiblingName = (prevSibling?.getAttribute("data-subdivision-name") || undefined) as keyof typeof PANEL_SIZES;
if (!gutter || !nextSibling || !prevSibling || !parentElement || !nextSiblingName || !prevSiblingName) return;
// Are we resizing horizontally?
const isHorizontal = gutter.getAttribute("data-gutter-horizontal") !== null;
// Get the current size in px of the panels being resized
// Get the current size in px of the panels being resized and the gutter
const gutterSize = isHorizontal ? gutter.getBoundingClientRect().width : gutter.getBoundingClientRect().height;
const nextSiblingSize = isHorizontal ? nextSibling.getBoundingClientRect().width : nextSibling.getBoundingClientRect().height;
const previousSiblingSize = isHorizontal ? previousSibling.getBoundingClientRect().width : previousSibling.getBoundingClientRect().height;
const prevSiblingSize = isHorizontal ? prevSibling.getBoundingClientRect().width : prevSibling.getBoundingClientRect().height;
const parentElementSize = isHorizontal ? parentElement.getBoundingClientRect().width : parentElement.getBoundingClientRect().height;
// Measure the resizing panels as a percentage of all sibling panels
const totalResizingSpaceOccupied = gutterSize + nextSiblingSize + prevSiblingSize;
const proportionBeingResized = totalResizingSpaceOccupied / parentElementSize;
// Prevent cursor flicker as mouse temporarily leaves the gutter
gutter.setPointerCapture(event.pointerId);
@ -118,10 +142,10 @@ export default defineComponent({
let mouseDelta = mouseStart - mouseCurrent;
mouseDelta = Math.max(nextSiblingSize + mouseDelta, MIN_PANEL_SIZE) - nextSiblingSize;
mouseDelta = previousSiblingSize - Math.max(previousSiblingSize - mouseDelta, MIN_PANEL_SIZE);
mouseDelta = prevSiblingSize - Math.max(prevSiblingSize - mouseDelta, MIN_PANEL_SIZE);
nextSibling.style.flexGrow = (nextSiblingSize + mouseDelta).toString();
previousSibling.style.flexGrow = (previousSiblingSize - mouseDelta).toString();
this.panelSizes[nextSiblingName] = ((nextSiblingSize + mouseDelta) / totalResizingSpaceOccupied) * proportionBeingResized * 100;
this.panelSizes[prevSiblingName] = ((prevSiblingSize - mouseDelta) / totalResizingSpaceOccupied) * proportionBeingResized * 100;
window.dispatchEvent(new CustomEvent("resize"));
};

View file

@ -2,7 +2,7 @@
import { reactive, readonly } from "vue";
import { downloadFileText, downloadFileBlob, upload } from "@/utility-functions/files";
import { imaginateGenerate, imaginateCheckConnection, imaginateTerminate, preloadAndSetImaginateBlobURL } from "@/utility-functions/imaginate";
import { imaginateGenerate, imaginateCheckConnection, imaginateTerminate, updateBackendImage } from "@/utility-functions/imaginate";
import { rasterizeSVG, rasterizeSVGCanvas } from "@/utility-functions/rasterization";
import { type Editor } from "@/wasm-communication/editor";
import {
@ -68,15 +68,15 @@ export function createPortfolioState(editor: Editor) {
imaginateCheckConnection(hostname, editor);
});
editor.subscriptions.subscribeJsMessage(TriggerImaginateGenerate, async (triggerImaginateGenerate) => {
const { documentId, layerPath, hostname, refreshFrequency, baseImage, maskImage, maskPaintMode, maskBlurPx, maskFillContent, parameters } = triggerImaginateGenerate;
const { documentId, layerPath, nodePath, hostname, refreshFrequency, baseImage, maskImage, maskPaintMode, maskBlurPx, maskFillContent, parameters } = triggerImaginateGenerate;
// Handle img2img mode
let image: Blob | undefined;
if (parameters.denoisingStrength !== undefined && baseImage !== undefined) {
// Rasterize the SVG to an image file
image = await rasterizeSVG(baseImage.svg, baseImage.size[0], baseImage.size[1], "image/png");
const buffer = new Uint8Array(baseImage.imageData.values()).buffer;
preloadAndSetImaginateBlobURL(editor, image, documentId, layerPath, baseImage.size[0], baseImage.size[1]);
image = new Blob([buffer], { type: baseImage.mime });
updateBackendImage(editor, image, documentId, layerPath, nodePath);
}
// Handle layer mask
@ -86,12 +86,12 @@ export function createPortfolioState(editor: Editor) {
mask = await rasterizeSVG(maskImage.svg, maskImage.size[0], maskImage.size[1], "image/png");
}
imaginateGenerate(parameters, image, mask, maskPaintMode, maskBlurPx, maskFillContent, hostname, refreshFrequency, documentId, layerPath, editor);
imaginateGenerate(parameters, image, mask, maskPaintMode, maskBlurPx, maskFillContent, hostname, refreshFrequency, documentId, layerPath, nodePath, editor);
});
editor.subscriptions.subscribeJsMessage(TriggerImaginateTerminate, async (triggerImaginateTerminate) => {
const { documentId, layerPath, hostname } = triggerImaginateTerminate;
const { documentId, layerPath, nodePath, hostname } = triggerImaginateTerminate;
imaginateTerminate(hostname, documentId, layerPath, editor);
imaginateTerminate(hostname, documentId, layerPath, nodePath, editor);
});
editor.subscriptions.subscribeJsMessage(UpdateImageData, (updateImageData) => {
updateImageData.imageData.forEach(async (element) => {
@ -109,12 +109,12 @@ export function createPortfolioState(editor: Editor) {
});
});
editor.subscriptions.subscribeJsMessage(TriggerNodeGraphFrameGenerate, async (triggerNodeGraphFrameGenerate) => {
const { documentId, layerPath, svg, size } = triggerNodeGraphFrameGenerate;
const { documentId, layerPath, svg, size, imaginateNode } = triggerNodeGraphFrameGenerate;
// Rasterize the SVG to an image file
const imageData = (await rasterizeSVGCanvas(svg, size[0], size[1])).getContext("2d")?.getImageData(0, 0, size[0], size[1]);
if (imageData) editor.instance.processNodeGraphFrame(documentId, layerPath, new Uint8Array(imageData.data), imageData.width, imageData.height);
if (imageData) editor.instance.processNodeGraphFrame(documentId, layerPath, new Uint8Array(imageData.data), imageData.width, imageData.height, imaginateNode);
});
editor.subscriptions.subscribeJsMessage(TriggerRevokeBlobUrl, async (triggerRevokeBlobUrl) => {
URL.revokeObjectURL(triggerRevokeBlobUrl.url);

View file

@ -26,7 +26,6 @@ export async function upload<T extends "text" | "data">(acceptedExtensions: stri
return new Promise<UploadResult<T>>((resolve, _) => {
const element = document.createElement("input");
element.type = "file";
element.style.display = "none";
element.accept = acceptedExtensions;
element.addEventListener(

View file

@ -12,6 +12,7 @@ import Add from "@/../assets/icon-12px-solid/add.svg";
import Checkmark from "@/../assets/icon-12px-solid/checkmark.svg";
import CloseX from "@/../assets/icon-12px-solid/close-x.svg";
import DropdownArrow from "@/../assets/icon-12px-solid/dropdown-arrow.svg";
import Edit from "@/../assets/icon-12px-solid/edit.svg";
import Empty12px from "@/../assets/icon-12px-solid/empty-12px.svg";
import FullscreenEnter from "@/../assets/icon-12px-solid/fullscreen-enter.svg";
import FullscreenExit from "@/../assets/icon-12px-solid/fullscreen-exit.svg";
@ -47,6 +48,7 @@ const SOLID_12PX = {
Checkmark: { component: Checkmark, size: 12 },
CloseX: { component: CloseX, size: 12 },
DropdownArrow: { component: DropdownArrow, size: 12 },
Edit: { component: Edit, size: 12 },
Empty12px: { component: Empty12px, size: 12 },
FullscreenEnter: { component: FullscreenEnter, size: 12 },
FullscreenExit: { component: FullscreenExit, size: 12 },

View file

@ -31,6 +31,7 @@ export async function imaginateGenerate(
refreshFrequency: number,
documentId: bigint,
layerPath: BigUint64Array,
nodePath: BigUint64Array,
editor: Editor
): Promise<void> {
// Ignore a request to generate a new image while another is already being generated
@ -39,11 +40,11 @@ export async function imaginateGenerate(
terminated = false;
// Immediately set the progress to 0% so the backend knows to update its layout
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, 0, "Beginning");
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, nodePath, 0, "Beginning");
// Initiate a request to the computation server
const discloseUploadingProgress = (progress: number): void => {
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, progress * 100, "Uploading");
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, nodePath, progress * 100, "Uploading");
};
const { uploaded, result, xhr } = await generate(discloseUploadingProgress, hostname, image, mask, maskPaintMode, maskBlurPx, maskFillContent, parameters);
generatingAbortRequest = xhr;
@ -51,13 +52,13 @@ export async function imaginateGenerate(
try {
// Wait until the request is fully uploaded, which could be slow if the img2img source is large and the user is on a slow connection
await uploaded;
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, 0, "Generating");
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, nodePath, 0, "Generating");
// Begin polling for updates to the in-progress image generation at the specified interval
// Don't poll if the chosen interval is 0, or if the chosen sampling method does not support polling
if (refreshFrequency > 0) {
const interval = Math.max(refreshFrequency * 1000, 500);
scheduleNextPollingUpdate(interval, Date.now(), 0, editor, hostname, documentId, layerPath, parameters.resolution);
scheduleNextPollingUpdate(interval, Date.now(), 0, editor, hostname, documentId, layerPath, nodePath, parameters.resolution);
}
// Wait for the final image to be returned by the initial request containing either the full image or the last frame if it was terminated by the user
@ -75,16 +76,12 @@ export async function imaginateGenerate(
// Send the backend an updated status
const percent = terminated ? undefined : 100;
const newStatus = terminated ? "Terminated" : "Idle";
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, percent, newStatus);
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, nodePath, percent, newStatus);
// Send the backend a blob URL for the final image
preloadAndSetImaginateBlobURL(editor, blob, documentId, layerPath, parameters.resolution.x, parameters.resolution.y);
// Send the backend the blob data to be stored persistently in the layer
const u8Array = new Uint8Array(await blob.arrayBuffer());
editor.instance.setImaginateImageData(documentId, layerPath, u8Array);
updateBackendImage(editor, blob, documentId, layerPath, nodePath);
} catch {
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, undefined, "Terminated");
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, nodePath, undefined, "Terminated");
await imaginateCheckConnection(hostname, editor);
}
@ -93,19 +90,19 @@ export async function imaginateGenerate(
abortAndResetPolling();
}
export async function imaginateTerminate(hostname: string, documentId: bigint, layerPath: BigUint64Array, editor: Editor): Promise<void> {
export async function imaginateTerminate(hostname: string, documentId: bigint, layerPath: BigUint64Array, nodePath: BigUint64Array, editor: Editor): Promise<void> {
terminated = true;
abortAndResetPolling();
try {
await terminate(hostname);
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, undefined, "Terminating");
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, nodePath, undefined, "Terminating");
} catch {
abortAndResetGenerating();
abortAndResetPolling();
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, undefined, "Terminated");
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, nodePath, undefined, "Terminated");
await imaginateCheckConnection(hostname, editor);
}
@ -116,15 +113,21 @@ export async function imaginateCheckConnection(hostname: string, editor: Editor)
editor.instance.setImaginateServerStatus(serverReached);
}
export async function preloadAndSetImaginateBlobURL(editor: Editor, blob: Blob, documentId: bigint, layerPath: BigUint64Array, width: number, height: number): Promise<void> {
const blobURL = URL.createObjectURL(blob);
// Converts the blob image into a list of pixels using an invisible canvas.
export async function updateBackendImage(editor: Editor, blob: Blob, documentId: bigint, layerPath: BigUint64Array, nodePath: BigUint64Array): Promise<void> {
const image = await createImageBitmap(blob);
const canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Could not create canvas context");
ctx.drawImage(image, 0, 0);
// Pre-decode the image so it is ready to be drawn instantly once it's placed into the viewport SVG
const image = new Image();
image.src = blobURL;
await image.decode();
// Send the backend the blob data to be stored persistently in the layer
const imageData = ctx.getImageData(0, 0, image.width, image.height);
const u8Array = new Uint8Array(imageData.data);
editor.instance.setImaginateBlobURL(documentId, layerPath, blobURL, width, height);
editor.instance.setImaginateImageData(documentId, layerPath, nodePath, u8Array, imageData.width, imageData.height);
}
// ABORTING AND RESETTING HELPERS
@ -150,6 +153,7 @@ function scheduleNextPollingUpdate(
hostname: string,
documentId: bigint,
layerPath: BigUint64Array,
nodePath: BigUint64Array,
resolution: XY
): void {
// Pick a future time that keeps to the user-requested interval if possible, but on slower connections will go as fast as possible without overlapping itself
@ -165,10 +169,10 @@ function scheduleNextPollingUpdate(
// After waiting for the polling result back from the server, if during that intervening time the user has terminated the generation, exit so we don't overwrite that terminated status
if (terminated) return;
if (blob) preloadAndSetImaginateBlobURL(editor, blob, documentId, layerPath, resolution.x, resolution.y);
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, percentComplete, "Generating");
if (blob) updateBackendImage(editor, blob, documentId, layerPath, nodePath);
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, nodePath, percentComplete, "Generating");
scheduleNextPollingUpdate(interval, nextTimeoutBegan, 0, editor, hostname, documentId, layerPath, resolution);
scheduleNextPollingUpdate(interval, nextTimeoutBegan, 0, editor, hostname, documentId, layerPath, nodePath, resolution);
} catch {
if (generatingAbortRequest === undefined) return;
@ -178,7 +182,7 @@ function scheduleNextPollingUpdate(
await imaginateCheckConnection(hostname, editor);
} else {
scheduleNextPollingUpdate(interval, nextTimeoutBegan, pollingRetries + 1, editor, hostname, documentId, layerPath, resolution);
scheduleNextPollingUpdate(interval, nextTimeoutBegan, pollingRetries + 1, editor, hostname, documentId, layerPath, nodePath, resolution);
}
}
}, timeFromNow);

View file

@ -41,11 +41,12 @@ export async function fetchImage(path: BigUint64Array, mime: string, documentId:
}
// export async function dispatchTauri(message: string): Promise<string> {
export async function dispatchTauri(message: any): Promise<void> {
export async function dispatchTauri(message: unknown): Promise<void> {
try {
const response = await invoke("handle_message", { message });
editorInstance?.tauriResponse(response);
} catch {
// eslint-disable-next-line no-console
console.error("Failed to dispatch Tauri message");
}
}

View file

@ -519,8 +519,8 @@ export class TriggerImaginateGenerate extends JsMessage {
@Type(() => ImaginateBaseImage)
readonly baseImage!: ImaginateBaseImage | undefined;
@Type(() => ImaginateBaseImage)
readonly maskImage: ImaginateBaseImage | undefined;
@Type(() => ImaginateMaskImage)
readonly maskImage: ImaginateMaskImage | undefined;
readonly maskPaintMode!: string;
@ -535,11 +535,22 @@ export class TriggerImaginateGenerate extends JsMessage {
readonly documentId!: bigint;
readonly layerPath!: BigUint64Array;
readonly nodePath!: BigUint64Array;
}
export class ImaginateMaskImage {
readonly svg!: string;
readonly size!: [number, number];
}
export class ImaginateBaseImage {
readonly svg!: string;
readonly mime!: string;
readonly imageData!: Uint8Array;
@TupleToVec2
readonly size!: [number, number];
}
@ -571,6 +582,8 @@ export class TriggerImaginateTerminate extends JsMessage {
readonly layerPath!: BigUint64Array;
readonly nodePath!: BigUint64Array;
readonly hostname!: string;
}
@ -582,6 +595,8 @@ export class TriggerNodeGraphFrameGenerate extends JsMessage {
readonly svg!: string;
readonly size!: [number, number];
readonly imaginateNode!: BigUint64Array | undefined;
}
export class TriggerRefreshBoundsOfViewports extends JsMessage {}

View file

@ -12,8 +12,8 @@ use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
use editor::messages::portfolio::utility_types::{ImaginateServerStatus, Platform};
use editor::messages::prelude::*;
use graph_craft::document::NodeId;
use graphene::color::Color;
use graphene::layers::imaginate_layer::ImaginateStatus;
use graphene::LayerId;
use serde::Serialize;
@ -508,27 +508,23 @@ impl JsEditorHandle {
/// Sends the blob URL generated by JS to the Imaginate layer in the respective document
#[wasm_bindgen(js_name = setImaginateImageData)]
pub fn set_imaginate_image_data(&self, document_id: u64, layer_path: Vec<LayerId>, image_data: Vec<u8>) {
let message = PortfolioMessage::ImaginateSetImageData { document_id, layer_path, image_data };
self.dispatch(message);
}
/// Sends the blob URL generated by JS to the Imaginate layer in the respective document
#[wasm_bindgen(js_name = setImaginateBlobURL)]
pub fn set_imaginate_blob_url(&self, document_id: u64, layer_path: Vec<LayerId>, blob_url: String, width: f64, height: f64) {
let resolution = (width, height);
let message = PortfolioMessage::ImaginateSetBlobUrl {
pub fn set_imaginate_image_data(&self, document_id: u64, layer_path: Vec<LayerId>, node_path: Vec<NodeId>, image_data: Vec<u8>, width: u32, height: u32) {
let message = PortfolioMessage::ImaginateSetImageData {
document_id,
node_path,
layer_path,
blob_url,
resolution,
image_data,
width,
height,
};
self.dispatch(message);
}
/// Notifies the Imaginate layer of a new percentage of completion and whether or not it's currently generating
#[wasm_bindgen(js_name = setImaginateGeneratingStatus)]
pub fn set_imaginate_generating_status(&self, document_id: u64, path: Vec<LayerId>, percent: Option<f64>, status: String) {
pub fn set_imaginate_generating_status(&self, document_id: u64, layer_path: Vec<LayerId>, node_path: Vec<NodeId>, percent: Option<f64>, status: String) {
use graph_craft::imaginate_input::ImaginateStatus;
let status = match status.as_str() {
"Idle" => ImaginateStatus::Idle,
"Beginning" => ImaginateStatus::Beginning,
@ -541,7 +537,13 @@ impl JsEditorHandle {
let percent = if matches!(status, ImaginateStatus::Uploading(_)) { None } else { percent };
let message = PortfolioMessage::ImaginateSetGeneratingStatus { document_id, path, percent, status };
let message = PortfolioMessage::ImaginateSetGeneratingStatus {
document_id,
layer_path,
node_path,
percent,
status,
};
self.dispatch(message);
}
@ -563,12 +565,13 @@ impl JsEditorHandle {
/// Sends the blob URL generated by JS to the Imaginate layer in the respective document
#[wasm_bindgen(js_name = processNodeGraphFrame)]
pub fn process_node_graph_frame(&self, document_id: u64, layer_path: Vec<LayerId>, image_data: Vec<u8>, width: u32, height: u32) {
pub fn process_node_graph_frame(&self, document_id: u64, layer_path: Vec<LayerId>, image_data: Vec<u8>, width: u32, height: u32, imaginate_node: Option<Vec<NodeId>>) {
let message = PortfolioMessage::ProcessNodeGraphFrame {
document_id,
layer_path,
image_data,
size: (width, height),
imaginate_node,
};
self.dispatch(message);
}

View file

@ -19,6 +19,12 @@ impl Color {
pub const RED: Color = Color::from_unsafe(1., 0., 0.);
pub const GREEN: Color = Color::from_unsafe(0., 1., 0.);
pub const BLUE: Color = Color::from_unsafe(0., 0., 1.);
pub const TRANSPARENT: Color = Self {
red: 0.,
green: 0.,
blue: 0.,
alpha: 0.,
};
/// Returns `Some(Color)` if `red`, `green`, `blue` and `alpha` have a valid value. Negative numbers (including `-0.0`), NaN, and infinity are not valid values and return `None`.
/// Alpha values greater than `1.0` are not valid.

View file

@ -2,7 +2,6 @@ use crate::boolean_ops::composite_boolean_operation;
use crate::intersection::Quad;
use crate::layers::folder_layer::FolderLayer;
use crate::layers::image_layer::ImageLayer;
use crate::layers::imaginate_layer::{ImaginateImageData, ImaginateLayer, ImaginateStatus};
use crate::layers::layer_info::{Layer, LayerData, LayerDataType, LayerDataTypeDiscriminant};
use crate::layers::nodegraph_layer::NodeGraphFrameLayer;
use crate::layers::shape_layer::ShapeLayer;
@ -142,7 +141,7 @@ impl Document {
return Ok(&self.root);
}
let (path, id) = split_path(path)?;
self.folder(&path)?.layer(id).ok_or_else(|| DocumentError::LayerNotFound(path.into()))
self.folder(path)?.layer(id).ok_or_else(|| DocumentError::LayerNotFound(path.into()))
}
/// Returns a mutable reference to the layer or folder at the path.
@ -607,15 +606,13 @@ impl Document {
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
}
Operation::AddImaginateFrame { path, insert_index, transform } => {
let layer = Layer::new(LayerDataType::Imaginate(ImaginateLayer::default()), transform);
self.set_layer(&path, layer, insert_index)?;
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
}
Operation::AddNodeGraphFrame { path, insert_index, transform } => {
let layer = Layer::new(LayerDataType::NodeGraphFrame(NodeGraphFrameLayer::default()), transform);
Operation::AddNodeGraphFrame {
path,
insert_index,
transform,
network,
} => {
let layer = Layer::new(LayerDataType::NodeGraphFrame(NodeGraphFrameLayer { network, ..Default::default() }), transform);
self.set_layer(&path, layer, insert_index)?;
@ -837,52 +834,15 @@ impl Document {
node_graph_frame.blob_url = Some(blob_url);
node_graph_frame.dimensions = resolution.into();
}
LayerDataType::Imaginate(imaginate) => {
imaginate.blob_url = Some(blob_url);
imaginate.dimensions = resolution.into();
}
_ => panic!("Incorrectly trying to set the image blob URL for a layer that is not an Image, NodeGraphFrame or Imaginate layer type"),
}
self.mark_as_dirty(&layer_path)?;
Some([vec![DocumentChanged, LayerChanged { path: layer_path.clone() }], update_thumbnails_upstream(&layer_path)].concat())
}
Operation::ImaginateSetImageData { layer_path, image_data } => {
let layer = self.layer_mut(&layer_path).expect("Setting Imaginate image data for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
let image_data = std::sync::Arc::new(image_data);
imaginate.image_data = Some(ImaginateImageData { image_data });
} else {
panic!("Incorrectly trying to set image data for a layer that is not an Imaginate layer type");
}
Some(vec![LayerChanged { path: layer_path.clone() }])
}
Operation::ImaginateSetGeneratingStatus { path, percent, status } => {
let layer = self.layer_mut(&path).expect("Generating Imaginate for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
if let Some(percentage) = percent {
imaginate.percent_complete = percentage;
}
if status == ImaginateStatus::Generating {
imaginate.image_data = None;
}
imaginate.status = status;
} else {
panic!("Incorrectly trying to set the generating status for a layer that is not an Imaginate layer type");
}
Some(vec![LayerChanged { path: path.clone() }])
}
Operation::ClearBlobURL { path } => {
let layer = self.layer_mut(&path).expect("Clearing Imaginate image for invalid layer");
let layer = self.layer_mut(&path).expect("Clearing node graph image for invalid layer");
match &mut layer.data {
LayerDataType::Imaginate(imaginate) => {
imaginate.image_data = None;
imaginate.blob_url = None;
imaginate.status = ImaginateStatus::Idle;
imaginate.percent_complete = 0.;
}
LayerDataType::NodeGraphFrame(node_graph) => {
node_graph.image_data = None;
node_graph.blob_url = None;
@ -892,164 +852,6 @@ impl Document {
self.mark_as_dirty(&path)?;
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
}
Operation::ImaginateSetNegativePrompt { path, negative_prompt } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate negative prompt for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.negative_prompt = negative_prompt;
} else {
panic!("Incorrectly trying to set the negative prompt for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetPrompt { path, prompt } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate prompt for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.prompt = prompt;
} else {
panic!("Incorrectly trying to set the prompt for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetMaskBlurPx { path, mask_blur_px } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate mask blur for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.mask_blur_px = mask_blur_px;
} else {
panic!("Incorrectly trying to set the mask blur for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetMaskFillContent { path, mode } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate mask fill content for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.mask_fill_content = mode;
} else {
panic!("Incorrectly trying to set the mask fill content for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetMaskPaintMode { path, paint } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate mask paint mode for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.mask_paint_mode = paint;
} else {
panic!("Incorrectly trying to set the mask paint mode for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetCfgScale { path, cfg_scale } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate CFG scale for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.cfg_scale = cfg_scale;
} else {
panic!("Incorrectly trying to set the CFG scale for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetDenoisingStrength { path, denoising_strength } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate denoising strength for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.denoising_strength = denoising_strength;
} else {
panic!("Incorrectly trying to set the denoising strength for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetLayerPath { path, layer_path } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate layer path strength for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.mask_layer_ref = layer_path;
} else {
panic!("Incorrectly trying to set the layer path for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetSamples { path, samples } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate samples for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.samples = samples;
} else {
panic!("Incorrectly trying to set the samples for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::SetImaginateSamplingMethod { path, method } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate sampling method for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.sampling_method = method;
} else {
panic!("Incorrectly trying to set the sampling method for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetScaleFromResolution { path } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate scale from resolution for invalid layer");
let (width, height) = pick_layer_safe_imaginate_resolution(layer, font_cache);
let current_width = layer.transform.transform_vector2((1., 0.).into()).length();
let current_height = layer.transform.transform_vector2((0., 1.).into()).length();
let scale_x_by = width as f64 / current_width;
let scale_y_by = height as f64 / current_height;
let scale_by_vector = DVec2::new(scale_x_by, scale_y_by);
let scale_by_matrix = DAffine2::from_scale_angle_translation(scale_by_vector, 0., (0., 0.).into());
layer.transform = layer.transform * scale_by_matrix;
self.mark_as_dirty(&path)?;
Some([update_thumbnails_upstream(&path), vec![DocumentChanged, LayerChanged { path }]].concat())
}
Operation::ImaginateSetSeed { path, seed } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate seed for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.seed = seed;
} else {
panic!("Incorrectly trying to set the seed for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetUseImg2Img { path, use_img2img } => {
let layer = self.layer_mut(&path).expect("Calling Imaginate img2img for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.use_img2img = use_img2img;
} else {
panic!("Incorrectly trying to set the img2img status for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetRestoreFaces { path, restore_faces } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate restore faces for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.restore_faces = restore_faces;
} else {
panic!("Incorrectly trying to set the restore faces status for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetTiling { path, tiling } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate tiling for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.tiling = tiling;
} else {
panic!("Incorrectly trying to set the tiling status for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::SetPivot { layer_path, pivot } => {
let layer = self.layer_mut(&layer_path).expect("Setting pivot for invalid layer");
layer.pivot = pivot.into();
@ -1161,6 +963,33 @@ impl Document {
self.mark_as_dirty(&path)?;
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
}
Operation::TransformLayerScaleAroundPivot { path, scale_factor } => {
let layer = self.layer_mut(&path)?;
let offset = DAffine2::from_translation(-layer.pivot);
let scale = DAffine2::from_scale(scale_factor.into());
let offset_back = DAffine2::from_translation(layer.pivot);
layer.transform = layer.transform * offset_back * scale * offset;
self.mark_as_dirty(&path)?;
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
}
Operation::SetLayerScaleAroundPivot { path, new_scale } => {
let layer = self.layer_mut(&path)?;
let matrix = layer.transform.to_cols_array();
let old_scale = (matrix[0], matrix[3]);
let scale_factor = DVec2::from(new_scale) / DVec2::from(old_scale);
let offset = DAffine2::from_translation(-layer.pivot);
let scale = DAffine2::from_scale(scale_factor);
let offset_back = DAffine2::from_translation(layer.pivot);
layer.transform = layer.transform * offset_back * scale * offset;
self.mark_as_dirty(&path)?;
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
}
Operation::SetLayerTransform { path, transform } => {
let transform = DAffine2::from_cols_array(&transform);
let layer = self.layer_mut(&path)?;

View file

@ -1,351 +0,0 @@
use super::base64_serde;
use super::layer_info::LayerData;
use super::style::{RenderData, ViewMode};
use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::layers::text_layer::FontCache;
use crate::LayerId;
use glam::{DAffine2, DMat2, DVec2};
use kurbo::{Affine, BezPath, Shape as KurboShape};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
#[derive(Clone, PartialEq, Deserialize, Serialize)]
pub struct ImaginateLayer {
// User-configurable layer parameters
pub seed: u64,
pub samples: u32,
pub sampling_method: ImaginateSamplingMethod,
pub use_img2img: bool,
pub denoising_strength: f64,
pub mask_layer_ref: Option<Vec<LayerId>>,
pub mask_paint_mode: ImaginateMaskPaintMode,
pub mask_blur_px: u32,
pub mask_fill_content: ImaginateMaskFillContent,
pub cfg_scale: f64,
pub prompt: String,
pub negative_prompt: String,
pub restore_faces: bool,
pub tiling: bool,
// Image stored in layer after generation completes
pub image_data: Option<ImaginateImageData>,
pub mime: String,
/// 0 is not started, 100 is complete.
pub percent_complete: f64,
// TODO: Have the browser dispose of this blob URL when this is dropped (like when the layer is deleted)
#[serde(skip)]
pub blob_url: Option<String>,
#[serde(skip)]
pub status: ImaginateStatus,
#[serde(skip)]
pub dimensions: DVec2,
}
#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum ImaginateStatus {
#[default]
Idle,
Beginning,
Uploading(f64),
Generating,
Terminating,
Terminated,
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
pub struct ImaginateImageData {
#[serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64")]
pub image_data: std::sync::Arc<Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ImaginateBaseImage {
pub svg: String,
pub size: DVec2,
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)]
pub enum ImaginateMaskPaintMode {
#[default]
Inpaint,
Outpaint,
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)]
pub enum ImaginateMaskFillContent {
#[default]
Fill,
Original,
LatentNoise,
LatentNothing,
}
impl ImaginateMaskFillContent {
pub fn list() -> [ImaginateMaskFillContent; 4] {
[
ImaginateMaskFillContent::Fill,
ImaginateMaskFillContent::Original,
ImaginateMaskFillContent::LatentNoise,
ImaginateMaskFillContent::LatentNothing,
]
}
}
impl std::fmt::Display for ImaginateMaskFillContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImaginateMaskFillContent::Fill => write!(f, "Smeared Surroundings"),
ImaginateMaskFillContent::Original => write!(f, "Original Base Image"),
ImaginateMaskFillContent::LatentNoise => write!(f, "Randomness (Latent Noise)"),
ImaginateMaskFillContent::LatentNothing => write!(f, "Neutral (Latent Nothing)"),
}
}
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Deserialize, Serialize)]
pub enum ImaginateSamplingMethod {
#[default]
EulerA,
Euler,
LMS,
Heun,
DPM2,
DPM2A,
DPMPlusPlus2sA,
DPMPlusPlus2m,
DPMFast,
DPMAdaptive,
LMSKarras,
DPM2Karras,
DPM2AKarras,
DPMPlusPlus2sAKarras,
DPMPlusPlus2mKarras,
DDIM,
PLMS,
}
impl ImaginateSamplingMethod {
pub fn api_value(&self) -> &str {
match self {
ImaginateSamplingMethod::EulerA => "Euler a",
ImaginateSamplingMethod::Euler => "Euler",
ImaginateSamplingMethod::LMS => "LMS",
ImaginateSamplingMethod::Heun => "Heun",
ImaginateSamplingMethod::DPM2 => "DPM2",
ImaginateSamplingMethod::DPM2A => "DPM2 a",
ImaginateSamplingMethod::DPMPlusPlus2sA => "DPM++ 2S a",
ImaginateSamplingMethod::DPMPlusPlus2m => "DPM++ 2M",
ImaginateSamplingMethod::DPMFast => "DPM fast",
ImaginateSamplingMethod::DPMAdaptive => "DPM adaptive",
ImaginateSamplingMethod::LMSKarras => "LMS Karras",
ImaginateSamplingMethod::DPM2Karras => "DPM2 Karras",
ImaginateSamplingMethod::DPM2AKarras => "DPM2 a Karras",
ImaginateSamplingMethod::DPMPlusPlus2sAKarras => "DPM++ 2S a Karras",
ImaginateSamplingMethod::DPMPlusPlus2mKarras => "DPM++ 2M Karras",
ImaginateSamplingMethod::DDIM => "DDIM",
ImaginateSamplingMethod::PLMS => "PLMS",
}
}
pub fn list() -> [ImaginateSamplingMethod; 17] {
[
ImaginateSamplingMethod::EulerA,
ImaginateSamplingMethod::Euler,
ImaginateSamplingMethod::LMS,
ImaginateSamplingMethod::Heun,
ImaginateSamplingMethod::DPM2,
ImaginateSamplingMethod::DPM2A,
ImaginateSamplingMethod::DPMPlusPlus2sA,
ImaginateSamplingMethod::DPMPlusPlus2m,
ImaginateSamplingMethod::DPMFast,
ImaginateSamplingMethod::DPMAdaptive,
ImaginateSamplingMethod::LMSKarras,
ImaginateSamplingMethod::DPM2Karras,
ImaginateSamplingMethod::DPM2AKarras,
ImaginateSamplingMethod::DPMPlusPlus2sAKarras,
ImaginateSamplingMethod::DPMPlusPlus2mKarras,
ImaginateSamplingMethod::DDIM,
ImaginateSamplingMethod::PLMS,
]
}
}
impl std::fmt::Display for ImaginateSamplingMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImaginateSamplingMethod::EulerA => write!(f, "Euler A (Recommended)"),
ImaginateSamplingMethod::Euler => write!(f, "Euler"),
ImaginateSamplingMethod::LMS => write!(f, "LMS"),
ImaginateSamplingMethod::Heun => write!(f, "Heun"),
ImaginateSamplingMethod::DPM2 => write!(f, "DPM2"),
ImaginateSamplingMethod::DPM2A => write!(f, "DPM2 A"),
ImaginateSamplingMethod::DPMPlusPlus2sA => write!(f, "DPM++ 2S a"),
ImaginateSamplingMethod::DPMPlusPlus2m => write!(f, "DPM++ 2M"),
ImaginateSamplingMethod::DPMFast => write!(f, "DPM Fast"),
ImaginateSamplingMethod::DPMAdaptive => write!(f, "DPM Adaptive"),
ImaginateSamplingMethod::LMSKarras => write!(f, "LMS Karras"),
ImaginateSamplingMethod::DPM2Karras => write!(f, "DPM2 Karras"),
ImaginateSamplingMethod::DPM2AKarras => write!(f, "DPM2 A Karras"),
ImaginateSamplingMethod::DPMPlusPlus2sAKarras => write!(f, "DPM++ 2S a Karras"),
ImaginateSamplingMethod::DPMPlusPlus2mKarras => write!(f, "DPM++ 2M Karras"),
ImaginateSamplingMethod::DDIM => write!(f, "DDIM"),
ImaginateSamplingMethod::PLMS => write!(f, "PLMS"),
}
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ImaginateGenerationParameters {
pub seed: u64,
pub samples: u32,
/// Use `ImaginateSamplingMethod::api_value()` to generate this string
#[serde(rename = "samplingMethod")]
pub sampling_method: String,
#[serde(rename = "denoisingStrength")]
pub denoising_strength: Option<f64>,
#[serde(rename = "cfgScale")]
pub cfg_scale: f64,
pub prompt: String,
#[serde(rename = "negativePrompt")]
pub negative_prompt: String,
pub resolution: (u64, u64),
#[serde(rename = "restoreFaces")]
pub restore_faces: bool,
pub tiling: bool,
}
impl Default for ImaginateLayer {
fn default() -> Self {
Self {
seed: 0,
samples: 30,
sampling_method: Default::default(),
use_img2img: false,
denoising_strength: 0.66,
mask_paint_mode: ImaginateMaskPaintMode::default(),
mask_layer_ref: None,
mask_blur_px: 4,
mask_fill_content: ImaginateMaskFillContent::default(),
cfg_scale: 10.,
prompt: "".into(),
negative_prompt: "".into(),
restore_faces: false,
tiling: false,
image_data: None,
mime: "image/png".into(),
blob_url: None,
percent_complete: 0.,
status: Default::default(),
dimensions: Default::default(),
}
}
}
impl LayerData for ImaginateLayer {
fn render(&mut self, svg: &mut String, _svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: RenderData) {
let transform = self.transform(transforms, render_data.view_mode);
let inverse = transform.inverse();
let (width, height) = (transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length());
if !inverse.is_finite() {
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
return;
}
let _ = writeln!(svg, r#"<g transform="matrix("#);
inverse.to_cols_array().iter().enumerate().for_each(|(i, entry)| {
let _ = svg.write_str(&(entry.to_string() + if i == 5 { "" } else { "," }));
});
let _ = svg.write_str(r#")">"#);
if let Some(blob_url) = &self.blob_url {
let _ = write!(
svg,
r#"<image width="{}" height="{}" preserveAspectRatio="none" href="{}" transform="matrix("#,
width.abs(),
height.abs(),
blob_url
);
} else {
let _ = write!(
svg,
r#"<rect width="{}" height="{}" fill="none" stroke="var(--color-data-raster)" stroke-width="3" stroke-dasharray="8" transform="matrix("#,
width.abs(),
height.abs(),
);
}
(transform * DAffine2::from_scale((width, height).into()).inverse())
.to_cols_array()
.iter()
.enumerate()
.for_each(|(i, entry)| {
let _ = svg.write_str(&(entry.to_string() + if i == 5 { "" } else { "," }));
});
let _ = svg.write_str(r#")" /> </g>"#);
}
fn bounding_box(&self, transform: glam::DAffine2, _font_cache: &FontCache) -> Option<[DVec2; 2]> {
let mut path = self.bounds();
if transform.matrix2 == DMat2::ZERO {
return None;
}
path.apply_affine(glam_to_kurbo(transform));
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
Some([(x0, y0).into(), (x1, y1).into()])
}
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, _font_cache: &FontCache) {
if intersect_quad_bez_path(quad, &self.bounds(), true) {
intersections.push(path.clone());
}
}
}
impl ImaginateLayer {
pub fn transform(&self, transforms: &[DAffine2], mode: ViewMode) -> DAffine2 {
let start = match mode {
ViewMode::Outline => 0,
_ => (transforms.len() as i32 - 1).max(0) as usize,
};
transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY)
}
fn bounds(&self) -> BezPath {
kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.)).to_path(0.)
}
}
fn glam_to_kurbo(transform: DAffine2) -> Affine {
Affine::new(transform.to_cols_array())
}
impl std::fmt::Debug for ImaginateLayer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ImaginateLayer")
.field("seed", &self.seed)
.field("samples", &self.samples)
.field("use_img2img", &self.use_img2img)
.field("denoising_strength", &self.denoising_strength)
.field("cfg_scale", &self.cfg_scale)
.field("prompt", &self.prompt)
.field("negative_prompt", &self.negative_prompt)
.field("restore_faces", &self.restore_faces)
.field("tiling", &self.tiling)
.field("image_data", &self.image_data.as_ref().map(|_| "..."))
.field("mime", &self.mime)
.field("percent_complete", &self.percent_complete)
.field("blob_url", &self.blob_url)
.field("status", &self.status)
.field("dimensions", &self.dimensions)
.finish()
}
}

View file

@ -1,7 +1,6 @@
use super::blend_mode::BlendMode;
use super::folder_layer::FolderLayer;
use super::image_layer::ImageLayer;
use super::imaginate_layer::ImaginateLayer;
use super::nodegraph_layer::NodeGraphFrameLayer;
use super::shape_layer::ShapeLayer;
use super::style::{PathStyle, RenderData};
@ -29,8 +28,6 @@ pub enum LayerDataType {
Text(TextLayer),
/// A layer that wraps an [ImageLayer] struct.
Image(ImageLayer),
/// A layer that wraps an [ImaginateLayer] struct.
Imaginate(ImaginateLayer),
/// A layer that wraps an [NodeGraphFrameLayer] struct.
NodeGraphFrame(NodeGraphFrameLayer),
}
@ -42,7 +39,6 @@ impl LayerDataType {
LayerDataType::Folder(f) => f,
LayerDataType::Text(t) => t,
LayerDataType::Image(i) => i,
LayerDataType::Imaginate(a) => a,
LayerDataType::NodeGraphFrame(n) => n,
}
}
@ -53,7 +49,6 @@ impl LayerDataType {
LayerDataType::Folder(f) => f,
LayerDataType::Text(t) => t,
LayerDataType::Image(i) => i,
LayerDataType::Imaginate(a) => a,
LayerDataType::NodeGraphFrame(n) => n,
}
}
@ -65,7 +60,6 @@ pub enum LayerDataTypeDiscriminant {
Shape,
Text,
Image,
Imaginate,
NodeGraphFrame,
}
@ -76,7 +70,6 @@ impl fmt::Display for LayerDataTypeDiscriminant {
LayerDataTypeDiscriminant::Shape => write!(f, "Shape"),
LayerDataTypeDiscriminant::Text => write!(f, "Text"),
LayerDataTypeDiscriminant::Image => write!(f, "Image"),
LayerDataTypeDiscriminant::Imaginate => write!(f, "Imaginate"),
LayerDataTypeDiscriminant::NodeGraphFrame => write!(f, "Node Graph Frame"),
}
}
@ -91,7 +84,6 @@ impl From<&LayerDataType> for LayerDataTypeDiscriminant {
Shape(_) => LayerDataTypeDiscriminant::Shape,
Text(_) => LayerDataTypeDiscriminant::Text,
Image(_) => LayerDataTypeDiscriminant::Image,
Imaginate(_) => LayerDataTypeDiscriminant::Imaginate,
NodeGraphFrame(_) => LayerDataTypeDiscriminant::NodeGraphFrame,
}
}
@ -499,24 +491,6 @@ impl Layer {
}
}
/// Get a mutable reference to the Imaginate element wrapped by the layer.
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Imaginate`.
pub fn as_imaginate_mut(&mut self) -> Result<&mut ImaginateLayer, DocumentError> {
match &mut self.data {
LayerDataType::Imaginate(imaginate) => Ok(imaginate),
_ => Err(DocumentError::NotAnImaginate),
}
}
/// Get a reference to the Imaginate element wrapped by the layer.
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Imaginate`.
pub fn as_imaginate(&self) -> Result<&ImaginateLayer, DocumentError> {
match &self.data {
LayerDataType::Imaginate(imaginate) => Ok(imaginate),
_ => Err(DocumentError::NotAnImaginate),
}
}
pub fn style(&self) -> Result<&PathStyle, DocumentError> {
match &self.data {
LayerDataType::Shape(s) => Ok(&s.style),

View file

@ -22,8 +22,6 @@ pub mod blend_mode;
pub mod folder_layer;
/// Contains the [ImageLayer](image_layer::ImageLayer) type that contains a bitmap image.
pub mod image_layer;
/// Contains the [ImaginateLayer](imaginate_layer::ImaginateLayer) type that contains a bitmap image generated based on a prompt and optionally the layers beneath it.
pub mod imaginate_layer;
/// Contains the base [Layer](layer_info::Layer) type, an abstraction over the different types of layers.
pub mod layer_info;
/// Contains the [NodegraphLayer](nodegraph_layer::NodegraphLayer) type that contains a node graph.

View file

@ -6,13 +6,11 @@ use crate::layers::text_layer::FontCache;
use crate::LayerId;
use glam::{DAffine2, DMat2, DVec2};
use graph_craft::proto::Type;
use kurbo::{Affine, BezPath, Shape as KurboShape};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::fmt::Write;
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
pub struct NodeGraphFrameLayer {
// Image stored in layer after generation completes
pub mime: String,
@ -115,43 +113,3 @@ impl NodeGraphFrameLayer {
fn glam_to_kurbo(transform: DAffine2) -> Affine {
Affine::new(transform.to_cols_array())
}
impl Default for NodeGraphFrameLayer {
fn default() -> Self {
use graph_craft::document::*;
use graph_craft::proto::NodeIdentifier;
Self {
mime: String::new(),
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", &[Type::Generic(Cow::Borrowed("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", &[Type::Generic(Cow::Borrowed("T"))])),
metadata: DocumentNodeMetadata { position: (20, 4) },
},
),
]
.into_iter()
.collect(),
},
blob_url: None,
dimensions: DVec2::ZERO,
image_data: None,
}
}
}

View file

@ -1,6 +1,5 @@
use crate::boolean_ops::BooleanOperation as BooleanOperationType;
use crate::layers::blend_mode::BlendMode;
use crate::layers::imaginate_layer::{ImaginateMaskFillContent, ImaginateMaskPaintMode, ImaginateSamplingMethod, ImaginateStatus};
use crate::layers::layer_info::Layer;
use crate::layers::style::{self, Stroke};
use crate::LayerId;
@ -53,15 +52,11 @@ pub enum Operation {
mime: String,
image_data: Vec<u8>,
},
AddImaginateFrame {
path: Vec<LayerId>,
insert_index: isize,
transform: [f64; 6],
},
AddNodeGraphFrame {
path: Vec<LayerId>,
insert_index: isize,
transform: [f64; 6],
network: graph_craft::document::NodeNetwork,
},
SetNodeGraphFrameImageData {
layer_path: Vec<LayerId>,
@ -79,74 +74,6 @@ pub enum Operation {
ClearBlobURL {
path: Vec<LayerId>,
},
ImaginateSetGeneratingStatus {
path: Vec<LayerId>,
percent: Option<f64>,
status: ImaginateStatus,
},
ImaginateSetImageData {
layer_path: Vec<LayerId>,
image_data: Vec<u8>,
},
ImaginateSetNegativePrompt {
path: Vec<LayerId>,
negative_prompt: String,
},
ImaginateSetPrompt {
path: Vec<LayerId>,
prompt: String,
},
ImaginateSetMaskBlurPx {
path: Vec<LayerId>,
mask_blur_px: u32,
},
ImaginateSetMaskFillContent {
path: Vec<LayerId>,
mode: ImaginateMaskFillContent,
},
ImaginateSetMaskPaintMode {
path: Vec<LayerId>,
paint: ImaginateMaskPaintMode,
},
ImaginateSetCfgScale {
path: Vec<LayerId>,
cfg_scale: f64,
},
ImaginateSetSamples {
path: Vec<LayerId>,
samples: u32,
},
SetImaginateSamplingMethod {
path: Vec<LayerId>,
method: ImaginateSamplingMethod,
},
ImaginateSetScaleFromResolution {
path: Vec<LayerId>,
},
ImaginateSetSeed {
path: Vec<LayerId>,
seed: u64,
},
ImaginateSetDenoisingStrength {
path: Vec<LayerId>,
denoising_strength: f64,
},
ImaginateSetLayerPath {
path: Vec<LayerId>,
layer_path: Option<Vec<LayerId>>,
},
ImaginateSetUseImg2Img {
path: Vec<LayerId>,
use_img2img: bool,
},
ImaginateSetRestoreFaces {
path: Vec<LayerId>,
restore_faces: bool,
},
ImaginateSetTiling {
path: Vec<LayerId>,
tiling: bool,
},
SetPivot {
layer_path: Vec<LayerId>,
pivot: (f64, f64),
@ -295,6 +222,14 @@ pub enum Operation {
transform: [f64; 6],
scope: [f64; 6],
},
TransformLayerScaleAroundPivot {
path: Vec<LayerId>,
scale_factor: (f64, f64),
},
SetLayerScaleAroundPivot {
path: Vec<LayerId>,
new_scale: (f64, f64),
},
SetLayerTransform {
path: Vec<LayerId>,
transform: [f64; 6],

View file

@ -22,6 +22,7 @@ rand_chacha = "0.3.1"
log = "0.4"
serde = { version = "1", features = ["derive", "rc"], optional = true }
glam = { version = "0.22" }
base64 = "0.13"
vulkano = {git = "https://github.com/GraphiteEditor/vulkano", branch = "fix_rust_gpu", optional = true}
bytemuck = {version = "1.8" }

View file

@ -1,8 +1,10 @@
use dyn_any::StaticType;
pub use dyn_any::StaticType;
use dyn_any::{DynAny, Upcast};
use dyn_clone::DynClone;
use glam::DVec2;
use std::sync::Arc;
pub use glam::DVec2;
pub use std::sync::Arc;
pub use crate::imaginate_input::{ImaginateMaskStartingFill, ImaginateSamplingMethod, ImaginateStatus};
/// A type that is known, allowing serialization (serde::Deserialize is not object safe)
#[derive(Clone, Debug, PartialEq)]
@ -15,13 +17,20 @@ pub enum TaggedValue {
F64(f64),
Bool(bool),
DVec2(DVec2),
OptionalDVec2(Option<DVec2>),
Image(graphene_core::raster::Image),
RcImage(Option<Arc<graphene_core::raster::Image>>),
Color(graphene_core::raster::color::Color),
Subpath(graphene_core::vector::subpath::Subpath),
RcSubpath(Arc<graphene_core::vector::subpath::Subpath>),
ImaginateSamplingMethod(ImaginateSamplingMethod),
ImaginateMaskStartingFill(ImaginateMaskStartingFill),
ImaginateStatus(ImaginateStatus),
LayerPath(Option<Vec<u64>>),
}
impl TaggedValue {
/// Converts to a Box<dyn DynAny> - this isn't very neat but I'm not sure of a better approach
pub fn to_value(self) -> Value {
match self {
TaggedValue::None => Box::new(()),
@ -31,10 +40,16 @@ impl TaggedValue {
TaggedValue::F64(x) => Box::new(x),
TaggedValue::Bool(x) => Box::new(x),
TaggedValue::DVec2(x) => Box::new(x),
TaggedValue::OptionalDVec2(x) => Box::new(x),
TaggedValue::Image(x) => Box::new(x),
TaggedValue::RcImage(x) => Box::new(x),
TaggedValue::Color(x) => Box::new(x),
TaggedValue::Subpath(x) => Box::new(x),
TaggedValue::RcSubpath(x) => Box::new(x),
TaggedValue::ImaginateSamplingMethod(x) => Box::new(x),
TaggedValue::ImaginateMaskStartingFill(x) => Box::new(x),
TaggedValue::ImaginateStatus(x) => Box::new(x),
TaggedValue::LayerPath(x) => Box::new(x),
}
}
}

View file

@ -0,0 +1,285 @@
#[cfg(feature = "serde")]
mod base64_serde {
use serde::{Deserialize, Deserializer, Serializer};
pub fn as_base64<S>(key: &std::sync::Arc<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&base64::encode(key.as_slice()))
}
pub fn from_base64<'a, D>(deserializer: D) -> Result<std::sync::Arc<Vec<u8>>, D::Error>
where
D: Deserializer<'a>,
{
use serde::de::Error;
String::deserialize(deserializer)
.and_then(|string| base64::decode(string).map_err(|err| Error::custom(err.to_string())))
.map(std::sync::Arc::new)
.map_err(serde::de::Error::custom)
}
}
use dyn_any::{DynAny, StaticType};
use glam::DVec2;
use std::fmt::Debug;
#[derive(Clone, PartialEq, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImaginateInput {
// User-configurable layer parameters
pub seed: u64,
pub samples: u32,
pub sampling_method: ImaginateSamplingMethod,
pub use_img2img: bool,
pub denoising_strength: f64,
pub mask_layer_ref: Option<Vec<u64>>,
pub mask_paint_mode: ImaginateMaskPaintMode,
pub mask_blur_px: u32,
pub mask_fill_content: ImaginateMaskStartingFill,
pub cfg_scale: f64,
pub prompt: String,
pub negative_prompt: String,
pub restore_faces: bool,
pub tiling: bool,
pub image_data: Option<ImaginateImageData>,
pub mime: String,
/// 0 is not started, 100 is complete.
pub percent_complete: f64,
// TODO: Have the browser dispose of this blob URL when this is dropped (like when the layer is deleted)
#[cfg_attr(feature = "serde", serde(skip))]
pub blob_url: Option<String>,
#[cfg_attr(feature = "serde", serde(skip))]
pub status: ImaginateStatus,
#[cfg_attr(feature = "serde", serde(skip))]
pub dimensions: DVec2,
}
#[derive(Default, Debug, Clone, Copy, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ImaginateStatus {
#[default]
Idle,
Beginning,
Uploading(f64),
Generating,
Terminating,
Terminated,
}
#[derive(Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImaginateImageData {
#[cfg_attr(feature = "serde", serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64"))]
pub image_data: std::sync::Arc<Vec<u8>>,
}
impl Debug for ImaginateImageData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("[image data...]")
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImaginateBaseImage {
pub mime: String,
#[serde(rename = "imageData")]
pub image_data: Vec<u8>,
pub size: DVec2,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImaginateMaskImage {
pub svg: String,
pub size: DVec2,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
pub enum ImaginateMaskPaintMode {
#[default]
Inpaint,
Outpaint,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny)]
pub enum ImaginateMaskStartingFill {
#[default]
Fill,
Original,
LatentNoise,
LatentNothing,
}
impl ImaginateMaskStartingFill {
pub fn list() -> [ImaginateMaskStartingFill; 4] {
[
ImaginateMaskStartingFill::Fill,
ImaginateMaskStartingFill::Original,
ImaginateMaskStartingFill::LatentNoise,
ImaginateMaskStartingFill::LatentNothing,
]
}
}
impl std::fmt::Display for ImaginateMaskStartingFill {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImaginateMaskStartingFill::Fill => write!(f, "Smeared Surroundings"),
ImaginateMaskStartingFill::Original => write!(f, "Original Base Image"),
ImaginateMaskStartingFill::LatentNoise => write!(f, "Randomness (Latent Noise)"),
ImaginateMaskStartingFill::LatentNothing => write!(f, "Neutral (Latent Nothing)"),
}
}
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ImaginateSamplingMethod {
#[default]
EulerA,
Euler,
LMS,
Heun,
DPM2,
DPM2A,
DPMPlusPlus2sA,
DPMPlusPlus2m,
DPMFast,
DPMAdaptive,
LMSKarras,
DPM2Karras,
DPM2AKarras,
DPMPlusPlus2sAKarras,
DPMPlusPlus2mKarras,
DDIM,
PLMS,
}
impl ImaginateSamplingMethod {
pub fn api_value(&self) -> &str {
match self {
ImaginateSamplingMethod::EulerA => "Euler a",
ImaginateSamplingMethod::Euler => "Euler",
ImaginateSamplingMethod::LMS => "LMS",
ImaginateSamplingMethod::Heun => "Heun",
ImaginateSamplingMethod::DPM2 => "DPM2",
ImaginateSamplingMethod::DPM2A => "DPM2 a",
ImaginateSamplingMethod::DPMPlusPlus2sA => "DPM++ 2S a",
ImaginateSamplingMethod::DPMPlusPlus2m => "DPM++ 2M",
ImaginateSamplingMethod::DPMFast => "DPM fast",
ImaginateSamplingMethod::DPMAdaptive => "DPM adaptive",
ImaginateSamplingMethod::LMSKarras => "LMS Karras",
ImaginateSamplingMethod::DPM2Karras => "DPM2 Karras",
ImaginateSamplingMethod::DPM2AKarras => "DPM2 a Karras",
ImaginateSamplingMethod::DPMPlusPlus2sAKarras => "DPM++ 2S a Karras",
ImaginateSamplingMethod::DPMPlusPlus2mKarras => "DPM++ 2M Karras",
ImaginateSamplingMethod::DDIM => "DDIM",
ImaginateSamplingMethod::PLMS => "PLMS",
}
}
pub fn list() -> [ImaginateSamplingMethod; 17] {
[
ImaginateSamplingMethod::EulerA,
ImaginateSamplingMethod::Euler,
ImaginateSamplingMethod::LMS,
ImaginateSamplingMethod::Heun,
ImaginateSamplingMethod::DPM2,
ImaginateSamplingMethod::DPM2A,
ImaginateSamplingMethod::DPMPlusPlus2sA,
ImaginateSamplingMethod::DPMPlusPlus2m,
ImaginateSamplingMethod::DPMFast,
ImaginateSamplingMethod::DPMAdaptive,
ImaginateSamplingMethod::LMSKarras,
ImaginateSamplingMethod::DPM2Karras,
ImaginateSamplingMethod::DPM2AKarras,
ImaginateSamplingMethod::DPMPlusPlus2sAKarras,
ImaginateSamplingMethod::DPMPlusPlus2mKarras,
ImaginateSamplingMethod::DDIM,
ImaginateSamplingMethod::PLMS,
]
}
}
impl std::fmt::Display for ImaginateSamplingMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImaginateSamplingMethod::EulerA => write!(f, "Euler A (Recommended)"),
ImaginateSamplingMethod::Euler => write!(f, "Euler"),
ImaginateSamplingMethod::LMS => write!(f, "LMS"),
ImaginateSamplingMethod::Heun => write!(f, "Heun"),
ImaginateSamplingMethod::DPM2 => write!(f, "DPM2"),
ImaginateSamplingMethod::DPM2A => write!(f, "DPM2 A"),
ImaginateSamplingMethod::DPMPlusPlus2sA => write!(f, "DPM++ 2S a"),
ImaginateSamplingMethod::DPMPlusPlus2m => write!(f, "DPM++ 2M"),
ImaginateSamplingMethod::DPMFast => write!(f, "DPM Fast"),
ImaginateSamplingMethod::DPMAdaptive => write!(f, "DPM Adaptive"),
ImaginateSamplingMethod::LMSKarras => write!(f, "LMS Karras"),
ImaginateSamplingMethod::DPM2Karras => write!(f, "DPM2 Karras"),
ImaginateSamplingMethod::DPM2AKarras => write!(f, "DPM2 A Karras"),
ImaginateSamplingMethod::DPMPlusPlus2sAKarras => write!(f, "DPM++ 2S a Karras"),
ImaginateSamplingMethod::DPMPlusPlus2mKarras => write!(f, "DPM++ 2M Karras"),
ImaginateSamplingMethod::DDIM => write!(f, "DDIM"),
ImaginateSamplingMethod::PLMS => write!(f, "PLMS"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImaginateGenerationParameters {
pub seed: u64,
pub samples: u32,
/// Use `ImaginateSamplingMethod::api_value()` to generate this string
#[cfg_attr(feature = "serde", serde(rename = "samplingMethod"))]
pub sampling_method: String,
#[cfg_attr(feature = "serde", serde(rename = "denoisingStrength"))]
pub image_creativity: Option<f64>,
#[cfg_attr(feature = "serde", serde(rename = "cfgScale"))]
pub text_guidance: f64,
#[cfg_attr(feature = "serde", serde(rename = "prompt"))]
pub text_prompt: String,
#[cfg_attr(feature = "serde", serde(rename = "negativePrompt"))]
pub negative_prompt: String,
pub resolution: (u32, u32),
#[cfg_attr(feature = "serde", serde(rename = "restoreFaces"))]
pub restore_faces: bool,
pub tiling: bool,
}
impl Default for ImaginateInput {
fn default() -> Self {
Self {
seed: 0,
samples: 30,
sampling_method: Default::default(),
use_img2img: false,
denoising_strength: 0.66,
mask_paint_mode: ImaginateMaskPaintMode::default(),
mask_layer_ref: None,
mask_blur_px: 4,
mask_fill_content: ImaginateMaskStartingFill::default(),
cfg_scale: 10.,
prompt: "".into(),
negative_prompt: "".into(),
restore_faces: false,
tiling: false,
image_data: None,
mime: "image/png".into(),
blob_url: None,
percent_complete: 0.,
status: Default::default(),
dimensions: Default::default(),
}
}
}

View file

@ -5,6 +5,7 @@ pub mod document;
pub mod proto;
pub mod executor;
pub mod imaginate_input;
#[cfg(feature = "gpu")]
pub mod gpu;

View file

@ -259,7 +259,6 @@ impl ProtoNetwork {
if temp_marks.contains(&node_id) {
panic!("Cycle detected");
}
info!("Visiting {node_id}");
if let Some(dependencies) = inwards_edges.get(&node_id) {
temp_marks.insert(node_id);
@ -273,7 +272,6 @@ impl ProtoNetwork {
assert!(self.nodes.iter().any(|(id, _)| *id == self.output), "Output id {} does not exist", self.output);
visit(self.output, &mut HashSet::new(), &mut sorted, &inwards_edges);
info!("Sorted order {sorted:?}");
sorted
}
@ -307,7 +305,6 @@ impl ProtoNetwork {
let order = self.topological_sort();
// Map of node ids to indexes (which become the node ids as they are inserted into the borrow stack)
let lookup: HashMap<_, _> = order.iter().enumerate().map(|(pos, id)| (*id, pos as NodeId)).collect();
info!("Order {order:?}");
self.nodes = order
.iter()
.enumerate()
@ -324,7 +321,7 @@ impl ProtoNetwork {
self.nodes.iter_mut().for_each(|(_, node)| {
node.map_ids(|id| *lookup.get(&id).expect("node not found in lookup table"));
});
self.inputs = self.inputs.iter().map(|id| *lookup.get(id).unwrap()).collect();
self.inputs = self.inputs.iter().filter_map(|id| lookup.get(id).copied()).collect();
self.output = *lookup.get(&self.output).unwrap();
}
}

View file

@ -9,9 +9,8 @@ license = "MIT OR Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
derive = ["graph-proc-macros"]
memoization = ["once_cell"]
default = ["derive", "memoization"]
default = ["memoization"]
gpu = ["graph-craft/gpu", "graphene-core/gpu"]
@ -19,7 +18,6 @@ gpu = ["graph-craft/gpu", "graphene-core/gpu"]
graphene-core = {path = "../gcore", features = ["async", "std" ], default-features = false}
borrow_stack = {path = "../borrow_stack"}
dyn-any = {path = "../../libraries/dyn-any", features = ["derive"]}
graph-proc-macros = {path = "../proc-macro", optional = true}
graph-craft = {path = "../graph-craft"}
bytemuck = {version = "1.8" }
tempfile = "3"
@ -37,6 +35,7 @@ kurbo = { git = "https://github.com/linebender/kurbo.git", features = [
"serde",
] }
glam = { version = "0.22", features = ["serde"] }
node-macro = { path="../node-macro" }
[dependencies.serde]
version = "1.0"

View file

@ -142,6 +142,10 @@ pub fn export_image_node<'n>() -> impl Node<(Image, &'n str), Output = Result<()
})
}
#[derive(Debug, Clone, Copy)]
pub struct GrayscaleNode;
#[node_macro::node_fn(GrayscaleNode)]
fn grayscale_image(mut image: Image) -> Image {
for pixel in &mut image.data {
let avg = (pixel.r() + pixel.g() + pixel.b()) / 3.;
@ -151,21 +155,9 @@ fn grayscale_image(mut image: Image) -> Image {
}
#[derive(Debug, Clone, Copy)]
pub struct GrayscaleNode;
impl Node<Image> for GrayscaleNode {
type Output = Image;
fn eval(self, image: Image) -> Image {
grayscale_image(image)
}
}
impl Node<Image> for &GrayscaleNode {
type Output = Image;
fn eval(self, image: Image) -> Image {
grayscale_image(image)
}
}
pub struct InvertRGBNode;
#[node_macro::node_fn(InvertRGBNode)]
fn invert_image(mut image: Image) -> Image {
for pixel in &mut image.data {
*pixel = Color::from_rgbaf32_unchecked(1. - pixel.r(), 1. - pixel.g(), 1. - pixel.b(), pixel.a());
@ -174,22 +166,15 @@ fn invert_image(mut image: Image) -> Image {
}
#[derive(Debug, Clone, Copy)]
pub struct InvertRGBNode;
impl Node<Image> for InvertRGBNode {
type Output = Image;
fn eval(self, image: Image) -> Image {
invert_image(image)
}
}
impl Node<Image> for &InvertRGBNode {
type Output = Image;
fn eval(self, image: Image) -> Image {
invert_image(image)
}
pub struct HueSaturationNode<Hue, Sat, Lit> {
hue_shift: Hue,
saturation_shift: Sat,
lightness_shift: Lit,
}
fn shift_image_hsl(mut image: Image, hue_shift: f32, saturation_shift: f32, lightness_shift: f32) -> Image {
#[node_macro::node_fn(HueSaturationNode)]
fn shift_image_hsl(mut image: Image, hue_shift: f64, saturation_shift: f64, lightness_shift: f64) -> Image {
let (hue_shift, saturation_shift, lightness_shift) = (hue_shift as f32, saturation_shift as f32, lightness_shift as f32);
for pixel in &mut image.data {
let [hue, saturation, lightness, alpha] = pixel.to_hsla();
*pixel = Color::from_hsla(
@ -203,108 +188,18 @@ fn shift_image_hsl(mut image: Image, hue_shift: f32, saturation_shift: f32, ligh
}
#[derive(Debug, Clone, Copy)]
pub struct HueSaturationNode<Hue, Sat, Lit>
where
Hue: Node<(), Output = f64>,
Sat: Node<(), Output = f64>,
Lit: Node<(), Output = f64>,
{
hue: Hue,
saturation: Sat,
lightness: Lit,
}
impl<Hue, Sat, Lit> Node<Image> for HueSaturationNode<Hue, Sat, Lit>
where
Hue: Node<(), Output = f64>,
Sat: Node<(), Output = f64>,
Lit: Node<(), Output = f64>,
{
type Output = Image;
fn eval(self, image: Image) -> Image {
shift_image_hsl(image, self.hue.eval(()) as f32, self.saturation.eval(()) as f32, self.lightness.eval(()) as f32)
}
}
impl<Hue, Sat, Lit> Node<Image> for &HueSaturationNode<Hue, Sat, Lit>
where
Hue: Node<(), Output = f64> + Copy,
Sat: Node<(), Output = f64> + Copy,
Lit: Node<(), Output = f64> + Copy,
{
type Output = Image;
fn eval(self, image: Image) -> Image {
shift_image_hsl(image, self.hue.eval(()) as f32, self.saturation.eval(()) as f32, self.lightness.eval(()) as f32)
}
}
impl<Hue, Sat, Lit> HueSaturationNode<Hue, Sat, Lit>
where
Hue: Node<(), Output = f64>,
Sat: Node<(), Output = f64>,
Lit: Node<(), Output = f64>,
{
pub fn new(hue: Hue, saturation: Sat, lightness: Lit) -> Self {
Self { hue, saturation, lightness }
}
}
// Copy pasta from https://stackoverflow.com/questions/2976274/adjust-bitmap-image-brightness-contrast-using-c
fn adjust_image_brightness_and_contrast(mut image: Image, brightness_shift: f32, contrast: f32) -> Image {
let factor = (259. * (contrast + 255.)) / (255. * (259. - contrast));
let channel = |channel: f32| ((factor * (channel * 255. + brightness_shift - 128.) + 128.) / 255.).clamp(0., 1.);
for pixel in &mut image.data {
*pixel = Color::from_rgbaf32_unchecked(channel(pixel.r()), channel(pixel.g()), channel(pixel.b()), pixel.a())
}
image
}
#[derive(Debug, Clone, Copy)]
pub struct BrightnessContrastNode<Brightness, Contrast>
where
Brightness: Node<(), Output = f64>,
Contrast: Node<(), Output = f64>,
{
pub struct BrightnessContrastNode<Brightness, Contrast> {
brightness: Brightness,
contrast: Contrast,
}
impl<Brightness, Contrast> Node<Image> for BrightnessContrastNode<Brightness, Contrast>
where
Brightness: Node<(), Output = f64>,
Contrast: Node<(), Output = f64>,
{
type Output = Image;
fn eval(self, image: Image) -> Image {
adjust_image_brightness_and_contrast(image, self.brightness.eval(()) as f32, self.contrast.eval(()) as f32)
}
}
// From https://stackoverflow.com/questions/2976274/adjust-bitmap-image-brightness-contrast-using-c
#[node_macro::node_fn(BrightnessContrastNode)]
fn adjust_image_brightness_and_contrast(mut image: Image, brightness: f64, contrast: f64) -> Image {
let (brightness, contrast) = (brightness as f32, contrast as f32);
let factor = (259. * (contrast + 255.)) / (255. * (259. - contrast));
let channel = |channel: f32| ((factor * (channel * 255. + brightness - 128.) + 128.) / 255.).clamp(0., 1.);
impl<Brightness, Contrast> Node<Image> for &BrightnessContrastNode<Brightness, Contrast>
where
Brightness: Node<(), Output = f64> + Copy,
Contrast: Node<(), Output = f64> + Copy,
{
type Output = Image;
fn eval(self, image: Image) -> Image {
adjust_image_brightness_and_contrast(image, self.brightness.eval(()) as f32, self.contrast.eval(()) as f32)
}
}
impl<Brightness, Contrast> BrightnessContrastNode<Brightness, Contrast>
where
Brightness: Node<(), Output = f64>,
Contrast: Node<(), Output = f64>,
{
pub fn new(brightness: Brightness, contrast: Contrast) -> Self {
Self { brightness, contrast }
}
}
// https://www.dfstudios.co.uk/articles/programming/image-programming-algorithms/image-processing-algorithms-part-6-gamma-correction/
fn image_gamma(mut image: Image, gamma: f32) -> Image {
let inverse_gamma = 1. / gamma;
let channel = |channel: f32| channel.powf(inverse_gamma);
for pixel in &mut image.data {
*pixel = Color::from_rgbaf32_unchecked(channel(pixel.r()), channel(pixel.g()), channel(pixel.b()), pixel.a())
}
@ -312,36 +207,44 @@ fn image_gamma(mut image: Image, gamma: f32) -> Image {
}
#[derive(Debug, Clone, Copy)]
pub struct GammaNode<N: Node<(), Output = f64>>(N);
impl<N: Node<(), Output = f64>> Node<Image> for GammaNode<N> {
type Output = Image;
fn eval(self, image: Image) -> Image {
image_gamma(image, self.0.eval(()) as f32)
}
}
impl<N: Node<(), Output = f64> + Copy> Node<Image> for &GammaNode<N> {
type Output = Image;
fn eval(self, image: Image) -> Image {
image_gamma(image, self.0.eval(()) as f32)
}
pub struct GammaNode<G> {
gamma: G,
}
impl<N: Node<(), Output = f64> + Copy> GammaNode<N> {
pub fn new(node: N) -> Self {
Self(node)
// https://www.dfstudios.co.uk/articles/programming/image-programming-algorithms/image-processing-algorithms-part-6-gamma-correction/
#[node_macro::node_fn(GammaNode)]
fn image_gamma(mut image: Image, gamma: f64) -> Image {
let inverse_gamma = 1. / gamma;
let channel = |channel: f32| channel.powf(inverse_gamma as f32);
for pixel in &mut image.data {
*pixel = Color::from_rgbaf32_unchecked(channel(pixel.r()), channel(pixel.g()), channel(pixel.b()), pixel.a())
}
image
}
fn image_opacity(mut image: Image, opacity_multiplier: f32) -> Image {
#[derive(Debug, Clone, Copy)]
pub struct OpacityNode<O> {
opacity_multiplier: O,
}
#[node_macro::node_fn(OpacityNode)]
fn image_opacity(mut image: Image, opacity_multiplier: f64) -> Image {
let opacity_multiplier = opacity_multiplier as f32;
for pixel in &mut image.data {
*pixel = Color::from_rgbaf32_unchecked(pixel.r(), pixel.g(), pixel.b(), pixel.a() * opacity_multiplier)
}
image
}
#[derive(Debug, Clone, Copy)]
pub struct PosterizeNode<P> {
posterize_value: P,
}
// Based on http://www.axiomx.com/posterize.htm
fn posterize(mut image: Image, posterize_value: f32) -> Image {
#[node_macro::node_fn(PosterizeNode)]
fn posterize(mut image: Image, posterize_value: f64) -> Image {
let posterize_value = posterize_value as f32;
let number_of_areas = posterize_value.recip();
let size_of_areas = (posterize_value - 1.).recip();
let channel = |channel: f32| (channel / number_of_areas).floor() * size_of_areas;
@ -351,9 +254,15 @@ fn posterize(mut image: Image, posterize_value: f32) -> Image {
image
}
#[derive(Debug, Clone, Copy)]
pub struct ExposureNode<E> {
exposure: E,
}
// Based on https://stackoverflow.com/questions/12166117/what-is-the-math-behind-exposure-adjustment-on-photoshop
fn exposure(mut image: Image, exposure: f32) -> Image {
let multiplier = 2f32.powf(exposure);
#[node_macro::node_fn(ExposureNode)]
fn exposure(mut image: Image, exposure: f64) -> Image {
let multiplier = 2f32.powf(exposure as f32);
let channel = |channel: f32| channel * multiplier;
for pixel in &mut image.data {
*pixel = Color::from_rgbaf32_unchecked(channel(pixel.r()), channel(pixel.g()), channel(pixel.b()), pixel.a())
@ -362,69 +271,15 @@ fn exposure(mut image: Image, exposure: f32) -> Image {
}
#[derive(Debug, Clone, Copy)]
pub struct PosterizeNode<N: Node<(), Output = f64>>(N);
impl<N: Node<(), Output = f64>> Node<Image> for PosterizeNode<N> {
type Output = Image;
fn eval(self, image: Image) -> Image {
posterize(image, self.0.eval(()) as f32)
}
}
impl<N: Node<(), Output = f64> + Copy> Node<Image> for &PosterizeNode<N> {
type Output = Image;
fn eval(self, image: Image) -> Image {
posterize(image, self.0.eval(()) as f32)
}
pub struct ImaginateNode<E> {
cached: E,
}
impl<N: Node<(), Output = f64> + Copy> PosterizeNode<N> {
pub fn new(node: N) -> Self {
Self(node)
}
}
#[derive(Debug, Clone, Copy)]
pub struct OpacityNode<N: Node<(), Output = f64>>(N);
impl<N: Node<(), Output = f64>> Node<Image> for OpacityNode<N> {
type Output = Image;
fn eval(self, image: Image) -> Image {
image_opacity(image, self.0.eval(()) as f32)
}
}
impl<N: Node<(), Output = f64> + Copy> Node<Image> for &OpacityNode<N> {
type Output = Image;
fn eval(self, image: Image) -> Image {
image_opacity(image, self.0.eval(()) as f32)
}
}
impl<N: Node<(), Output = f64> + Copy> OpacityNode<N> {
pub fn new(node: N) -> Self {
Self(node)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ExposureNode<N: Node<(), Output = f64>>(N);
impl<N: Node<(), Output = f64>> Node<Image> for ExposureNode<N> {
type Output = Image;
fn eval(self, image: Image) -> Image {
exposure(image, self.0.eval(()) as f32)
}
}
impl<N: Node<(), Output = f64> + Copy> Node<Image> for &ExposureNode<N> {
type Output = Image;
fn eval(self, image: Image) -> Image {
exposure(image, self.0.eval(()) as f32)
}
}
impl<N: Node<(), Output = f64> + Copy> ExposureNode<N> {
pub fn new(node: N) -> Self {
Self(node)
}
// Based on https://stackoverflow.com/questions/12166117/what-is-the-math-behind-exposure-adjustment-on-photoshop
#[node_macro::node_fn(ImaginateNode)]
fn imaginate(image: Image, cached: Option<std::sync::Arc<graphene_core::raster::Image>>) -> Image {
info!("Imaginating image with {} pixels", image.data.len());
cached.map(|mut x| std::sync::Arc::make_mut(&mut x).clone()).unwrap_or(image)
}
#[cfg(test)]

View file

@ -190,7 +190,6 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
})
}),
(NodeIdentifier::new("graphene_core::raster::BrightenColorNode", &[concrete!("&TypeErasedNode")]), |proto_node, stack| {
info!("proto node {:?}", proto_node);
stack.push_fn(|nodes| {
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("Brighten Color Node constructed with out brightness input node") };
let value_node = nodes.get(construction_nodes[0] as usize).unwrap();
@ -272,7 +271,6 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
(NodeIdentifier::new("graphene_std::raster::MapImageNode", &[]), |proto_node, stack| {
if let ConstructionArgs::Nodes(operation_node_id) = proto_node.construction_args {
stack.push_fn(move |nodes| {
info!("Map image Depending upon id {:?}", operation_node_id);
let operation_node = nodes.get(operation_node_id[0] as usize).unwrap();
let operation_node: DowncastBothNode<_, Color, Color> = DowncastBothNode::new(operation_node);
let map_node = DynAnyNode::new(graphene_std::raster::MapImageNode::new(operation_node));
@ -403,6 +401,21 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
}
})
}),
(NodeIdentifier::new("graphene_std::raster::ImaginateNode", &[concrete!("&TypeErasedNode")]), |proto_node, stack| {
stack.push_fn(move |nodes| {
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("ImaginateNode constructed without inputs") };
let value: DowncastBothNode<_, (), Option<std::sync::Arc<graphene_core::raster::Image>>> = DowncastBothNode::new(nodes.get(construction_nodes[15] as usize).unwrap());
let node = DynAnyNode::new(graphene_std::raster::ImaginateNode::new(value));
if let ProtoNodeInput::Node(node_id) = proto_node.input {
let pre_node = nodes.get(node_id as usize).unwrap();
(pre_node).then(node).into_type_erased()
} else {
node.into_type_erased()
}
})
}),
(NodeIdentifier::new("graphene_std::raster::ImageNode", &[concrete!("&str")]), |_proto_node, stack| {
stack.push_fn(|_nodes| {
let image = FnNode::new(|s: &str| graphene_std::raster::image_node::<&str>().eval(s).unwrap());

View file

@ -0,0 +1,20 @@
[package]
name = "node-macro"
publish = false
version = "0.0.0"
rust-version = "1.65.0"
authors = ["Graphite Authors <contact@graphite.rs>"]
edition = "2021"
readme = "../../README.md"
homepage = "https://graphite.rs"
repository = "https://github.com/GraphiteEditor/Graphite"
license = "Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true
[dependencies]
syn = { version = "1.0", features = ["full"] }
quote = "1.0"

View file

@ -0,0 +1,83 @@
use proc_macro::TokenStream;
use quote::{format_ident, ToTokens};
use syn::{parse_macro_input, FnArg, Ident, ItemFn, Pat, PatIdent, ReturnType};
#[proc_macro_attribute]
pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream {
let node_name = parse_macro_input!(attr as Ident);
let function = parse_macro_input!(item as ItemFn);
let function_name = &function.sig.ident;
let mut function_inputs = function.sig.inputs.iter().filter_map(|arg| if let FnArg::Typed(typed_arg) = arg { Some(typed_arg) } else { None });
// Extract primary input as first argument
let primary_input = function_inputs.next().expect("Primary input required - set to `()` if not needed.");
let Pat::Ident(PatIdent{ident: primary_input_ident,..} ) =&*primary_input.pat else{
panic!("Expected ident as primary input.");
};
let primary_input_ty = &primary_input.ty;
// Extract secondary inputs as all other arguments
let secondary_inputs = function_inputs.collect::<Vec<_>>();
let secondary_idents = secondary_inputs
.iter()
.map(|input| {
let Pat::Ident(PatIdent { ident: primary_input_ident,.. }) = &*input.pat else { panic!("Expected ident for secondary input."); };
primary_input_ident
})
.collect::<Vec<_>>();
// Extract the output type of the entire node - `()` by default
let output = if let ReturnType::Type(_, ty) = &function.sig.output {
ty.to_token_stream()
} else {
quote::quote!(())
};
// Generics are simply `S0` through to `Sn-1` where n is the number of secondary inputs
let generics = (0..secondary_inputs.len()).map(|x| format_ident!("S{x}")).collect::<Vec<_>>();
// Bindings for all of the above generics to a node with an input of `()` and an output of the type in the function
let where_clause = secondary_inputs
.iter()
.zip(&generics)
.map(|(ty, name)| {
let ty = &ty.ty;
quote::quote!(#name: Node<(), Output = #ty>)
})
.collect::<Vec<_>>();
quote::quote! {
#function
impl <#(#generics),*> Node<#primary_input_ty> for #node_name<#(#generics),*>
where
#(#where_clause),* {
type Output = #output;
fn eval(self, #primary_input_ident: #primary_input_ty) -> #output{
#function_name(#primary_input_ident #(, self.#secondary_idents.eval(()))*)
}
}
impl <#(#generics),*> Node<#primary_input_ty> for &#node_name<#(#generics),*>
where
#(#where_clause + Copy),* {
type Output = #output;
fn eval(self, #primary_input_ident: #primary_input_ty) -> #output{
#function_name(#primary_input_ident #(, self.#secondary_idents.eval(()))*)
}
}
impl <#(#generics),*> #node_name<#(#generics),*>
where
#(#where_clause + Copy),* {
pub fn new(#(#secondary_idents: #generics),*) -> Self{
Self{
#(#secondary_idents),*
}
}
}
}
.into()
}

View file

@ -1,18 +0,0 @@
[package]
name = "graph-proc-macros"
version = "0.1.0"
authors = ["Graphite Authors <contact@graphite.rs>"]
edition = "2021"
publish = false
license = "MIT OR Apache-2.0"
[lib]
path = "src/lib.rs"
proc-macro = true
[dependencies]
proc-macro2 = "1.0"
proc_macro_roids = "0.7"
syn = { version = "1.0", features = ["full"] }
quote = "1.0"
graphene-core = {path = "../gcore"}

View file

@ -1,98 +0,0 @@
use proc_macro::TokenStream;
use proc_macro_roids::*;
use quote::{quote, ToTokens};
use syn::punctuated::Punctuated;
use syn::{parse_macro_input, FnArg, ItemFn, Pat, Type};
fn extract_type(a: FnArg) -> Type {
match a {
FnArg::Typed(p) => *p.ty, // notice `ty` instead of `pat`
_ => panic!("Not supported on types with `self`!"),
}
}
fn extract_arg_types(fn_args: Punctuated<FnArg, syn::token::Comma>) -> Vec<Type> {
fn_args.into_iter().map(extract_type).collect::<Vec<_>>()
}
fn extract_arg_idents(fn_args: Punctuated<FnArg, syn::token::Comma>) -> Vec<Pat> {
fn_args.into_iter().map(extract_arg_pat).collect::<Vec<_>>()
}
fn extract_arg_pat(a: FnArg) -> Pat {
match a {
FnArg::Typed(p) => *p.pat,
_ => panic!("Not supported on types with `self`!"),
}
}
#[proc_macro_attribute] // 2
pub fn to_node(_attr: TokenStream, item: TokenStream) -> TokenStream {
let string = item.to_string();
let item2 = item;
let parsed = parse_macro_input!(item2 as ItemFn); // 3
//item.extend(generate_to_string(parsed, string)); // 4
//item
generate_to_string(parsed, string)
}
fn generate_to_string(parsed: ItemFn, string: String) -> TokenStream {
let whole_function = parsed.clone();
//let fn_body = parsed.block; // function body
let sig = parsed.sig; // function signature
//let vis = parsed.vis; // visibility, pub or not
let generics = sig.generics;
let fn_args = sig.inputs; // comma separated args
let fn_return_type = sig.output; // return type
let fn_name = sig.ident; // function name/identifier
let idents = extract_arg_idents(fn_args.clone());
let types = extract_arg_types(fn_args);
let types = types.iter().map(|t| t.to_token_stream()).collect::<Vec<_>>();
let idents = idents.iter().map(|t| t.to_token_stream()).collect::<Vec<_>>();
let _const_idents = idents
.iter()
.map(|t| {
let name = t.to_string().to_uppercase();
quote! {#name}
})
.collect::<Vec<_>>();
let node_fn_name = fn_name.append("_node");
let struct_name = fn_name.append("_input");
let return_type_string = fn_return_type.to_token_stream().to_string().replace("->", "");
let arg_type_string = types.iter().map(|t| t.to_string()).collect::<Vec<_>>().join(", ");
let error = format!("called {} with the wrong type", fn_name);
let x = quote! {
//#whole_function
mod #fn_name {
#[derive(Copy, Clone)]
type F32Node<'n> = &'n (dyn Node<'n, (), Output = &'n (dyn Any + 'static)> + 'n);
struct #struct_name {
#(#idents: #types,)*
}
impl Node for #struct_name {
}
}
fn #node_fn_name #generics() -> Node<'static> {
Node { func: Box::new(move |x| {
let args = x.downcast::<(#(#types,)*)>().expect(#error);
let (#(#idents,)*) = *args;
#whole_function
Box::new(#fn_name(#(#idents,)*))
}),
code: #string.to_string(),
return_type: #return_type_string.trim().to_string(),
args: format!("({})",#arg_type_string.trim()),
position: (0., 0.),
}
}
};
//panic!("{}\n{:?}", x.to_string(), x);
x.into()
}