Update graph UI and improve simplicity and robustness of sending graph and layer panel updates (#1564)

* WIP

* Fix loading the structure of layers

* Fix broken indents

* Remove debugging stuff

* Fix displaying errors and node graph UI fixes/improvements

* Fix compilation failure

---------

Co-authored-by: 0hypercube <0hypercube@gmail.com>
This commit is contained in:
Keavon Chambers
2024-01-13 04:15:36 -08:00
committed by GitHub
parent 83116aa744
commit aab0fcf84c
33 changed files with 836 additions and 813 deletions

View File

@@ -1,10 +1,7 @@
use crate::consts::FILE_SAVE_SUFFIX;
use crate::messages::frontend::utility_types::FrontendImageData;
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
use crate::messages::portfolio::document::node_graph::wrap_network_in_scope;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::layer_panel::LayerClassification;
use crate::messages::portfolio::document::utility_types::misc::LayerPanelEntry;
use crate::messages::prelude::*;
use graph_craft::concrete;
@@ -15,8 +12,9 @@ use graph_craft::imaginate_input::ImaginatePreferences;
use graph_craft::proto::GraphErrors;
use graphene_core::application_io::{NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
use graphene_core::memo::IORecord;
use graphene_core::raster::{Image, ImageFrame};
use graphene_core::renderer::{ClickTarget, GraphicElementRendered, SvgSegmentList};
use graphene_core::raster::ImageFrame;
use graphene_core::renderer::{ClickTarget, GraphicElementRendered, ImageRenderMode, RenderParams, SvgRender};
use graphene_core::renderer::{RenderSvgSegmentList, SvgSegment};
use graphene_core::text::FontCache;
use graphene_core::transform::{Footprint, Transform};
use graphene_core::vector::style::ViewMode;
@@ -27,28 +25,45 @@ use interpreted_executor::dynamic_executor::{DynamicExecutor, ResolvedDocumentNo
use glam::{DAffine2, DVec2, UVec2};
use std::cell::RefCell;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hash;
use std::hash::Hasher;
use std::rc::Rc;
use std::sync::mpsc::{Receiver, Sender};
use std::sync::Arc;
/// Persistent data between graph executions. It's updated via message passing from the editor thread with [`NodeRuntimeMessage`]`.
/// Some of these fields are put into a [`WasmEditorApi`] which is passed to the final compiled graph network upon each execution.
/// Once the implementation is finished, this will live in a separate thread. Right now it's part of the main JS thread, but its own separate JS stack frame independent from the editor.
pub struct NodeRuntime {
pub(crate) executor: DynamicExecutor,
font_cache: FontCache,
executor: DynamicExecutor,
receiver: Receiver<NodeRuntimeMessage>,
sender: InternalNodeGraphUpdateSender,
wasm_io: Option<WasmApplicationIo>,
/// Font data (for rendering text) made available to the graph through the [`WasmEditorApi`].
font_cache: FontCache,
/// Imaginate preferences made available to the graph through the [`WasmEditorApi`].
imaginate_preferences: ImaginatePreferences,
pub(crate) thumbnails: HashMap<NodeId, SvgSegmentList>,
pub(crate) click_targets: HashMap<NodeId, Vec<ClickTarget>>,
pub(crate) upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
pub(crate) resolved_types: ResolvedDocumentNodeTypes,
pub(crate) node_graph_errors: GraphErrors,
/// Gives access to APIs like a rendering surface (native window handle or HTML5 canvas) and WGPU (which becomes WebGPU on web).
wasm_application_io: Option<WasmApplicationIo>,
graph_hash: Option<u64>,
node_graph_errors: GraphErrors,
resolved_types: ResolvedDocumentNodeTypes,
monitor_nodes: Vec<Vec<NodeId>>,
// TODO: Remove, it doesn't need to be persisted anymore
/// The current renders of the thumbnails for layer nodes.
thumbnail_renders: HashMap<NodeId, Vec<SvgSegment>>,
/// The current click targets for layer nodes.
click_targets: HashMap<NodeId, Vec<ClickTarget>>,
/// The current upstream transforms for nodes.
upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
}
/// Messages passed from the editor thread to the node runtime thread.
enum NodeRuntimeMessage {
GenerationRequest(GenerationRequest),
ExecutionRequest(ExecutionRequest),
FontCacheUpdate(FontCache),
ImaginatePreferencesUpdate(ImaginatePreferences),
}
@@ -63,17 +78,16 @@ pub struct ExportConfig {
pub size: DVec2,
}
pub(crate) struct GenerationRequest {
generation_id: u64,
pub(crate) struct ExecutionRequest {
execution_id: u64,
graph: NodeNetwork,
render_config: RenderConfig,
}
pub(crate) struct GenerationResponse {
generation_id: u64,
pub(crate) struct ExecutionResponse {
execution_id: u64,
result: Result<TaggedValue, String>,
updates: VecDeque<Message>,
new_thumbnails: HashMap<NodeId, SvgSegmentList>,
responses: VecDeque<Message>,
new_click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
new_upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
resolved_types: ResolvedDocumentNodeTypes,
@@ -82,15 +96,15 @@ pub(crate) struct GenerationResponse {
}
enum NodeGraphUpdate {
GenerationResponse(GenerationResponse),
ExecutionResponse(ExecutionResponse),
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")
fn send_generation_response(&self, response: ExecutionResponse) {
self.0.send(NodeGraphUpdate::ExecutionResponse(response)).expect("Failed to send response")
}
}
@@ -106,30 +120,33 @@ thread_local! {
impl NodeRuntime {
fn new(receiver: Receiver<NodeRuntimeMessage>, sender: Sender<NodeGraphUpdate>) -> Self {
let executor = DynamicExecutor::default();
Self {
executor,
executor: DynamicExecutor::default(),
receiver,
sender: InternalNodeGraphUpdateSender(sender),
font_cache: FontCache::default(),
imaginate_preferences: Default::default(),
thumbnails: Default::default(),
wasm_io: None,
click_targets: HashMap::new(),
wasm_application_io: None,
graph_hash: None,
upstream_transforms: HashMap::new(),
resolved_types: ResolvedDocumentNodeTypes::default(),
node_graph_errors: Vec::new(),
resolved_types: ResolvedDocumentNodeTypes::default(),
monitor_nodes: Vec::new(),
thumbnail_renders: Default::default(),
click_targets: HashMap::new(),
upstream_transforms: HashMap::new(),
}
}
pub async fn run(&mut self) {
let mut requests = self.receiver.try_iter().collect::<Vec<_>>();
// TODO: Currently we still render the document after we submit the node graph execution request.
// This should be avoided in the future.
requests.reverse();
requests.dedup_by_key(|x| match x {
NodeRuntimeMessage::GenerationRequest(x) => Some(x.graph.current_hash()),
NodeRuntimeMessage::ExecutionRequest(x) => Some(x.graph.current_hash()),
_ => None,
});
requests.reverse();
@@ -137,49 +154,45 @@ impl NodeRuntime {
match request {
NodeRuntimeMessage::FontCacheUpdate(font_cache) => self.font_cache = font_cache,
NodeRuntimeMessage::ImaginatePreferencesUpdate(preferences) => self.imaginate_preferences = preferences,
NodeRuntimeMessage::GenerationRequest(GenerationRequest {
generation_id, graph, render_config, ..
NodeRuntimeMessage::ExecutionRequest(ExecutionRequest {
execution_id, graph, render_config, ..
}) => {
let transform = render_config.viewport.transform;
let result = self.execute_network(graph, render_config).await;
let mut responses = VecDeque::new();
self.process_monitor_nodes(&mut responses);
self.update_thumbnails(&mut responses);
self.update_upstream_transforms();
let response = GenerationResponse {
generation_id,
self.sender.send_generation_response(ExecutionResponse {
execution_id,
result,
updates: responses,
new_thumbnails: self.thumbnails.clone(),
responses,
new_click_targets: self.click_targets.clone().into_iter().map(|(id, targets)| (LayerNodeIdentifier::new_unchecked(id), targets)).collect(),
new_upstream_transforms: self.upstream_transforms.clone(),
resolved_types: self.resolved_types.clone(),
node_graph_errors: core::mem::take(&mut self.node_graph_errors),
transform,
};
self.sender.send_generation_response(response);
});
}
}
}
}
async fn execute_network<'a>(&'a mut self, graph: NodeNetwork, render_config: RenderConfig) -> Result<TaggedValue, String> {
if self.wasm_io.is_none() {
self.wasm_io = Some(WasmApplicationIo::new().await);
if self.wasm_application_io.is_none() {
self.wasm_application_io = Some(WasmApplicationIo::new().await);
}
let editor_api = WasmEditorApi {
font_cache: &self.font_cache,
application_io: self.wasm_io.as_ref().unwrap(),
node_graph_message_sender: &self.sender,
imaginate_preferences: &self.imaginate_preferences,
application_io: self.wasm_application_io.as_ref().unwrap(),
node_graph_message_sender: &self.sender,
render_config,
image_frame: None,
};
use std::collections::hash_map::DefaultHasher;
use std::hash::Hash;
use std::hash::Hasher;
// Required to ensure that the appropriate protonodes are reinserted when the Editor API changes.
let mut graph_input_hash = DefaultHasher::new();
editor_api.font_cache.hash(&mut graph_input_hash);
@@ -243,85 +256,85 @@ impl NodeRuntime {
Ok(result)
}
/// Recomputes the thumbnails for the layers in the graph, modifying the state and updating the UI.
pub fn update_thumbnails(&mut self, responses: &mut VecDeque<Message>) {
let mut image_data: Vec<_> = Vec::new();
self.thumbnails.retain(|id, _| self.monitor_nodes.iter().any(|node_path| node_path.contains(id)));
for node_path in &self.monitor_nodes {
let Some(node_id) = node_path.get(node_path.len() - 2).copied() else {
/// Updates state data
pub fn process_monitor_nodes(&mut self, responses: &mut VecDeque<Message>) {
// TODO: Consider optimizing this since it's currently O(m*n^2), with a sort it could be made O(m * n*log(n))
self.thumbnail_renders.retain(|id, _| self.monitor_nodes.iter().any(|monitor_node_path| monitor_node_path.contains(id)));
for monitor_node_path in &self.monitor_nodes {
// The monitor nodes are located within a document node, and are thus children in that network, so this gets the parent document node's ID
let Some(parent_network_node_id) = monitor_node_path.get(monitor_node_path.len() - 2).copied() else {
warn!("Monitor node has invalid node id");
continue;
};
let Some(value) = self.executor.introspect(node_path).flatten() else {
warn!("Failed to introspect monitor node for thumbnail");
continue;
};
let Some(io_data) = value.downcast_ref::<IORecord<Footprint, graphene_core::GraphicElement>>() else {
// Extract the monitor node's stored `GraphicElement` data.
let Some(introspected_data) = self.executor.introspect(monitor_node_path).flatten() else {
// TODO: Fix the root of the issue causing the spam of this warning (this at least temporarily disables it in release builds)
#[cfg(debug_assertions)]
warn!("Failed to introspect monitor node");
continue;
};
let graphic_element = &io_data.output;
use graphene_core::renderer::*;
let bounds = graphic_element.bounding_box(DAffine2::IDENTITY);
let render_params = RenderParams::new(ViewMode::Normal, ImageRenderMode::BlobUrl, bounds, true, false, false);
let mut render = SvgRender::new();
graphic_element.render_svg(&mut render, &render_params);
let [min, max] = bounds.unwrap_or_default();
render.format_svg(min, max);
let click_targets = self.click_targets.entry(node_id).or_default();
click_targets.clear();
// Add the graphic element data's click targets to the click targets vector
graphic_element.add_click_targets(click_targets);
// If this is `GraphicElement` data:
// Regenerate click targets and thumbnails for the layers in the graph, modifying the state and updating the UI.
if let Some(io_data) = introspected_data.downcast_ref::<IORecord<Footprint, graphene_core::GraphicElement>>() {
let graphic_element = &io_data.output;
let old_thumbnail = self.thumbnails.entry(node_id).or_default();
if *old_thumbnail != render.svg {
responses.add(FrontendMessage::UpdateNodeThumbnail {
id: node_id,
value: render.svg.to_string(),
});
*old_thumbnail = render.svg;
// UPDATE CLICK TARGETS
// Get the previously stored click targets and wipe them out, then regenerate them
let click_targets = self.click_targets.entry(parent_network_node_id).or_default();
click_targets.clear();
graphic_element.add_click_targets(click_targets);
// RENDER THUMBNAIL
let bounds = graphic_element.bounding_box(DAffine2::IDENTITY);
// Render the thumbnail from a `GraphicElement` into an SVG string
let render_params = RenderParams::new(ViewMode::Normal, ImageRenderMode::Base64, bounds, true, false, false);
let mut render = SvgRender::new();
graphic_element.render_svg(&mut render, &render_params);
// And give the SVG a viewbox and outer <svg>...</svg> wrapper tag
let [min, max] = bounds.unwrap_or_default();
render.format_svg(min, max);
// UPDATE FRONTEND THUMBNAIL
let new_thumbnail_svg = render.svg;
let old_thumbnail_svg = self.thumbnail_renders.entry(parent_network_node_id).or_default();
if old_thumbnail_svg != &new_thumbnail_svg {
responses.add(FrontendMessage::UpdateNodeThumbnail {
id: parent_network_node_id,
value: new_thumbnail_svg.to_svg_string(),
});
*old_thumbnail_svg = new_thumbnail_svg;
}
}
let resize = Some(DVec2::splat(100.));
image_data.extend(render.image_data.into_iter().filter_map(|(_, image)| NodeGraphExecutor::to_frontend_image_data(image, resize).ok()))
}
if !image_data.is_empty() {
responses.add(FrontendMessage::UpdateImageData {
document_id: DocumentId(0),
image_data,
});
}
}
pub fn update_upstream_transforms(&mut self) {
for node_path in &self.monitor_nodes {
let Some(node_id) = node_path.get(node_path.len() - 2).copied() else {
warn!("Monitor node has invalid node id");
continue;
};
let Some(value) = self.executor.introspect(node_path).flatten() else {
warn!("Failed to introspect monitor node for upstream transforms");
continue;
};
fn try_downcast<T: Transform + 'static>(value: &dyn std::any::Any) -> Option<(Footprint, DAffine2)> {
let io_data = value.downcast_ref::<IORecord<Footprint, T>>()?;
let transform = io_data.output.transform();
Some((io_data.input, transform))
// If this is `VectorData`, `ImageFrame`, or `GraphicElement` data:
// Update the stored upstream transforms for this layer/node.
if let Some(transform) = {
fn try_downcast<T: Transform + 'static>(value: &dyn std::any::Any) -> Option<(Footprint, DAffine2)> {
let io_data = value.downcast_ref::<IORecord<Footprint, T>>()?;
let transform = io_data.output.transform();
Some((io_data.input, transform))
}
None.or_else(|| try_downcast::<VectorData>(introspected_data.as_ref()))
.or_else(|| try_downcast::<ImageFrame<Color>>(introspected_data.as_ref()))
.or_else(|| try_downcast::<GraphicElement>(introspected_data.as_ref()))
} {
self.upstream_transforms.insert(parent_network_node_id, transform);
}
let Some(transform) = try_downcast::<VectorData>(value.as_ref())
.or_else(|| try_downcast::<ImageFrame<Color>>(value.as_ref()))
.or_else(|| try_downcast::<GraphicElement>(value.as_ref()))
else {
warn!("Failed to downcast transform input");
continue;
};
self.upstream_transforms.insert(node_id, transform);
}
}
}
pub fn introspect_node(path: &[NodeId]) -> Option<Arc<dyn std::any::Any>> {
NODE_RUNTIME
.try_with(|runtime| {
@@ -384,15 +397,15 @@ impl Default for NodeGraphExecutor {
impl NodeGraphExecutor {
/// Execute the network by flattening it and creating a borrow stack.
fn queue_execution(&self, network: NodeNetwork, render_config: RenderConfig) -> u64 {
let generation_id = generate_uuid();
let request = GenerationRequest {
let execution_id = generate_uuid();
let request = ExecutionRequest {
graph: network,
generation_id,
execution_id,
render_config,
};
self.sender.send(NodeRuntimeMessage::GenerationRequest(request)).expect("Failed to send generation request");
self.sender.send(NodeRuntimeMessage::ExecutionRequest(request)).expect("Failed to send generation request");
generation_id
execution_id
}
pub fn introspect_node(&self, path: &[NodeId]) -> Option<Arc<dyn std::any::Any>> {
@@ -429,36 +442,6 @@ impl NodeGraphExecutor {
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};
use std::io::Cursor;
let (result_bytes, width, height) = image.to_flat_u8();
let mut output: ImageBuffer<Rgba<u8>, _> = image::ImageBuffer::from_raw(width, 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();
let mut image_data: Vec<u8> = Vec::new();
output.write_to(&mut Cursor::new(&mut image_data), format).map_err(|e| e.to_string())?;
Ok::<_, String>((image_data, size))
}
/// Generate a new [`FrontendImageData`] from the [`Image`].
fn to_frontend_image_data(image: Image<Color>, resize: Option<DVec2>) -> Result<FrontendImageData, String> {
let (image_data, _size) = Self::encode_img(image, resize, image::ImageOutputFormat::Bmp)?;
let mime = "image/bmp".to_string();
let image_data = std::sync::Arc::new(image_data);
Ok(FrontendImageData { image_data, mime })
}
/// Evaluates a node graph, computing the entire graph
pub fn submit_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2) -> Result<(), String> {
// Get the node graph layer
@@ -466,7 +449,7 @@ impl NodeGraphExecutor {
let render_config = RenderConfig {
viewport: Footprint {
transform: document.metadata.document_to_viewport,
transform: document.document_metadata.document_to_viewport,
resolution: viewport_resolution,
..Default::default()
},
@@ -480,9 +463,9 @@ impl NodeGraphExecutor {
};
// Execute the node graph
let generation_id = self.queue_execution(network, render_config);
let execution_id = self.queue_execution(network, render_config);
self.futures.insert(generation_id, ExecutionContext { export_config: None });
self.futures.insert(execution_id, ExecutionContext { export_config: None });
Ok(())
}
@@ -515,9 +498,9 @@ impl NodeGraphExecutor {
export_config.size = size;
// Execute the node graph
let generation_id = self.queue_execution(network, render_config);
let execution_id = self.queue_execution(network, render_config);
let execution_context = ExecutionContext { export_config: Some(export_config) };
self.futures.insert(generation_id, execution_context);
self.futures.insert(execution_id, execution_context);
Ok(())
}
@@ -552,89 +535,59 @@ impl NodeGraphExecutor {
}
pub fn poll_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, responses: &mut VecDeque<Message>) -> Result<(), String> {
let DocumentMessageHandler {
network: document_network,
metadata: document_metadata,
collapsed,
..
} = document;
let results = self.receiver.try_iter().collect::<Vec<_>>();
for response in results {
match response {
NodeGraphUpdate::GenerationResponse(GenerationResponse {
generation_id,
result,
updates,
new_thumbnails,
new_click_targets,
new_upstream_transforms,
resolved_types,
node_graph_errors,
transform,
}) => {
NodeGraphUpdate::ExecutionResponse(execution_response) => {
let ExecutionResponse {
execution_id,
result,
responses: existing_responses,
new_click_targets,
new_upstream_transforms,
resolved_types,
node_graph_errors,
transform,
} = execution_response;
responses.extend(existing_responses);
responses.add(NodeGraphMessage::UpdateTypes { resolved_types, node_graph_errors });
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())?;
if let Some(export_config) = execution_context.export_config {
return self.export(node_graph_output, export_config, responses);
}
for (&node_id, svg) in &new_thumbnails {
if !document_network.nodes.contains_key(&node_id) {
warn!("Missing node");
continue;
}
let layer = LayerNodeIdentifier::new(node_id, document_network);
responses.add(FrontendMessage::UpdateDocumentLayerDetails {
data: LayerPanelEntry {
name: document_network.nodes.get(&node_id).map(|node| node.alias.clone()).unwrap_or_default(),
tooltip: if cfg!(debug_assertions) { format!("Layer ID: {node_id}") } else { "".into() },
layer_classification: if document_metadata.is_artboard(layer) {
LayerClassification::Artboard
} else if document_metadata.is_folder(layer) {
LayerClassification::Folder
} else {
LayerClassification::Layer
},
expanded: layer.has_children(document_metadata) && !collapsed.contains(&layer),
selected: document_metadata.selected_layers_contains(layer),
parent_id: layer.parent(document_metadata).map(|parent| parent.to_node()),
id: node_id,
depth: layer.ancestors(document_metadata).count() - 1,
thumbnail: svg.to_string(),
},
});
}
document_metadata.update_transforms(new_upstream_transforms);
document_metadata.update_click_targets(new_click_targets);
responses.extend(updates);
self.process_node_graph_output(node_graph_output, transform, responses)?;
responses.add(DocumentMessage::RenderDocument);
responses.add(DocumentMessage::DocumentStructureChanged);
responses.add(NodeGraphMessage::SendGraph);
responses.add(BroadcastEvent::DocumentIsDirty);
responses.add(OverlaysMessage::Draw);
let node_graph_output = result.map_err(|e| format!("Node graph evaluation failed: {e:?}"))?;
document.document_metadata.update_transforms(new_upstream_transforms);
document.document_metadata.update_click_targets(new_click_targets);
let execution_context = self.futures.remove(&execution_id).ok_or_else(|| "Invalid generation ID".to_string())?;
if let Some(export_config) = execution_context.export_config {
// Special handling for exporting the artwork
self.export(node_graph_output, export_config, responses)?
} else {
self.process_node_graph_output(node_graph_output, transform, responses)?
}
}
NodeGraphUpdate::NodeGraphUpdateMessage(NodeGraphUpdateMessage::ImaginateStatusUpdate) => {
responses.add(DocumentMessage::PropertiesPanel(PropertiesPanelMessage::Refresh));
}
NodeGraphUpdate::NodeGraphUpdateMessage(NodeGraphUpdateMessage::ImaginateStatusUpdate) => responses.add(DocumentMessage::PropertiesPanel(PropertiesPanelMessage::Refresh)),
}
}
Ok(())
}
fn render(render_object: impl GraphicElementRendered, transform: DAffine2, responses: &mut VecDeque<Message>) {
use graphene_core::renderer::{ImageRenderMode, RenderParams, SvgRender};
fn debug_render(render_object: impl GraphicElementRendered, transform: DAffine2, responses: &mut VecDeque<Message>) {
// Setup rendering
let mut render = SvgRender::new();
let render_params = RenderParams::new(ViewMode::Normal, ImageRenderMode::BlobUrl, None, false, false, false);
let render_params = RenderParams::new(ViewMode::Normal, ImageRenderMode::Base64, None, false, false, false);
// Render SVG
render_object.render_svg(&mut render, &render_params);
// Concatenate the defs and the SVG into one string
render.wrap_with_transform(transform, None);
let svg = render.svg.to_string();
let svg = render.svg.to_svg_string();
// Send to frontend
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
@@ -667,16 +620,16 @@ impl NodeGraphExecutor {
);
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
}
TaggedValue::Bool(render_object) => Self::render(render_object, transform, responses),
TaggedValue::String(render_object) => Self::render(render_object, transform, responses),
TaggedValue::F32(render_object) => Self::render(render_object, transform, responses),
TaggedValue::F64(render_object) => Self::render(render_object, transform, responses),
TaggedValue::OptionalColor(render_object) => Self::render(render_object, transform, responses),
TaggedValue::VectorData(render_object) => Self::render(render_object, transform, responses),
TaggedValue::GraphicGroup(render_object) => Self::render(render_object, transform, responses),
TaggedValue::Artboard(render_object) => Self::render(render_object, transform, responses),
TaggedValue::ImageFrame(render_object) => Self::render(render_object, transform, responses),
TaggedValue::Palette(render_object) => Self::render(render_object, transform, responses),
TaggedValue::Bool(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::String(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::F32(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::F64(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::OptionalColor(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::VectorData(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::GraphicGroup(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::Artboard(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::ImageFrame(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::Palette(render_object) => Self::debug_render(render_object, transform, responses),
_ => {
return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
}