Fix Imaginate by porting its JS roundtrip code to graph-based async execution in Rust (#1250)

* Create asynchronous rust imaginate node

* Make a first imaginate request via rust

* Implement parsing of imaginate API result image

* Stop refresh timer from affecting imaginate progress requests

* Add cargo-about clarification for rustls-webpki

* Delete imaginate.ts and all uses of its functions

* Add imaginate img2img feature

* Fix imaginate random seed button

* Fix imaginate ui inferring non-custom resolutions

* Fix the imaginate progress indicator

* Remove ImaginatePreferences from being compiled into node graph

* Regenerate imaginate only when hitting button

* Add ability to terminate imaginate requests

* Add imaginate server check feature

* Do not compile wasm_bindgen bindings in graphite_editor for tests

* Address some review suggestions

- move wasm futures dependency in editor to the future-executor crate
- guard wasm-bindgen in editor behind a `wasm` feature flag
- dont make seed number input a slider
- remove poll_server_check from process_message function beginning
- guard wasm related code behind `cfg(target_arch = "wasm32")` instead
  of `cfg(test)`
- Call the imaginate idle states "Ready" and "Done" instead of "Nothing
  to do"
- Call the imaginate uploading state "Uploading Image" instead of
  "Uploading Input Image"
- Remove the EvalSyncNode

* Fix imaginate host name being restored between graphite instances

also change the progress status texts a bit.

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
nat-rix
2023-06-09 09:03:15 +02:00
committed by Keavon Chambers
parent a1c70c4d90
commit f76b850b9c
35 changed files with 1500 additions and 1326 deletions

View File

@@ -9,7 +9,6 @@ use crate::messages::tool::utility_types::HintData;
use document_legacy::LayerId;
use graph_craft::document::NodeId;
use graph_craft::imaginate_input::*;
use graphene_core::raster::color::Color;
use graphene_core::text::Font;
@@ -75,40 +74,6 @@ pub enum FrontendMessage {
#[serde(rename = "isDefault")]
is_default: bool,
},
TriggerImaginateCheckServerStatus {
hostname: String,
},
TriggerImaginateGenerate {
parameters: Box<ImaginateGenerationParameters>,
#[serde(rename = "baseImage")]
base_image: Option<Box<ImaginateBaseImage>>,
#[serde(rename = "maskImage")]
mask_image: Option<Box<ImaginateMaskImage>>,
#[serde(rename = "maskPaintMode")]
mask_paint_mode: ImaginateMaskPaintMode,
#[serde(rename = "maskBlurPx")]
mask_blur_px: u32,
#[serde(rename = "maskFillContent")]
imaginate_mask_starting_fill: ImaginateMaskStartingFill,
hostname: String,
#[serde(rename = "refreshFrequency")]
refresh_frequency: f64,
#[serde(rename = "documentId")]
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,
TriggerIndexedDbRemoveDocument {
#[serde(rename = "documentId")]

View File

@@ -93,8 +93,6 @@ pub enum DocumentMessage {
GroupSelectedLayers,
ImaginateClear {
layer_path: Vec<LayerId>,
node_id: NodeId,
cached_index: usize,
},
ImaginateGenerate {
layer_path: Vec<LayerId>,
@@ -105,10 +103,6 @@ pub enum DocumentMessage {
imaginate_node: Vec<NodeId>,
then_generate: bool,
},
ImaginateTerminate {
layer_path: Vec<LayerId>,
node_path: Vec<NodeId>,
},
InputFrameRasterizeRegionBelowLayer {
layer_path: Vec<LayerId>,
},

View File

@@ -465,15 +465,7 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
replacement_selected_layers: vec![new_folder_path],
});
}
ImaginateClear {
layer_path,
node_id,
cached_index: input_index,
} => {
let value = graph_craft::document::value::TaggedValue::RcImage(None);
responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value });
responses.add(InputFrameRasterizeRegionBelowLayer { layer_path });
}
ImaginateClear { layer_path } => responses.add(InputFrameRasterizeRegionBelowLayer { layer_path }),
ImaginateGenerate { layer_path, imaginate_node } => {
if let Some(message) = self.rasterize_region_below_layer(document_id, layer_path, preferences, persistent_data, Some(imaginate_node)) {
responses.add(message);
@@ -484,12 +476,16 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
imaginate_node,
then_generate,
} => {
// Generate a random seed. We only want values between -2^53 and 2^53, because integer values
// outside of this range can get rounded in f64
let random_bits = generate_uuid();
let random_value = ((random_bits >> 11) as f64).copysign(f64::from_bits(random_bits & (1 << 63)));
// Set a random seed input
responses.add(NodeGraphMessage::SetInputValue {
node_id: *imaginate_node.last().unwrap(),
// Needs to match the index of the seed parameter in `pub const IMAGINATE_NODE: DocumentNodeType` in `document_node_type.rs`
input_index: 1,
value: graph_craft::document::value::TaggedValue::F64((generate_uuid() >> 1) as f64),
input_index: 3,
value: graph_craft::document::value::TaggedValue::F64(random_value),
});
// Generate the image
@@ -497,14 +493,6 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
responses.add(DocumentMessage::ImaginateGenerate { layer_path, imaginate_node });
}
}
ImaginateTerminate { layer_path, node_path } => {
responses.add(FrontendMessage::TriggerImaginateTerminate {
document_id,
layer_path,
node_path,
hostname: preferences.imaginate_server_hostname.clone(),
});
}
InputFrameRasterizeRegionBelowLayer { layer_path } => {
if layer_path.is_empty() {
responses.add(NodeGraphMessage::RunDocumentGraph);
@@ -996,7 +984,7 @@ impl DocumentMessageHandler {
// Check if we use the "Input Frame" node.
// TODO: Remove once rasterization is moved into a node.
let input_frame_node_id = node_network.nodes.iter().find(|(_, node)| node.name == "Input Frame").map(|(&id, _)| id);
let input_frame_connected_to_graph_output = input_frame_node_id.map_or(false, |target_node_id| node_network.connected_to_output(target_node_id, imaginate_node_path.is_none()));
let input_frame_connected_to_graph_output = input_frame_node_id.map_or(false, |target_node_id| node_network.connected_to_output(target_node_id));
// If the Input Frame node is connected upstream, rasterize the artwork below this layer by calling into JS
let response = if input_frame_connected_to_graph_output {

View File

@@ -459,7 +459,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &NodeGraphExecutor, u64)>
let input = NodeInput::node(output_node, output_node_connector_index);
responses.add(NodeGraphMessage::SetNodeInput { node_id, input_index, input });
let should_rerender = network.connected_to_output(node_id, true);
let should_rerender = network.connected_to_output(node_id);
responses.add(NodeGraphMessage::SendGraph { should_rerender });
}
NodeGraphMessage::Copy => {
@@ -517,7 +517,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &NodeGraphExecutor, u64)>
if let Some(network) = self.get_active_network(document) {
// Only generate node graph if one of the selected nodes is connected to the output
if self.selected_nodes.iter().any(|&node_id| network.connected_to_output(node_id, true)) {
if self.selected_nodes.iter().any(|&node_id| network.connected_to_output(node_id)) {
if let Some(layer_path) = self.layer_path.clone() {
responses.add(DocumentMessage::InputFrameRasterizeRegionBelowLayer { layer_path });
}
@@ -549,7 +549,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &NodeGraphExecutor, u64)>
}
responses.add(NodeGraphMessage::SetNodeInput { node_id, input_index, input });
let should_rerender = network.connected_to_output(node_id, true);
let should_rerender = network.connected_to_output(node_id);
responses.add(NodeGraphMessage::SendGraph { should_rerender });
}
NodeGraphMessage::DoubleClickNode { node } => {
@@ -626,7 +626,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &NodeGraphExecutor, u64)>
}
responses.add(NodeGraphMessage::SetNodeInput { node_id, input_index, input });
let should_rerender = network.connected_to_output(node_id, true);
let should_rerender = network.connected_to_output(node_id);
responses.add(NodeGraphMessage::SendGraph { should_rerender });
responses.add(PropertiesPanelMessage::ResendActiveProperties);
}
@@ -743,7 +743,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &NodeGraphExecutor, u64)>
let input = NodeInput::Value { tagged_value: value, exposed: false };
responses.add(NodeGraphMessage::SetNodeInput { node_id, input_index, input });
responses.add(PropertiesPanelMessage::ResendActiveProperties);
if (node.name != "Imaginate" || input_index == 0) && network.connected_to_output(node_id, true) {
if (node.name != "Imaginate" || input_index == 0) && network.connected_to_output(node_id) {
if let Some(layer_path) = self.layer_path.clone() {
responses.add(DocumentMessage::InputFrameRasterizeRegionBelowLayer { layer_path });
} else {
@@ -780,7 +780,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &NodeGraphExecutor, u64)>
node.inputs.extend(((node.inputs.len() - 1)..input_index).map(|_| NodeInput::Network(generic!(T))));
}
node.inputs[input_index] = NodeInput::Value { tagged_value: value, exposed: false };
if network.connected_to_output(*node_id, true) {
if network.connected_to_output(*node_id) {
responses.add(DocumentMessage::InputFrameRasterizeRegionBelowLayer { layer_path });
}
}
@@ -854,7 +854,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &NodeGraphExecutor, u64)>
Self::send_graph(network, executor, &self.layer_path, responses);
// Only generate node graph if one of the selected nodes is connected to the output
if self.selected_nodes.iter().any(|&node_id| network.connected_to_output(node_id, true)) {
if self.selected_nodes.iter().any(|&node_id| network.connected_to_output(node_id)) {
if let Some(layer_path) = self.layer_path.clone() {
responses.add(DocumentMessage::InputFrameRasterizeRegionBelowLayer { layer_path });
}

View File

@@ -1673,12 +1673,63 @@ fn static_nodes() -> Vec<DocumentNodeType> {
pub static IMAGINATE_NODE: Lazy<DocumentNodeType> = Lazy::new(|| DocumentNodeType {
name: "Imaginate",
category: "Image Synthesis",
identifier: NodeImplementation::proto("graphene_std::raster::ImaginateNode<_>"),
identifier: NodeImplementation::DocumentNode(NodeNetwork {
inputs: vec![0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
outputs: vec![NodeOutput::new(1, 0)],
nodes: [
(
0,
DocumentNode {
name: "Frame Monitor".into(),
inputs: vec![NodeInput::Network(concrete!(ImageFrame<Color>))],
implementation: DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode<_>"),
..Default::default()
},
),
(
1,
DocumentNode {
name: "Imaginate".into(),
inputs: vec![
NodeInput::node(0, 0),
NodeInput::Network(concrete!(WasmEditorApi)),
NodeInput::Network(concrete!(ImaginateController)),
NodeInput::Network(concrete!(f64)),
NodeInput::Network(concrete!(Option<DVec2>)),
NodeInput::Network(concrete!(u32)),
NodeInput::Network(concrete!(ImaginateSamplingMethod)),
NodeInput::Network(concrete!(f64)),
NodeInput::Network(concrete!(String)),
NodeInput::Network(concrete!(String)),
NodeInput::Network(concrete!(bool)),
NodeInput::Network(concrete!(f64)),
NodeInput::Network(concrete!(Option<Vec<u64>>)),
NodeInput::Network(concrete!(bool)),
NodeInput::Network(concrete!(f64)),
NodeInput::Network(concrete!(ImaginateMaskStartingFill)),
NodeInput::Network(concrete!(bool)),
NodeInput::Network(concrete!(bool)),
NodeInput::Network(concrete!(ImaginateCache)),
],
implementation: DocumentNodeImplementation::proto("graphene_std::raster::ImaginateNode<_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _>"),
..Default::default()
},
),
]
.into(),
..Default::default()
}),
inputs: vec![
DocumentInputType::value("Input Image", TaggedValue::ImageFrame(ImageFrame::empty()), true),
DocumentInputType {
name: "Editor Api",
data_type: FrontendGraphDataType::General,
default: NodeInput::Network(concrete!(WasmEditorApi)),
},
DocumentInputType::value("Controller", TaggedValue::ImaginateController(Default::default()), false),
DocumentInputType::value("Seed", TaggedValue::F64(0.), false), // Remember to keep index used in `ImaginateRandom` updated with this entry's index
DocumentInputType::value("Resolution", TaggedValue::OptionalDVec2(None), false),
DocumentInputType::value("Samples", TaggedValue::F64(30.), false),
DocumentInputType::value("Samples", TaggedValue::U32(30), false),
DocumentInputType::value("Sampling Method", TaggedValue::ImaginateSamplingMethod(ImaginateSamplingMethod::EulerA), false),
DocumentInputType::value("Prompt Guidance", TaggedValue::F64(7.5), false),
DocumentInputType::value("Prompt", TaggedValue::String(String::new()), false),
@@ -1691,10 +1742,7 @@ pub static IMAGINATE_NODE: Lazy<DocumentNodeType> = Lazy::new(|| DocumentNodeTyp
DocumentInputType::value("Mask Starting Fill", TaggedValue::ImaginateMaskStartingFill(ImaginateMaskStartingFill::Fill), false),
DocumentInputType::value("Improve Faces", TaggedValue::Bool(false), false),
DocumentInputType::value("Tiling", TaggedValue::Bool(false), false),
// Non-user status (is document input the right way to do this?)
DocumentInputType::value("Cached Data", TaggedValue::RcImage(None), false),
DocumentInputType::value("Percent Complete", TaggedValue::F64(0.), false),
DocumentInputType::value("Status", TaggedValue::ImaginateStatus(ImaginateStatus::Idle), false),
DocumentInputType::value("Cache", TaggedValue::ImaginateCache(Default::default()), false),
],
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
properties: node_properties::imaginate_properties,

View File

@@ -5,9 +5,11 @@ use super::FrontendGraphDataType;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
use document_legacy::{layers::layer_info::LayerDataTypeDiscriminant, Operation};
use graph_craft::concrete;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, NodeId, NodeInput};
use graph_craft::imaginate_input::{ImaginateMaskStartingFill, ImaginateSamplingMethod, ImaginateServerStatus, ImaginateStatus};
use graphene_core::raster::{BlendMode, Color, ImageFrame, LuminanceCalculation, RedGreenBlue, RelativeAbsolute, SelectiveColorChoice};
use graphene_core::text::Font;
use graphene_core::vector::style::{FillType, GradientType, LineCap, LineJoin};
@@ -980,12 +982,17 @@ pub fn node_section_font(document_node: &DocumentNode, node_id: NodeId, _context
result
}
pub fn imaginate_properties(_document_node: &DocumentNode, _node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
/*
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 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 resolve_input = |name: &str| {
super::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");
@@ -1001,22 +1008,12 @@ pub fn imaginate_properties(_document_node: &DocumentNode, _node_id: NodeId, _co
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 controller = &document_node.inputs[resolve_input("Controller")];
let server_status = {
let status = match &context.persistent_data.imaginate_server_status {
ImaginateServerStatus::Unknown => {
context.responses.add(PortfolioMessage::ImaginateCheckServerStatus);
"Checking..."
}
ImaginateServerStatus::Checking => "Checking...",
ImaginateServerStatus::Unavailable => "Unavailable",
ImaginateServerStatus::Connected => "Connected",
};
let server_status = context.persistent_data.imaginate.server_status();
let status_text = server_status.to_text();
let mut widgets = vec![
WidgetHolder::text_widget("Server"),
WidgetHolder::unrelated_separator(),
@@ -1025,14 +1022,14 @@ pub fn imaginate_properties(_document_node: &DocumentNode, _node_id: NodeId, _co
.on_update(|_| DialogMessage::RequestPreferencesDialog.into())
.widget_holder(),
WidgetHolder::unrelated_separator(),
WidgetHolder::bold_text(status),
WidgetHolder::bold_text(status_text),
WidgetHolder::related_separator(),
IconButton::new("Reload", 24)
.tooltip("Refresh connection status")
.on_update(|_| PortfolioMessage::ImaginateCheckServerStatus.into())
.widget_holder(),
];
if context.persistent_data.imaginate_server_status == ImaginateServerStatus::Unavailable {
if let ImaginateServerStatus::Unavailable | ImaginateServerStatus::Failed(_) = server_status {
widgets.extend([
WidgetHolder::unrelated_separator(),
TextButton::new("Server Help")
@@ -1049,15 +1046,11 @@ pub fn imaginate_properties(_document_node: &DocumentNode, _node_id: NodeId, _co
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, received {:?}, index: {}", cached_value, cached_index)
};
let &NodeInput::Value {tagged_value: TaggedValue::F64( percent_complete),..} = complete_value else {
panic!("Invalid percent complete input")
let &NodeInput::Value {tagged_value: TaggedValue::ImaginateController(ref controller),..} = controller else {
panic!("Invalid output status input")
};
let imaginate_status = controller.get_status();
let use_base_image = if let &NodeInput::Value {
tagged_value: TaggedValue::Bool(use_base_image),
..
@@ -1071,23 +1064,7 @@ pub fn imaginate_properties(_document_node: &DocumentNode, _node_id: NodeId, _co
let transform_not_connected = false;
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 Input Image: {percent:.0}%"),
ImaginateStatus::Generating => format!("Generating: {percent_complete:.0}%"),
ImaginateStatus::Terminating => "Terminating...".into(),
ImaginateStatus::Terminated => format!("{percent_complete:.0}% (Terminated)"),
};
let status = imaginate_status.to_text();
let widgets = vec![
WidgetHolder::text_widget("Progress"),
WidgetHolder::unrelated_separator(),
@@ -1095,38 +1072,38 @@ pub fn imaginate_properties(_document_node: &DocumentNode, _node_id: NodeId, _co
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),
WidgetHolder::bold_text(status.as_ref()),
];
LayoutGroup::Row { widgets }.with_tooltip("When generating, the percentage represents how many sampling steps have so far been processed out of the target number")
LayoutGroup::Row { widgets }.with_tooltip(match imaginate_status {
ImaginateStatus::Failed(_) => status.as_ref(),
_ => "When generating, the percentage represents how many sampling steps have so far been processed out of the target number",
})
};
let image_controls = {
let image_controls: _ = {
let mut widgets = vec![WidgetHolder::text_widget("Image"), WidgetHolder::unrelated_separator()];
let assist_separators = vec![
let assist_separators = [
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(_) => {
match &imaginate_status {
ImaginateStatus::Beginning | ImaginateStatus::Uploading => {
widgets.extend_from_slice(&assist_separators);
widgets.push(TextButton::new("Beginning...").tooltip("Sending image generation request to the server").disabled(true).widget_holder());
}
ImaginateStatus::Generating => {
ImaginateStatus::Generating(_) => {
widgets.extend_from_slice(&assist_separators);
widgets.push(
TextButton::new("Terminate")
.tooltip("Cancel the in-progress image generation and keep the latest progress")
.on_update({
let imaginate_node = imaginate_node.clone();
let controller = controller.clone();
move |_| {
DocumentMessage::ImaginateTerminate {
layer_path: layer_path.clone(),
node_path: imaginate_node.clone(),
}
.into()
controller.request_termination();
Message::NoOp
}
})
.widget_holder(),
@@ -1141,13 +1118,15 @@ pub fn imaginate_properties(_document_node: &DocumentNode, _node_id: NodeId, _co
.widget_holder(),
);
}
ImaginateStatus::Idle | ImaginateStatus::Terminated => widgets.extend_from_slice(&[
ImaginateStatus::Ready | ImaginateStatus::ReadyDone | ImaginateStatus::Terminated | ImaginateStatus::Failed(_) => widgets.extend_from_slice(&[
IconButton::new("Random", 24)
.tooltip("Generate with a new random seed")
.on_update({
let imaginate_node = imaginate_node.clone();
let layer_path = context.layer_path.to_vec();
let controller = controller.clone();
move |_| {
controller.trigger_regenerate();
DocumentMessage::ImaginateRandom {
layer_path: layer_path.clone(),
imaginate_node: imaginate_node.clone(),
@@ -1163,7 +1142,9 @@ pub fn imaginate_properties(_document_node: &DocumentNode, _node_id: NodeId, _co
.on_update({
let imaginate_node = imaginate_node.clone();
let layer_path = context.layer_path.to_vec();
let controller = controller.clone();
move |_| {
controller.trigger_regenerate();
DocumentMessage::ImaginateGenerate {
layer_path: layer_path.clone(),
imaginate_node: imaginate_node.clone(),
@@ -1175,16 +1156,13 @@ pub fn imaginate_properties(_document_node: &DocumentNode, _node_id: NodeId, _co
WidgetHolder::related_separator(),
TextButton::new("Clear")
.tooltip("Remove generated image from the layer frame")
.disabled(cached_data.is_none())
.disabled(!matches!(imaginate_status, ImaginateStatus::ReadyDone))
.on_update({
let layer_path = context.layer_path.to_vec();
let controller = controller.clone();
move |_| {
DocumentMessage::ImaginateClear {
node_id,
layer_path: layer_path.clone(),
cached_index,
}
.into()
controller.set_status(ImaginateStatus::Ready);
DocumentMessage::ImaginateClear { layer_path: layer_path.clone() }.into()
}
})
.widget_holder(),
@@ -1221,9 +1199,11 @@ pub fn imaginate_properties(_document_node: &DocumentNode, _node_id: NodeId, _co
.widget_holder(),
WidgetHolder::unrelated_separator(),
NumberInput::new(Some(seed))
.min(0.)
.int()
.min(-((1u64 << f64::MANTISSA_DIGITS) as f64))
.max((1u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(update_value(move |input: &NumberInput| TaggedValue::F64(input.value.unwrap()), node_id, seed_index))
.mode(NumberInputMode::Increment)
.widget_holder(),
])
}
@@ -1231,23 +1211,19 @@ pub fn imaginate_properties(_document_node: &DocumentNode, _node_id: NodeId, _co
LayoutGroup::Row { widgets }.with_tooltip("Seed determines the random outcome, enabling limitless unique variations")
};
// Create the input to the graph using an empty image
let editor_api = std::borrow::Cow::Owned(EditorApi {
image_frame: None,
font_cache: Some(&context.persistent_data.font_cache),
});
// Compute the transform input to the image frame
let image_frame: ImageFrame<Color> = context.executor.compute_input(context.network, &imaginate_node, 0, editor_api).unwrap_or_default();
let transform = image_frame.transform;
let transform = context
.executor
.introspect_node_in_network(context.network, &imaginate_node, |network| network.inputs.first().copied(), |frame: &ImageFrame<Color>| frame.transform)
.unwrap_or_default();
let resolution = {
use document_legacy::document::pick_safe_imaginate_resolution;
use graphene_std::imaginate::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))
DVec2::new(x as f64, y as f64)
};
if let &NodeInput::Value {
@@ -1256,14 +1232,7 @@ pub fn imaginate_properties(_document_node: &DocumentNode, _node_id: NodeId, _co
} = &document_node.inputs[resolution_index]
{
let dimensions_is_auto = vec2.is_none();
let vec2 = vec2.unwrap_or_else(|| {
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 vec2 = vec2.unwrap_or_else(|| round([transform.matrix2.x_axis, transform.matrix2.y_axis].map(DVec2::length).into()));
let layer_path = context.layer_path.to_vec();
widgets.extend_from_slice(&[
@@ -1308,7 +1277,7 @@ pub fn imaginate_properties(_document_node: &DocumentNode, _node_id: NodeId, _co
.unit(" px")
.disabled(dimensions_is_auto && !transform_not_connected)
.on_update(update_value(
move |number_input: &NumberInput| TaggedValue::OptionalDVec2(round(DVec2::new(number_input.value.unwrap(), vec2.y))),
move |number_input: &NumberInput| TaggedValue::OptionalDVec2(Some(round(DVec2::new(number_input.value.unwrap(), vec2.y)))),
node_id,
resolution_index,
))
@@ -1321,7 +1290,7 @@ pub fn imaginate_properties(_document_node: &DocumentNode, _node_id: NodeId, _co
.unit(" px")
.disabled(dimensions_is_auto && !transform_not_connected)
.on_update(update_value(
move |number_input: &NumberInput| TaggedValue::OptionalDVec2(round(DVec2::new(vec2.x, number_input.value.unwrap()))),
move |number_input: &NumberInput| TaggedValue::OptionalDVec2(Some(round(DVec2::new(vec2.x, number_input.value.unwrap())))),
node_id,
resolution_index,
))
@@ -1538,8 +1507,6 @@ pub fn imaginate_properties(_document_node: &DocumentNode, _node_id: NodeId, _co
layout.extend_from_slice(&[improve_faces, tiling]);
layout
*/
todo!()
}
fn unknown_node_properties(document_node: &DocumentNode) -> Vec<LayoutGroup> {

View File

@@ -1,10 +1,8 @@
use super::utility_types::ImaginateServerStatus;
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::prelude::*;
use document_legacy::LayerId;
use graph_craft::document::NodeId;
use graph_craft::imaginate_input::ImaginateStatus;
use graphene_core::text::Font;
use serde::{Deserialize, Serialize};
@@ -57,24 +55,9 @@ pub enum PortfolioMessage {
is_default: bool,
},
ImaginateCheckServerStatus,
ImaginateSetGeneratingStatus {
document_id: u64,
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,
},
ImaginatePollServerStatus,
ImaginatePreferences,
ImaginateServerHostname,
Import,
LoadDocumentResources {
document_id: u64,

View File

@@ -7,9 +7,7 @@ use crate::messages::dialog::simple_dialogs;
use crate::messages::frontend::utility_types::FrontendDocumentDetails;
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::utility_types::ImaginateServerStatus;
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::{HintData, HintGroup};
use crate::node_graph_executor::NodeGraphExecutor;
@@ -19,7 +17,6 @@ use document_legacy::layers::style::RenderData;
use document_legacy::Operation as DocumentOperation;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_core::raster::Image;
use graphene_core::text::Font;
#[derive(Debug, Default)]
@@ -218,70 +215,35 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
self.executor.update_font_cache(self.persistent_data.font_cache.clone());
}
PortfolioMessage::ImaginateCheckServerStatus => {
self.persistent_data.imaginate_server_status = ImaginateServerStatus::Checking;
responses.add(FrontendMessage::TriggerImaginateCheckServerStatus {
hostname: preferences.imaginate_server_hostname.clone(),
});
responses.add(PropertiesPanelMessage::ResendActiveProperties);
}
PortfolioMessage::ImaginateSetGeneratingStatus {
document_id,
layer_path,
node_path,
percent,
status,
} => {
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.add(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),
let server_status = self.persistent_data.imaginate.server_status().clone();
self.persistent_data.imaginate.poll_server_check();
#[cfg(target_arch = "wasm32")]
if let Some(fut) = self.persistent_data.imaginate.initiate_server_check() {
future_executor::spawn(async move {
let () = fut.await;
use wasm_bindgen::prelude::*;
#[wasm_bindgen(module = "/../frontend/src/wasm-communication/editor.ts")]
extern "C" {
#[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
fn inject();
}
.into(),
});
inject();
})
}
if &server_status != self.persistent_data.imaginate.server_status() {
responses.add(PropertiesPanelMessage::ResendActiveProperties);
}
responses.add(PortfolioMessage::DocumentPassMessage {
document_id,
message: NodeGraphMessage::SetQualifiedInputValue {
layer_path,
node_path,
input_index: get("Status"),
value: TaggedValue::ImaginateStatus(status),
}
.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 image = Image::from_image_data(&image_data, width, height);
responses.add(PortfolioMessage::DocumentPassMessage {
document_id,
message: NodeGraphMessage::SetQualifiedInputValue {
layer_path,
node_path,
input_index: get("Cached Data"),
value: TaggedValue::RcImage(Some(std::sync::Arc::new(image))),
}
.into(),
});
}
PortfolioMessage::ImaginateSetServerStatus { status } => {
self.persistent_data.imaginate_server_status = status;
PortfolioMessage::ImaginatePollServerStatus => {
self.persistent_data.imaginate.poll_server_check();
responses.add(PropertiesPanelMessage::ResendActiveProperties);
}
PortfolioMessage::ImaginatePreferences => self.executor.update_imaginate_preferences(preferences.get_imaginate_preferences()),
PortfolioMessage::ImaginateServerHostname => {
info!("setting imaginate persistent data");
self.persistent_data.imaginate.set_host_name(&preferences.imaginate_server_hostname);
}
PortfolioMessage::Import => {
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages
if self.active_document().is_some() {
@@ -461,7 +423,6 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
(document_id, &mut self.documents),
layer_path,
(input_image_data, size),
imaginate_node_path,
(preferences, &self.persistent_data),
responses,
);

View File

@@ -1,29 +1,11 @@
use graphene_std::text::FontCache;
use graphene_std::{imaginate::ImaginatePersistentData, text::FontCache};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug)]
#[derive(Debug, Default)]
pub struct PersistentData {
pub font_cache: FontCache,
pub imaginate_server_status: ImaginateServerStatus,
}
impl Default for PersistentData {
fn default() -> Self {
Self {
font_cache: Default::default(),
imaginate_server_status: ImaginateServerStatus::Unknown,
}
}
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, Serialize, Deserialize, specta::Type)]
pub enum ImaginateServerStatus {
#[default]
Unknown,
Checking,
Unavailable,
Connected,
pub imaginate: ImaginatePersistentData,
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, Serialize, Deserialize)]

View File

@@ -1,5 +1,6 @@
use crate::messages::input_mapper::key_mapping::MappingVariant;
use crate::messages::prelude::*;
use graph_craft::imaginate_input::ImaginatePreferences;
use serde::{Deserialize, Serialize};
@@ -10,10 +11,19 @@ pub struct PreferencesMessageHandler {
pub zoom_with_scroll: bool,
}
impl PreferencesMessageHandler {
pub fn get_imaginate_preferences(&self) -> ImaginatePreferences {
ImaginatePreferences {
host_name: self.imaginate_server_hostname.clone(),
}
}
}
impl Default for PreferencesMessageHandler {
fn default() -> Self {
let ImaginatePreferences { host_name } = Default::default();
Self {
imaginate_server_hostname: "http://localhost:7860/".into(),
imaginate_server_hostname: host_name,
imaginate_refresh_frequency: 1.,
zoom_with_scroll: matches!(MappingVariant::default(), MappingVariant::ZoomWithScroll),
}
@@ -28,9 +38,9 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
if let Ok(deserialized_preferences) = serde_json::from_str::<PreferencesMessageHandler>(&preferences) {
*self = deserialized_preferences;
if self.imaginate_server_hostname != Self::default().imaginate_server_hostname {
responses.add(PortfolioMessage::ImaginateCheckServerStatus);
}
responses.add(PortfolioMessage::ImaginateServerHostname);
responses.add(PortfolioMessage::ImaginateCheckServerStatus);
responses.add(PortfolioMessage::ImaginatePreferences);
}
}
PreferencesMessage::ResetToDefaults => {
@@ -43,6 +53,7 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
PreferencesMessage::ImaginateRefreshFrequency { seconds } => {
self.imaginate_refresh_frequency = seconds;
responses.add(PortfolioMessage::ImaginateCheckServerStatus);
responses.add(PortfolioMessage::ImaginatePreferences);
}
PreferencesMessage::ImaginateServerHostname { hostname } => {
let initial = hostname.clone();
@@ -55,7 +66,9 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
}
self.imaginate_server_hostname = hostname;
responses.add(PortfolioMessage::ImaginateServerHostname);
responses.add(PortfolioMessage::ImaginateCheckServerStatus);
responses.add(PortfolioMessage::ImaginatePreferences);
}
PreferencesMessage::ModifyLayout { zoom_with_scroll } => {
self.zoom_with_scroll = zoom_with_scroll;

View File

@@ -10,16 +10,16 @@ use document_legacy::{LayerId, Operation};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{generate_uuid, DocumentNodeImplementation, NodeId, NodeNetwork};
use graph_craft::graphene_compiler::Compiler;
use graph_craft::imaginate_input::ImaginatePreferences;
use graph_craft::{concrete, Type, TypeDescriptor};
use graphene_core::application_io::ApplicationIo;
use graphene_core::application_io::{ApplicationIo, NodeGraphUpdateMessage, NodeGraphUpdateSender};
use graphene_core::raster::{Image, ImageFrame};
use graphene_core::renderer::{SvgSegment, SvgSegmentList};
use graphene_core::text::FontCache;
use graphene_core::vector::style::ViewMode;
use graphene_core::{Color, SurfaceFrame, SurfaceId};
use graphene_std::wasm_application_io::WasmApplicationIo;
use graphene_std::wasm_application_io::WasmEditorApi;
use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi};
use interpreted_executor::dynamic_executor::DynamicExecutor;
use glam::{DAffine2, DVec2};
@@ -33,8 +33,9 @@ pub struct NodeRuntime {
pub(crate) executor: DynamicExecutor,
font_cache: FontCache,
receiver: Receiver<NodeRuntimeMessage>,
sender: Sender<GenerationResponse>,
sender: InternalNodeGraphUpdateSender,
wasm_io: Option<WasmApplicationIo>,
imaginate_preferences: ImaginatePreferences,
pub(crate) thumbnails: HashMap<LayerId, HashMap<NodeId, SvgSegmentList>>,
canvas_cache: HashMap<Vec<LayerId>, SurfaceId>,
}
@@ -42,6 +43,7 @@ pub struct NodeRuntime {
enum NodeRuntimeMessage {
GenerationRequest(GenerationRequest),
FontCacheUpdate(FontCache),
ImaginatePreferencesUpdate(ImaginatePreferences),
}
pub(crate) struct GenerationRequest {
@@ -50,6 +52,7 @@ pub(crate) struct GenerationRequest {
path: Vec<LayerId>,
image_frame: Option<ImageFrame<Color>>,
}
pub(crate) struct GenerationResponse {
generation_id: u64,
result: Result<TaggedValue, String>,
@@ -57,18 +60,38 @@ pub(crate) struct GenerationResponse {
new_thumbnails: HashMap<LayerId, HashMap<NodeId, SvgSegmentList>>,
}
enum NodeGraphUpdate {
GenerationResponse(GenerationResponse),
NodeGraphUpdateMessage(NodeGraphUpdateMessage),
}
struct InternalNodeGraphUpdateSender(Sender<NodeGraphUpdate>);
impl InternalNodeGraphUpdateSender {
fn send_generation_response(&self, response: GenerationResponse) {
self.0.send(NodeGraphUpdate::GenerationResponse(response)).expect("Failed to send response")
}
}
impl NodeGraphUpdateSender for InternalNodeGraphUpdateSender {
fn send(&self, message: NodeGraphUpdateMessage) {
self.0.send(NodeGraphUpdate::NodeGraphUpdateMessage(message)).expect("Failed to send response")
}
}
thread_local! {
pub(crate) static NODE_RUNTIME: Rc<RefCell<Option<NodeRuntime>>> = Rc::new(RefCell::new(None));
}
impl NodeRuntime {
fn new(receiver: Receiver<NodeRuntimeMessage>, sender: Sender<GenerationResponse>) -> Self {
fn new(receiver: Receiver<NodeRuntimeMessage>, sender: Sender<NodeGraphUpdate>) -> Self {
let executor = DynamicExecutor::default();
Self {
executor,
receiver,
sender,
sender: InternalNodeGraphUpdateSender(sender),
font_cache: FontCache::default(),
imaginate_preferences: Default::default(),
thumbnails: Default::default(),
wasm_io: None,
canvas_cache: Default::default(),
@@ -80,13 +103,14 @@ impl NodeRuntime {
// This should be avoided in the future.
requests.reverse();
requests.dedup_by_key(|x| match x {
NodeRuntimeMessage::FontCacheUpdate(_) => None,
NodeRuntimeMessage::GenerationRequest(x) => Some(x.path.clone()),
_ => None,
});
requests.reverse();
for request in requests {
match request {
NodeRuntimeMessage::FontCacheUpdate(font_cache) => self.font_cache = font_cache,
NodeRuntimeMessage::ImaginatePreferencesUpdate(preferences) => self.imaginate_preferences = preferences,
NodeRuntimeMessage::GenerationRequest(GenerationRequest {
generation_id,
graph,
@@ -105,7 +129,7 @@ impl NodeRuntime {
updates: responses,
new_thumbnails: self.thumbnails.clone(),
};
self.sender.send(response).expect("Failed to send response");
self.sender.send_generation_response(response);
}
}
}
@@ -134,6 +158,8 @@ impl NodeRuntime {
font_cache: &self.font_cache,
image_frame,
application_io: &self.wasm_io.as_ref().unwrap(),
node_graph_message_sender: &self.sender,
imaginate_preferences: &self.imaginate_preferences,
};
// We assume only one output
@@ -240,7 +266,7 @@ pub async fn run_node_graph() {
#[derive(Debug)]
pub struct NodeGraphExecutor {
sender: Sender<NodeRuntimeMessage>,
receiver: Receiver<GenerationResponse>,
receiver: Receiver<NodeGraphUpdate>,
// TODO: This is a memory leak since layers are never removed
pub(crate) last_output_type: HashMap<Vec<LayerId>, Option<Type>>,
pub(crate) thumbnails: HashMap<LayerId, HashMap<NodeId, SvgSegmentList>>,
@@ -294,10 +320,31 @@ impl NodeGraphExecutor {
self.sender.send(NodeRuntimeMessage::FontCacheUpdate(font_cache)).expect("Failed to send font cache update");
}
pub fn update_imaginate_preferences(&self, imaginate_preferences: ImaginatePreferences) {
self.sender
.send(NodeRuntimeMessage::ImaginatePreferencesUpdate(imaginate_preferences))
.expect("Failed to send imaginate preferences");
}
pub fn previous_output_type(&self, path: &[LayerId]) -> Option<Type> {
self.last_output_type.get(path).cloned().flatten()
}
pub fn introspect_node_in_network<T: std::any::Any + core::fmt::Debug, U, F1: FnOnce(&NodeNetwork) -> Option<NodeId>, F2: FnOnce(&T) -> U>(
&mut self,
network: &NodeNetwork,
node_path: &[NodeId],
find_node: F1,
extract_data: F2,
) -> Option<U> {
let wrapping_document_node = network.nodes.get(node_path.last()?)?;
let DocumentNodeImplementation::Network(wrapped_network) = &wrapping_document_node.implementation else { return None; };
let introspection_node = find_node(&wrapped_network)?;
let introspection = self.introspect_node(&[node_path, &[introspection_node]].concat())?;
let downcasted: &T = <dyn std::any::Any>::downcast_ref(introspection.as_ref())?;
Some(extract_data(downcasted))
}
/// Encodes an image into a format using the image crate
fn encode_img(image: Image<Color>, resize: Option<DVec2>, format: image::ImageOutputFormat) -> Result<(Vec<u8>, (u32, u32)), String> {
use image::{ImageBuffer, Rgba};
@@ -334,13 +381,12 @@ impl NodeGraphExecutor {
})
}
/// Evaluates a node graph, computing either the Imaginate node or the entire graph
/// Evaluates a node graph, computing the entire graph
pub fn submit_node_graph_evaluation(
&mut self,
(document_id, documents): (u64, &mut HashMap<u64, DocumentMessageHandler>),
layer_path: Vec<LayerId>,
(input_image_data, (width, height)): (Vec<u8>, (u32, u32)),
_imaginate_node: Option<Vec<NodeId>>,
_persistent_data: (&PreferencesMessageHandler, &PersistentData),
_responses: &mut VecDeque<Message>,
) -> Result<(), String> {
@@ -365,11 +411,6 @@ impl NodeGraphExecutor {
let transform = DAffine2::IDENTITY;
let image_frame = ImageFrame { image, transform };
// Special execution path for generating Imaginate (as generation requires IO from outside node graph)
/*if let Some(imaginate_node) = imaginate_node {
responses.add(self.generate_imaginate(network, imaginate_node, (document, document_id), layer_path, editor_api, persistent_data)?);
return Ok(());
}*/
// Execute the node graph
let generation_id = self.queue_execution(network, Some(image_frame), layer_path.clone());
@@ -381,26 +422,32 @@ impl NodeGraphExecutor {
pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque<Message>) -> Result<(), String> {
let results = self.receiver.try_iter().collect::<Vec<_>>();
for response in results {
let GenerationResponse {
generation_id,
result,
updates,
new_thumbnails,
} = response;
self.thumbnails = new_thumbnails;
let node_graph_output = result.map_err(|e| format!("Node graph evaluation failed: {:?}", e))?;
let execution_context = self.futures.remove(&generation_id).ok_or_else(|| "Invalid generation ID".to_string())?;
responses.extend(updates);
self.process_node_graph_output(node_graph_output, execution_context.layer_path.clone(), responses, execution_context.document_id)?;
responses.add(DocumentMessage::LayerChanged {
affected_layer_path: execution_context.layer_path,
});
responses.add(DocumentMessage::RenderDocument);
responses.add(ArtboardMessage::RenderArtboards);
responses.add(DocumentMessage::DocumentStructureChanged);
responses.add(BroadcastEvent::DocumentIsDirty);
responses.add(DocumentMessage::DirtyRenderDocument);
responses.add(DocumentMessage::Overlays(OverlaysMessage::Rerender));
match response {
NodeGraphUpdate::GenerationResponse(GenerationResponse {
generation_id,
result,
updates,
new_thumbnails,
}) => {
self.thumbnails = new_thumbnails;
let node_graph_output = result.map_err(|e| format!("Node graph evaluation failed: {:?}", e))?;
let execution_context = self.futures.remove(&generation_id).ok_or_else(|| "Invalid generation ID".to_string())?;
responses.extend(updates);
self.process_node_graph_output(node_graph_output, execution_context.layer_path.clone(), responses, execution_context.document_id)?;
responses.add(DocumentMessage::LayerChanged {
affected_layer_path: execution_context.layer_path,
});
responses.add(DocumentMessage::RenderDocument);
responses.add(ArtboardMessage::RenderArtboards);
responses.add(DocumentMessage::DocumentStructureChanged);
responses.add(BroadcastEvent::DocumentIsDirty);
responses.add(DocumentMessage::DirtyRenderDocument);
responses.add(DocumentMessage::Overlays(OverlaysMessage::Rerender));
}
NodeGraphUpdate::NodeGraphUpdateMessage(NodeGraphUpdateMessage::ImaginateStatusUpdate) => {
responses.add(DocumentMessage::PropertiesPanel(PropertiesPanelMessage::ResendActiveProperties))
}
}
}
Ok(())
}