Context nullification, cached monitor nodes

This commit is contained in:
Adam
2025-07-16 01:42:03 -07:00
parent 1398405529
commit cf0a32b9b1
82 changed files with 2235 additions and 1684 deletions
+248 -172
View File
@@ -1,16 +1,17 @@
pub mod value;
use crate::document::value::TaggedValue;
use crate::proto::{ConstructionArgs, NodeConstructionArgs, OriginalLocation, ProtoNode};
use crate::proto::{ConstructionArgs, NodeConstructionArgs, NodeValueArgs, ProtoNetwork, ProtoNode, UpstreamInputMetadata};
use dyn_any::DynAny;
use glam::IVec2;
use graphene_core::memo::MemoHashGuard;
use graphene_core::registry::NODE_CONTEXT_DEPENDENCY;
pub use graphene_core::uuid::generate_uuid;
use graphene_core::uuid::{CompiledProtonodeInput, NodeId, ProtonodePath, SNI};
use graphene_core::{Context, Cow, MemoHash, ProtoNodeIdentifier, Type};
use rustc_hash::FxHashMap;
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
/// Utility function for providing a default boolean value to serde.
@@ -509,94 +510,170 @@ impl NodeNetwork {
/// Functions for compiling the network
impl NodeNetwork {
// Returns a topologically sorted vec of protonodes, as well as metadata extracted during compilation
// Returns a topologically sorted vec of vec of protonodes, as well as metadata extracted during compilation
// The first index represents the greatest distance to the export
// Compiles a network with one export where any scope injections are added the top level network, and the network to run is implemented as a DocumentNodeImplementation::Network
// The traversal input is the node which calls the network to be flattened. If it is None, then start from the export.
// Every value protonode stores the connector which directly called it, which is used to map the value input to the protonode caller.
// Every value input connector is mapped to its caller, and every protonode is mapped to its caller. If there are multiple, then they are compared to ensure it is the same between compilations
pub fn flatten(&mut self) -> Result<(Vec<ProtoNode>, Vec<(AbsoluteInputConnector, CompiledProtonodeInput)>, Vec<(ProtonodePath, CompiledProtonodeInput)>), String> {
pub fn flatten(
&mut self,
) -> Result<
(
ProtoNetwork,
Vec<(Vec<AbsoluteInputConnector>, CompiledProtonodeInput)>,
Vec<(Vec<ProtonodePath>, CompiledProtonodeInput)>,
),
String,
> {
// These three arrays are stored in parallel
let mut protonetwork = Vec::new();
let mut value_connectors = Vec::new();
let mut protonode_paths = Vec::new();
let mut calling_protonodes = HashMap::new();
// This function creates a flattened network with populated original location fields but unmapped inputs
// This function creates a topologically flattened network with populated original location fields but unmapped inputs
// The input to flattened protonode hashmap is used to map the inputs
self.traverse_input(
&mut protonetwork,
&mut value_connectors,
&mut protonode_paths,
&mut calling_protonodes,
&mut HashMap::new(),
AbsoluteInputConnector::traversal_start(),
(0, 0),
);
let mut protonode_indices = HashMap::new();
self.traverse_input(&mut protonetwork, &mut HashMap::new(), &mut protonode_indices, AbsoluteInputConnector::traversal_start(), None);
let mut generated_snis = HashSet::new();
// If a node with the same sni is reached, then its original location metadata must be added to the one at the higher vec index
// The index will always be a ProtonodeEntry::Protonode
let mut generated_snis_to_index = HashMap::new();
// Generate SNI's. This gets called after all node inputs are replaced with their indices
for protonode_index in (0..protonetwork.len()).rev() {
let protonode = protonetwork.get_mut(protonode_index).unwrap();
if let ConstructionArgs::Nodes(NodeConstructionArgs { inputs: input_snis, .. }) = &protonode.construction_args {
for input_sni in input_snis {
assert_ne!(
*input_sni,
NodeId(0),
"All inputs should be mapped to a stable node index, and the calling nodes inputs should be updated"
);
}
}
use std::hash::Hasher;
let mut hasher = rustc_hash::FxHasher::default();
protonode.construction_args.hash(&mut hasher);
let mut stable_node_id = NodeId(hasher.finish());
// The stable node index must be unique for every protonode. If it has the same hash as another protonode, continue hashing itself
// For example two cache nodes connected to a Context getter node have two cache different values, even though the stable node id is the same.
while !generated_snis.insert(stable_node_id) {
stable_node_id.hash(&mut hasher);
stable_node_id = NodeId(hasher.finish());
}
protonode.stable_node_id = stable_node_id;
for (calling_node_index, input_index) in calling_protonodes.get(&protonode_index).unwrap() {
match &mut protonetwork.get_mut(*calling_node_index).unwrap().construction_args {
ConstructionArgs::Nodes(nodes) => {
*nodes.inputs.get_mut(*input_index).unwrap() = stable_node_id;
for protonode_index in 0..protonetwork.len() {
let ProtonodeEntry::Protonode(protonode) = protonetwork.get_mut(protonode_index).unwrap() else {
panic!("No protonode can be deduplicated during flattening");
};
// Generate context dependencies. If None, then it is a value node and does not require nullification
let mut protonode_context_dependencies = None;
if let ConstructionArgs::Nodes(NodeConstructionArgs { inputs, context_dependencies, .. }) = &mut protonode.construction_args {
for upstream_metadata in inputs.iter() {
let Some(upstream_metadata) = upstream_metadata else {
panic!("All inputs should be when the upstream SNI was generated");
};
for upstream_dependency in upstream_metadata.context_dependencies.iter().flatten() {
if !context_dependencies.contains(upstream_dependency) {
context_dependencies.push(upstream_dependency.clone());
}
}
// TODO: Implement for extract
_ => unreachable!(),
}
// The context_dependencies are now the union of all inputs and the dependencies of the protonode. Set the dependencies of each input to the difference, which represents the data to nullify
for upstream_metadata in inputs.iter_mut() {
let Some(upstream_metadata) = upstream_metadata else {
panic!("All inputs should be when the upstream SNI was generated");
};
match upstream_metadata.context_dependencies.as_ref() {
Some(upstream_dependencies) => {
upstream_metadata.context_dependencies = Some(
context_dependencies
.iter()
.filter(|protonode_dependency| !upstream_dependencies.contains(protonode_dependency))
.cloned()
.collect::<Vec<_>>(),
)
}
// If none then the upstream node is a Value node, so do not nullify the context
None => upstream_metadata.context_dependencies = Some(Vec::new()),
}
}
protonode_context_dependencies = Some(context_dependencies.clone());
}
protonode.generate_stable_node_id();
let current_stable_node_id = protonode.stable_node_id;
// If the stable node id is the same as a previous node, then deduplicate
let callers = if let Some(upstream_index) = generated_snis_to_index.get(&protonode.stable_node_id) {
let ProtonodeEntry::Protonode(deduplicated_protonode) = std::mem::replace(&mut protonetwork[protonode_index], ProtonodeEntry::Deduplicated(*upstream_index)) else {
panic!("Reached protonode must not be deduplicated");
};
let ProtonodeEntry::Protonode(upstream_protonode) = &mut protonetwork[*upstream_index] else {
panic!("Upstream protonode must not be deduplicated");
};
match deduplicated_protonode.construction_args {
ConstructionArgs::Value(node_value_args) => {
let ConstructionArgs::Value(upstream_value_args) = &mut upstream_protonode.construction_args else {
panic!("Upstream protonode must match current protonode construction args");
};
upstream_value_args.connector_paths.extend(node_value_args.connector_paths);
}
ConstructionArgs::Nodes(node_construction_args) => {
let ConstructionArgs::Nodes(upstream_value_args) = &mut upstream_protonode.construction_args else {
panic!("Upstream protonode must match current protonode construction args");
};
upstream_value_args.node_paths.extend(node_construction_args.node_paths);
// The dependencies of the deduplicated node and the upstream node are the same because all inputs are the same
}
ConstructionArgs::Inline(_) => todo!(),
}
// Set the caller of the upstream node to be the minimum of all deduplicated nodes and itself
upstream_protonode.caller = deduplicated_protonode.callers.iter().chain(upstream_protonode.caller.iter()).min().cloned();
deduplicated_protonode.callers
} else {
generated_snis_to_index.insert(protonode.stable_node_id, protonode_index);
protonode.caller = protonode.callers.iter().min().cloned();
std::mem::take(&mut protonode.callers)
};
// This runs for all protonodes
for (caller_path, input_index) in callers {
let caller_index = protonode_indices[&caller_path];
let ProtonodeEntry::Protonode(caller_protonode) = &mut protonetwork[caller_index] else {
panic!("Downstream caller cannot be deduplicated");
};
match &mut caller_protonode.construction_args {
ConstructionArgs::Nodes(nodes) => {
assert!(caller_index > protonode_index, "Caller index must be higher than current index");
let input_metadata: &mut Option<UpstreamInputMetadata> = &mut nodes.inputs[input_index];
if input_metadata.is_none() {
*input_metadata = Some(UpstreamInputMetadata {
input_sni: current_stable_node_id,
context_dependencies: protonode_context_dependencies.clone(),
})
}
}
// Value node cannot be a caller
ConstructionArgs::Value(_) => unreachable!(),
ConstructionArgs::Inline(_) => todo!(),
}
}
}
// Do another traversal now that the caller SNI have been generated to collect metadata for the editor
// Do another traversal now that the metadata has been accumulated after deduplication
// This includes the caller of all absolute value connections which have a NodeInput::Value, as well as the caller for each protonode
let mut value_connector_callers = Vec::new();
let mut protonode_callers = Vec::new();
// Collect caller ids into a separate vec so that the pronetwork can be mutably iterated over to take the connector/node paths rather than cloning
let calling_protonode_ids = protonetwork
.iter()
.map(|entry| match entry {
ProtonodeEntry::Protonode(proto_node) => proto_node.stable_node_id,
ProtonodeEntry::Deduplicated(upstream_protonode_index) => {
let ProtonodeEntry::Protonode(proto_node) = &protonetwork[*upstream_protonode_index] else {
panic!("Upstream protonode index must not be dedeuplicated");
};
proto_node.stable_node_id
}
})
.collect::<Vec<_>>();
for (protonode_index, (value_connector, protonode_path)) in value_connectors.iter_mut().zip(protonode_paths.iter_mut()).enumerate().rev() {
let callers = calling_protonodes.get(&protonode_index).unwrap();
let &(min_protonode_index, input_index) = callers.iter().min().unwrap();
let protonode_id = protonetwork[min_protonode_index].stable_node_id;
if let Some(value_connector) = value_connector.take() {
value_connector_callers.push((value_connector, (protonode_id, input_index)));
}
if let Some(protonode_path) = protonode_path.take() {
protonode_callers.push((protonode_path, (protonode_id, input_index)));
for protonode_entry in &mut protonetwork {
if let ProtonodeEntry::Protonode(protonode) = protonode_entry {
if let Some((caller_path, caller_input_index)) = protonode.caller.as_ref() {
let caller_index = protonode_indices[caller_path];
match &mut protonode.construction_args {
ConstructionArgs::Value(node_value_args) => {
value_connector_callers.push((std::mem::take(&mut node_value_args.connector_paths), (calling_protonode_ids[caller_index], *caller_input_index)))
}
ConstructionArgs::Nodes(node_construction_args) => {
protonode_callers.push((std::mem::take(&mut node_construction_args.node_paths), (calling_protonode_ids[caller_index], *caller_input_index)))
}
ConstructionArgs::Inline(_) => todo!(),
}
}
}
}
let mut existing_ids = HashSet::new();
// Value nodes can be deduplicated if they share the same hash, since they do not depend on the input
let protonetwork = protonetwork
.into_iter()
.filter(|protonode| !(matches!(protonode.construction_args, ConstructionArgs::Value(_)) && !existing_ids.insert(protonode.stable_node_id)))
.collect();
Ok((protonetwork, value_connector_callers, protonode_callers))
log::debug!("protonetwork: {:?}", protonetwork);
Ok((ProtoNetwork::from_vec(protonetwork), value_connector_callers, protonode_callers))
}
fn get_input_from_absolute_connector(&mut self, traversal_input: &AbsoluteInputConnector) -> Option<&mut NodeInput> {
@@ -632,39 +709,32 @@ impl NodeNetwork {
}
}
}
// Performs a recursive graph traversal starting from all protonode inputs and the root export until reaching the next protonode or value input.
// Automatically inserts value nodes by moving the value from the current network
// Performs a recursive graph traversal starting from the root export across all node inputs
// Inserts values into the protonetwork by moving the value from the current network
//
// protonetwork - The topologically sorted flattened protonetwork. The caller of each protonode is at a lower index. The output of the network is the first protonode
//
// calling protonodes - anytime a protonode is reached, the caller is added as a value with (caller protonetwork index, caller input index).
// This is necessary so the calling protonodes input can be looked up and mapped when generating SNI's
// None indicates that the caller is the traversal start, which is skipped
//
// Protonode indices - mapping of protonode path to its index in the protonetwork, updated when inserting a protonode
//
// Traversal input - current connector to traverse over. added to downstream_calling_inputs every time the function is called.
//
// downstream_calling_inputs - tracks all inputs reached during traversal
//
// any_input_to_downstream_protonode_input - used by the runtime/javascript to get the calling protonode input from any input connector.
// When a protonode is reached, each input connector in downstream_calling_inputs, is looked up in `any_input_to_downstream_protonode_input`. If there is an entry,
// Then the paths are compared, and the greater one is chosen using stable ordering.
// This is to ensure a constant mapping, since an export for instance can have multiple calling nodes in the parent network
//
// any_input_to_upstream_protonode - used by the runtime to get the node to evaluate for any given input connector.
// Each input connector is inserted into any_input_to_upstream_protonode with the value being the path to the reached protonode.
// It doesnt matter if its overwritten since it must have previously pointed to the same protonode anyways
//
pub fn traverse_input(
&mut self,
protonetwork: &mut Vec<ProtoNode>, // Flattened node id to protonode, stable node ids can only be generated once the network is fully flattened, since it runs in reverse
value_connector: &mut Vec<Option<AbsoluteInputConnector>>,
protonode_path: &mut Vec<Option<ProtonodePath>>,
calling_protonodes: &mut HashMap<usize, Vec<(usize, usize)>>, // A mapping of protonode path to all (flattened network indices, their input index) that called the protonode, used during SNI generation to remap inputs
protonode_indices: &mut HashMap<Vec<SNI>, usize>, // Mapping of protonode path to its index in the flattened protonetwork
protonetwork: &mut Vec<ProtonodeEntry>, // None represents a deduplicated value node
// Every time a value input is reached, it is added to a mapping so if it reached again, it can be moved to the end of the protonetwork
value_protonode_indices: &mut HashMap<AbsoluteInputConnector, usize>,
// Every time a protonode is reached, is it added to a mapping so if it reached again, it can be moved to the end of the protonetwork
protonode_indices: &mut HashMap<ProtonodePath, usize>,
// The original location of the current traversal
traversal_input: AbsoluteInputConnector,
// Protonode index, input index
traversal_start: (usize, usize),
// The protnode input which started the traversal. None if it is called from the root export
traversal_start: Option<(ProtonodePath, usize)>,
) {
let network_path = &traversal_input.network_path;
@@ -730,90 +800,111 @@ impl NodeNetwork {
network_path: upstream_node_path.clone(),
connector: InputConnector::Export(output_index),
};
self.traverse_input(protonetwork, value_connector, protonode_path, calling_protonodes, protonode_indices, traversal_input, traversal_start);
self.traverse_input(protonetwork, value_protonode_indices, protonode_indices, traversal_input, traversal_start);
}
DocumentNodeImplementation::ProtoNode(protonode_id) => {
// Only insert the protonode if it has not previously been inserted
// Do not insert the protonode into the proto network or traverse over inputs if its already visited
let reached_protonode_index = match protonode_indices.get(&upstream_node_path) {
// The protonode has already been inserted, return its index
Some(reached_protonode_index) => *reached_protonode_index,
// Insert the protonode and traverse over inputs
None => {
let construction_args = ConstructionArgs::Nodes(NodeConstructionArgs {
identifier: protonode_id.clone(),
inputs: vec![NodeId(0); upstream_document_node.inputs.len()],
});
let protonode = ProtoNode {
construction_args,
// All protonodes take Context by default
input: concrete!(Context),
original_location: OriginalLocation {
protonode_path: upstream_node_path.clone().into(),
send_types_to_editor: true,
},
stable_node_id: NodeId(0),
// Check if the protonode has already been reached
let reached_protonode = match protonode_indices.get(&upstream_node_path) {
// The protonode has already been inserted, add the caller and node path to its metadata
Some(previous_protonode_index) => {
let ProtonodeEntry::Protonode(protonode) = &mut protonetwork[*previous_protonode_index] else {
panic!("Previously inserted protonode must exist at mapped protonode index");
};
let new_protonode_index = protonetwork.len();
protonode_indices.insert(upstream_node_path.clone(), new_protonode_index);
protonetwork.push(protonode);
value_connector.push(None);
protonode_path.push(Some(upstream_node_path.into_boxed_slice()));
// Iterate over all upstream inputs, which will map the inputs to the index of the connected protonode
protonode
}
// Construct the protonode and traverse over inputs
None => {
let number_of_inputs = upstream_document_node.inputs.len();
let identifier = protonode_id.clone();
for input_index in 0..upstream_document_node.inputs.len() {
self.traverse_input(
protonetwork,
value_connector,
protonode_path,
calling_protonodes,
value_protonode_indices,
protonode_indices,
AbsoluteInputConnector {
network_path: network_path.clone(),
connector: InputConnector::node(upstream_node_id, input_index),
},
(new_protonode_index, input_index),
Some((upstream_node_path.clone(), input_index)),
);
}
new_protonode_index
let context_dependencies = NODE_CONTEXT_DEPENDENCY.lock().unwrap().get(identifier.name.as_ref()).cloned().unwrap_or_default();
let construction_args = ConstructionArgs::Nodes(NodeConstructionArgs {
identifier,
inputs: vec![None; number_of_inputs],
context_dependencies,
node_paths: Vec::new(),
});
let protonode = ProtoNode {
construction_args,
// All protonodes take Context by default
input: concrete!(Context),
stable_node_id: NodeId(0),
callers: Vec::new(),
caller: None,
};
let new_protonode_index = protonetwork.len();
protonetwork.push(ProtonodeEntry::Protonode(protonode));
protonode_indices.insert(upstream_node_path.clone(), new_protonode_index);
let ProtonodeEntry::Protonode(protonode) = &mut protonetwork[new_protonode_index] else {
panic!("Inserted protonode must exist at new_protonode_index");
};
protonode
}
};
calling_protonodes.entry(reached_protonode_index).or_insert_with(Vec::new).push(traversal_start);
// Only add the traversal start if it is not the root export
if let Some(traversal_start) = traversal_start {
reached_protonode.callers.push(traversal_start);
}
let ConstructionArgs::Nodes(args) = &mut reached_protonode.construction_args else {
panic!("Reached protonode must have Nodes construction args");
};
args.node_paths.push(upstream_node_path);
}
DocumentNodeImplementation::Extract => todo!(),
}
}
NodeInput::Value { tagged_value, .. } => {
// Deduplication of value nodes based on their tagged value, since they do not depend on the Context
//
use std::hash::Hasher;
let mut hasher = rustc_hash::FxHasher::default();
tagged_value.hash(&mut hasher);
let value_node_path = vec![NodeId(hasher.finish())];
// Only insert the value protonode if it has not previously been inserted
let value_protonode_index = match protonode_indices.get(&value_node_path) {
// The value input has already been inserted, return it the existing value nodes index
Some(value_protonode_index) => *value_protonode_index,
// Check if the protonode has already been reached
let reached_protonode = match value_protonode_indices.get(&traversal_input) {
// The protonode has already been inserted, add the caller and node path to its metadata
Some(previous_protonode_index) => {
let ProtonodeEntry::Protonode(protonode) = &mut protonetwork[*previous_protonode_index] else {
panic!("Previously inserted protonode must exist at mapped protonode index");
};
protonode
}
// Insert the protonode and traverse over inputs
None => {
let protonode = ProtoNode {
construction_args: ConstructionArgs::Value(std::mem::replace(tagged_value, TaggedValue::None.into())),
let value_protonode = ProtoNode {
construction_args: ConstructionArgs::Value(NodeValueArgs {
value: std::mem::replace(tagged_value, TaggedValue::None.into()),
connector_paths: Vec::new(),
}),
input: concrete!(Context), // Could be ()
original_location: OriginalLocation {
protonode_path: Vec::new().into(),
send_types_to_editor: false,
},
stable_node_id: NodeId(0),
callers: Vec::new(),
caller: None,
};
let new_protonode_index = protonetwork.len();
protonode_indices.insert(value_node_path.clone(), new_protonode_index);
protonetwork.push(protonode);
value_connector.push(Some(traversal_input));
protonode_path.push(None);
new_protonode_index
protonetwork.push(ProtonodeEntry::Protonode(value_protonode));
value_protonode_indices.insert(traversal_input.clone(), new_protonode_index);
let ProtonodeEntry::Protonode(protonode) = &mut protonetwork[new_protonode_index] else {
panic!("Previously inserted protonode must exist at mapped protonode index");
};
protonode
}
};
calling_protonodes.entry(value_protonode_index).or_insert_with(Vec::new).push(traversal_start);
// Only add the traversal start if it is not the root export
if let Some(traversal_start) = traversal_start {
reached_protonode.callers.push(traversal_start);
}
let ConstructionArgs::Value(args) = &mut reached_protonode.construction_args else {
panic!("Reached protonode must have Nodes construction args");
};
args.connector_paths.push(traversal_input);
}
// Continue traversal
NodeInput::Network { import_index, .. } => {
@@ -823,35 +914,14 @@ impl NodeNetwork {
network_path: encapsulating_network_path,
connector: InputConnector::node(node_id, *import_index),
};
self.traverse_input(protonetwork, value_connector, protonode_path, calling_protonodes, protonode_indices, traversal_input, traversal_start);
self.traverse_input(protonetwork, value_protonode_indices, protonode_indices, traversal_input, traversal_start);
}
NodeInput::Scope(_cow) => unreachable!(),
NodeInput::Reflection(_document_node_metadata) => unreachable!(),
NodeInput::Inline(_inline_rust) => todo!(),
NodeInput::Scope(_) => unreachable!(),
NodeInput::Reflection(_) => unreachable!(),
NodeInput::Inline(_) => todo!(),
}
}
// pub fn collect_downstream_metadata(
// reached_protonode_index: usize,
// calling_protonodes: &mut HashMap<usize, Vec<(usize, usize)>>,
// protonode_indices: &mut HashMap<Vec<SNI>, usize>,
// downstream_calling_inputs: Vec<AbsoluteInputConnector>,
// ) {
// // Map the first downstream calling node input (which is traversed for every node input) to the reached protonode
// let downstream_protonode_caller = downstream_calling_inputs[0].clone();
// match &downstream_protonode_caller.connector {
// InputConnector::Node { node_id, input_index } => {
// // The calling protonode has already been added to the flattened network, so it can be looked up by index and the reached node can be mapped to it
// let mut calling_protonode_path = downstream_protonode_caller.network_path.clone();
// calling_protonode_path.push(*node_id);
// let calling_protonode_index = protonode_indices[&calling_protonode_path];
// }
// InputConnector::Export(_) => {}
// }
// }
/// Converts the `DocumentNode`s with a `DocumentNodeImplementation::Extract` into a `ClonedNode` that returns
/// the `DocumentNode` specified by the single `NodeInput::Node`.
/// The referenced node is removed from the network, and any `NodeInput::Node`s used by the referenced node are replaced with a generically typed network input.
@@ -898,11 +968,17 @@ impl NodeNetwork {
}
#[derive(Debug)]
pub enum ProtonodeEntry {
Protonode(ProtoNode),
// If deduplicated, then any upstream node which this node previously called needs to map to the new protonode
Deduplicated(usize),
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct CompilationMetadata {
// Stored for every value input in the compiled network
pub protonode_callers_for_value: Vec<(AbsoluteInputConnector, CompiledProtonodeInput)>,
pub protonode_caller_for_values: Vec<(Vec<AbsoluteInputConnector>, CompiledProtonodeInput)>,
// Stored for every protonode in the compiled network
pub protonode_callers_for_node: Vec<(ProtonodePath, CompiledProtonodeInput)>,
pub protonode_caller_for_nodes: Vec<(Vec<ProtonodePath>, CompiledProtonodeInput)>,
pub types_to_add: Vec<(SNI, Vec<Type>)>,
pub types_to_remove: Vec<(SNI, usize)>,
}
@@ -970,7 +1046,7 @@ pub struct AbsoluteOutputConnector {
}
/// Represents an output connector
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum OutputConnector {
#[serde(rename = "node")]
Node {
+94 -22
View File
@@ -1,18 +1,18 @@
use super::DocumentNode;
use crate::proto::{Any as DAny, FutureAny};
use crate::wasm_application_io::WasmEditorApi;
use crate::wasm_application_io::WasmApplicationIoValue;
use dyn_any::DynAny;
pub use dyn_any::StaticType;
pub use glam::{DAffine2, DVec2, IVec2, UVec2};
use graphene_application_io::SurfaceFrame;
use graphene_brush::brush_cache::BrushCache;
use graphene_brush::brush_stroke::BrushStroke;
use graphene_core::raster_types::CPU;
use graphene_core::raster_types::{CPU, GPU};
use graphene_core::transform::ReferencePoint;
use graphene_core::uuid::NodeId;
use graphene_core::vector::style::Fill;
use graphene_core::{Color, MemoHash, Node, Type};
use graphene_svg_renderer::RenderMetadata;
use graphene_svg_renderer::{GraphicElementRendered, RenderMetadata};
use std::fmt::Display;
use std::hash::Hash;
use std::marker::PhantomData;
@@ -32,10 +32,9 @@ macro_rules! tagged_value {
$( $(#[$meta] ) *$identifier( $ty ), )*
RenderOutput(RenderOutput),
SurfaceFrame(SurfaceFrame),
#[serde(skip)]
EditorApi(Arc<WasmEditorApi>)
}
// We must manually implement hashing because some values are floats and so do not reproducibly hash (see FakeHash below)
#[allow(clippy::derived_hash_with_manual_eq)]
impl Hash for TaggedValue {
@@ -46,7 +45,6 @@ macro_rules! tagged_value {
$( Self::$identifier(x) => {x.hash(state)}),*
Self::RenderOutput(x) => x.hash(state),
Self::SurfaceFrame(x) => x.hash(state),
Self::EditorApi(x) => x.hash(state),
}
}
}
@@ -58,7 +56,6 @@ macro_rules! tagged_value {
$( Self::$identifier(x) => Box::new(x), )*
Self::RenderOutput(x) => Box::new(x),
Self::SurfaceFrame(x) => Box::new(x),
Self::EditorApi(x) => Box::new(x),
}
}
/// Converts to a Arc<dyn Any + Send + Sync + 'static>
@@ -68,7 +65,6 @@ macro_rules! tagged_value {
$( Self::$identifier(x) => Arc::new(x), )*
Self::RenderOutput(x) => Arc::new(x),
Self::SurfaceFrame(x) => Arc::new(x),
Self::EditorApi(x) => Arc::new(x),
}
}
/// Creates a graphene_core::Type::Concrete(TypeDescriptor { .. }) with the type of the value inside the tagged value
@@ -78,7 +74,6 @@ macro_rules! tagged_value {
$( Self::$identifier(_) => concrete!($ty), )*
Self::RenderOutput(_) => concrete!(RenderOutput),
Self::SurfaceFrame(_) => concrete!(SurfaceFrame),
Self::EditorApi(_) => concrete!(&WasmEditorApi)
}
}
/// Attempts to downcast the dynamic type to a tagged value
@@ -115,7 +110,6 @@ macro_rules! tagged_value {
$(TaggedValue::$identifier(value) => {any.downcast_ref::<$ty>().map_or(false, |v| v==value)}, )*
TaggedValue::RenderOutput(value) => any.downcast_ref::<RenderOutput>().map_or(false, |v| v==value),
TaggedValue::SurfaceFrame(value) => any.downcast_ref::<SurfaceFrame>().map_or(false, |v| v==value),
TaggedValue::EditorApi(value) => any.downcast_ref::<Arc<WasmEditorApi>>().map_or(false, |v| v==value),
}
}
pub fn from_type(input: &Type) -> Option<Self> {
@@ -258,6 +252,9 @@ tagged_value! {
ReferencePoint(graphene_core::transform::ReferencePoint),
CentroidType(graphene_core::vector::misc::CentroidType),
BooleanOperation(graphene_path_bool::BooleanOperation),
EditorMetadata(EditorMetadata),
#[serde(skip)]
ApplicationIo(Arc<WasmApplicationIoValue>),
}
impl TaggedValue {
@@ -382,18 +379,6 @@ impl TaggedValue {
_ => panic!("Passed value is not of type u32"),
}
}
pub fn as_renderable<'a>(value: &'a TaggedValue) -> Option<&'a dyn graphene_svg_renderer::GraphicElementRendered> {
match value {
TaggedValue::VectorData(v) => Some(v),
TaggedValue::RasterData(r) => Some(r),
TaggedValue::GraphicElement(e) => Some(e),
TaggedValue::GraphicGroup(g) => Some(g),
TaggedValue::ArtboardGroup(a) => Some(a),
TaggedValue::Artboard(a) => Some(a),
_ => None,
}
}
}
impl Display for TaggedValue {
@@ -441,6 +426,8 @@ impl<T: AsRef<U> + Sync + Send, U: Sync + Send> UpcastAsRefNode<T, U> {
}
}
#[derive(Debug, Clone, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)]
pub struct RenderOutput {
pub data: RenderOutputType,
@@ -460,6 +447,47 @@ impl Hash for RenderOutput {
}
}
impl Default for RenderOutput {
fn default() -> Self {
RenderOutput {
data: RenderOutputType::Image(Vec::new()),
metadata: RenderMetadata::default(),
}
}
}
// Passed as a scope input
#[derive(Clone, Debug, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub struct EditorMetadata {
// pub imaginate_hostname: String,
pub use_vello: bool,
pub hide_artboards: bool,
// If exporting, hide the artboard name and do not collect metadata
pub for_export: bool,
pub view_mode: graphene_core::vector::style::ViewMode,
pub transform_to_viewport: bool,
}
unsafe impl dyn_any::StaticType for EditorMetadata {
type Static = EditorMetadata;
}
impl Default for EditorMetadata {
fn default() -> Self {
Self {
// imaginate_hostname: "http://localhost:7860/".into(),
#[cfg(target_arch = "wasm32")]
use_vello: false,
#[cfg(not(target_arch = "wasm32"))]
use_vello: true,
hide_artboards: false,
for_export: false,
view_mode: graphene_core::vector::style::ViewMode::Normal,
transform_to_viewport: true,
}
}
}
/// We hash the floats and so-forth despite it not being reproducible because all inputs to the node graph must be hashed otherwise the graph execution breaks (so sorry about this hack)
trait FakeHash {
fn hash<H: core::hash::Hasher>(&self, state: &mut H);
@@ -509,3 +537,47 @@ mod fake_hash {
}
}
}
macro_rules! thumbnail_render {
( $( $ty:ty ),* $(,)? ) => {
pub fn render_thumbnail_if_change(new_value: &Arc<dyn std::any::Any + Send + Sync>, old_value: Option<&Arc<dyn std::any::Any + Send + Sync>>) -> ThumbnailRenderResult {
$(
if let Some(new_value) = new_value.downcast_ref::<$ty>() {
match old_value {
None => return ThumbnailRenderResult::UpdateThumbnail(new_value.render_thumbnail()),
Some(old_value) => {
if let Some(old_value) = old_value.downcast_ref::<$ty>() {
match new_value == old_value {
true => return ThumbnailRenderResult::NoChange,
false => return ThumbnailRenderResult::UpdateThumbnail(new_value.render_thumbnail())
}
} else {
return ThumbnailRenderResult::UpdateThumbnail(new_value.render_thumbnail())
}
},
}
}
)*
return ThumbnailRenderResult::ClearThumbnail;
}
};
}
thumbnail_render! {
graphene_core::GraphicGroupTable,
graphene_core::vector::VectorDataTable,
graphene_core::Artboard,
graphene_core::ArtboardGroupTable,
graphene_core::raster_types::RasterDataTable<CPU>,
graphene_core::raster_types::RasterDataTable<GPU>,
graphene_core::GraphicElement,
Option<Color>,
Vec<Color>,
}
pub enum ThumbnailRenderResult {
NoChange,
// Cleared if there is an error or the data could not be rendered
ClearThumbnail,
UpdateThumbnail(String),
}
+124 -87
View File
@@ -1,4 +1,4 @@
use crate::document::{InlineRust, value};
use crate::document::{AbsoluteInputConnector, InlineRust, ProtonodeEntry, value};
pub use graphene_core::registry::*;
use graphene_core::uuid::{NodeId, ProtonodePath, SNI};
use graphene_core::*;
@@ -6,18 +6,43 @@ use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::Hash;
use std::ops::Deref;
// #[derive(Debug, Default, PartialEq, Clone, Hash, Eq, serde::Serialize, serde::Deserialize)]
// /// A list of [`ProtoNode`]s, which is an intermediate step between the [`crate::document::NodeNetwork`] and the `BorrowTree` containing a single flattened network.
// pub struct ProtoNetwork {
// // TODO: remove this since it seems to be unused?
// // Should a proto Network even allow inputs? Don't think so
// pub inputs: Vec<NodeId>,
// /// The node ID that provides the output. This node is then responsible for calling the rest of the graph.
// pub output: NodeId,
// /// A list of nodes stored in a Vec to allow for sorting.
// pub nodes: Vec<(NodeId, ProtoNode)>,
// }
#[derive(Debug, Default)]
/// A list of [`ProtoNode`]s, which is an intermediate step between the [`crate::document::NodeNetwork`] and the `BorrowTree` containing a single flattened network.
pub struct ProtoNetwork {
/// A list of nodes stored in a Vec to allow for sorting.
nodes: Vec<ProtonodeEntry>,
/// The most downstream node in the protonetwork
pub output: NodeId,
}
impl ProtoNetwork {
pub fn from_vec(nodes: Vec<ProtonodeEntry>) -> Self {
let last_entry = nodes.last().expect("Cannot compile empty protonetwork");
let output = match last_entry {
ProtonodeEntry::Protonode(proto_node) => proto_node.stable_node_id,
ProtonodeEntry::Deduplicated(deduplicated_index) => {
let ProtonodeEntry::Protonode(protonode) = &nodes[*deduplicated_index] else {
panic!("Deduplicated protonode must point to valid protonode");
};
protonode.stable_node_id
}
};
ProtoNetwork { nodes, output }
}
pub fn nodes(&self) -> impl Iterator<Item = &ProtoNode> {
self.nodes
.iter()
.filter_map(|entry| if let ProtonodeEntry::Protonode(protonode) = entry { Some(protonode) } else { None })
}
pub fn into_nodes(self) -> impl Iterator<Item = ProtoNode> {
self.nodes
.into_iter()
.filter_map(|entry| if let ProtonodeEntry::Protonode(protonode) = entry { Some(protonode) } else { None })
}
}
// impl core::fmt::Display for ProtoNetwork {
// fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
@@ -69,59 +94,57 @@ use std::hash::Hash;
// }
// }
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
#[derive(Clone, Debug)]
pub struct UpstreamInputMetadata {
pub input_sni: SNI,
// Context dependencies are accumulated during compilation, then replaced with whatever needs to be nullified
// If None, then the upstream node is a value node, so replace with an empty vec
pub context_dependencies: Option<Vec<ContextDependency>>,
}
#[derive(Debug, Clone)]
pub struct NodeConstructionArgs {
// Used to get the constructor from the function in `node_registry.rs`.
pub identifier: ProtoNodeIdentifier,
/// A list of stable node ids used as inputs to the constructor
pub inputs: Vec<SNI>,
// A node is dependent on whatever is marked in its implementation, as well as all inputs
// If a node is dependent on more than its input, then a context nullification node is placed on the input
// Starts as None, and is populated during stable node id generation
pub inputs: Vec<Option<UpstreamInputMetadata>>,
// The union of all input context dependencies and the nodes context dependency. Used to generate the context nullification for the editor entry point
pub context_dependencies: Vec<ContextDependency>,
// Stores the path of document nodes which correspond to it
pub node_paths: Vec<ProtonodePath>,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone)]
pub struct NodeValueArgs {
/// A value of a type that is known, allowing serialization (serde::Deserialize is not object safe)
/// Also stores its caller inputs, which is used to map the rendered thumbnail to the wire input
pub value: MemoHash<value::TaggedValue>,
// Stores all absolute input connectors which correspond to this value.
pub connector_paths: Vec<AbsoluteInputConnector>,
}
#[derive(Debug, Clone)]
/// Defines the arguments used to construct the boxed node struct. This is used to call the constructor function in the `node_registry.rs` file - which is hidden behind a wall of macros.
pub enum ConstructionArgs {
/// A value of a type that is known, allowing serialization (serde::Deserialize is not object safe)
Value(MemoHash<value::TaggedValue>),
Value(NodeValueArgs),
Nodes(NodeConstructionArgs),
/// Used for GPU computation to work around the limitations of rust-gpu.
Inline(InlineRust),
}
impl Eq for ConstructionArgs {}
impl Hash for ConstructionArgs {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
core::mem::discriminant(self).hash(state);
match self {
Self::Nodes(nodes) => {
for node in &nodes.inputs {
node.hash(state);
}
}
Self::Value(value) => value.hash(state),
Self::Inline(inline) => inline.hash(state),
}
}
}
impl ConstructionArgs {
// TODO: what? Used in the gpu_compiler crate for something.
pub fn new_function_args(&self) -> Vec<String> {
match self {
ConstructionArgs::Nodes(nodes) => nodes.inputs.iter().map(|n| format!("n{:0x}", n.0)).collect(),
ConstructionArgs::Value(value) => vec![value.to_primitive_string()],
ConstructionArgs::Inline(inline) => vec![inline.expr.clone()],
}
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct OriginalLocation {
/// The original location to the document node - e.g. [grandparent_id, parent_id, node_id].
pub protonode_path: ProtonodePath,
// // Types should not be sent for autogenerated nodes or value nodes, which are not visible and inserted during compilation
pub send_types_to_editor: bool,
}
// impl ConstructionArgs {
// // TODO: what? Used in the gpu_compiler crate for something.
// pub fn new_function_args(&self) -> Vec<String> {
// match self {
// ConstructionArgs::Nodes(nodes) => nodes.inputs.iter().map(|n| format!("n{:0x}", n.0)).collect(),
// ConstructionArgs::Value(value) => vec![value.to_primitive_string()],
// ConstructionArgs::Inline(inline) => vec![inline.expr.clone()],
// }
// }
// }
#[derive(Debug, Clone)]
/// A proto node is an intermediate step between the `DocumentNode` and the boxed struct that actually runs the node (found in the [`BorrowTree`]).
@@ -130,43 +153,61 @@ pub struct OriginalLocation {
pub struct ProtoNode {
pub construction_args: ConstructionArgs,
pub input: Type,
pub original_location: OriginalLocation,
pub stable_node_id: SNI,
// Each protonode stores the path and input index of the protonodes which called it
pub callers: Vec<(ProtonodePath, usize)>,
// Each protonode will finally store a single caller (the minimum of all callers), used by the editor
pub caller: Option<(ProtonodePath, usize)>,
}
impl Default for ProtoNode {
fn default() -> Self {
Self {
construction_args: ConstructionArgs::Value(value::TaggedValue::U32(0).into()),
construction_args: ConstructionArgs::Value(NodeValueArgs {
value: value::TaggedValue::U32(0).into(),
connector_paths: Vec::new(),
}),
input: concrete!(Context),
original_location: Default::default(),
stable_node_id: NodeId(0),
callers: Vec::new(),
caller: None,
}
}
}
impl ProtoNode {
/// Construct a new [`ProtoNode`] with the specified construction args and a `ClonedNode` implementation.
pub fn value(value: ConstructionArgs, path: Vec<NodeId>, stable_node_id: SNI) -> Self {
let inputs_exposed = match &value {
ConstructionArgs::Nodes(nodes) => nodes.inputs.len() + 1,
_ => 2,
};
pub fn value(value: ConstructionArgs, stable_node_id: SNI) -> Self {
Self {
construction_args: value,
input: concrete!(Context),
original_location: OriginalLocation {
protonode_path: path.into(),
send_types_to_editor: false,
},
stable_node_id,
callers: Vec::new(),
caller: None,
}
}
// Hashes the inputs and implementation of non value nodes, and the value for value nodes
pub fn generate_stable_node_id(&mut self) {
use std::hash::Hasher;
let mut hasher = rustc_hash::FxHasher::default();
match &self.construction_args {
ConstructionArgs::Nodes(nodes) => {
for upstream_input in &nodes.inputs {
upstream_input.as_ref().unwrap().input_sni.hash(&mut hasher);
}
nodes.identifier.hash(&mut hasher);
}
ConstructionArgs::Value(value) => value.value.hash(&mut hasher),
ConstructionArgs::Inline(_) => todo!(),
}
self.stable_node_id = NodeId(hasher.finish());
}
}
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum GraphErrorType {
NodeNotFound(NodeId),
InputNodeNotFound(NodeId),
UnexpectedGenerics { index: usize, inputs: Vec<Type> },
NoImplementations,
@@ -178,7 +219,6 @@ impl Debug for GraphErrorType {
// TODO: format with the document graph context so the input index is the same as in the graph UI.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GraphErrorType::NodeNotFound(id) => write!(f, "Input node {id} is not present in the typing context"),
GraphErrorType::InputNodeNotFound(id) => write!(f, "Input node {id} is not present in the typing context"),
GraphErrorType::UnexpectedGenerics { index, inputs } => write!(f, "Generic inputs should not exist but found at {index}: {inputs:?}"),
GraphErrorType::NoImplementations => write!(f, "No implementations found"),
@@ -222,7 +262,7 @@ impl Debug for GraphErrorType {
}
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct GraphError {
pub node_path: Vec<NodeId>,
pub stable_node_id: SNI,
pub identifier: Cow<'static, str>,
pub error: GraphErrorType,
}
@@ -231,11 +271,11 @@ impl GraphError {
let identifier = match &node.construction_args {
ConstructionArgs::Nodes(node_construction_args) => node_construction_args.identifier.name.clone(),
// Values are inserted into upcast nodes
ConstructionArgs::Value(memo_hash) => "Value Node".into(),
ConstructionArgs::Inline(inline_rust) => "Inline".into(),
ConstructionArgs::Value(node_value_args) => format!("{:?} Value Node", node_value_args.value.deref().ty()).into(),
ConstructionArgs::Inline(_) => "Inline".into(),
};
Self {
node_path: node.original_location.protonode_path.to_vec(),
stable_node_id: node.stable_node_id,
identifier,
error: text.into(),
}
@@ -243,11 +283,7 @@ impl GraphError {
}
impl Debug for GraphError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NodeGraphError")
.field("path", &self.node_path.iter().map(|id| id.0).collect::<Vec<_>>())
.field("identifier", &self.identifier.to_string())
.field("error", &self.error)
.finish()
f.debug_struct("NodeGraphError").field("identifier", &self.identifier.to_string()).field("error", &self.error).finish()
}
}
pub type GraphErrors = Vec<GraphError>;
@@ -256,17 +292,17 @@ pub type GraphErrors = Vec<GraphError>;
#[derive(Default, Clone, dyn_any::DynAny)]
pub struct TypingContext {
lookup: Cow<'static, HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>>>,
monitor_lookup: Cow<'static, HashMap<Type, MonitorConstructor>>,
cache_lookup: Cow<'static, HashMap<Type, CacheConstructor>>,
inferred: HashMap<NodeId, NodeIOTypes>,
constructor: HashMap<NodeId, NodeConstructor>,
}
impl TypingContext {
/// Creates a new `TypingContext` with the given lookup table.
pub fn new(lookup: &'static HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>>, monitor_lookup: &'static HashMap<Type, MonitorConstructor>) -> Self {
pub fn new(lookup: &'static HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>>, cache_lookup: &'static HashMap<Type, CacheConstructor>) -> Self {
Self {
lookup: Cow::Borrowed(lookup),
monitor_lookup: Cow::Borrowed(monitor_lookup),
cache_lookup: Cow::Borrowed(cache_lookup),
..Default::default()
}
}
@@ -274,9 +310,9 @@ impl TypingContext {
/// Updates the `TypingContext` with a given proto network. This will infer the types of the nodes
/// and store them in the `inferred` field. The proto network has to be topologically sorted
/// and contain fully resolved stable node ids.
pub fn update(&mut self, network: &Vec<ProtoNode>) -> Result<(), GraphErrors> {
pub fn update(&mut self, network: &ProtoNetwork) -> Result<(), GraphErrors> {
// Update types from the most upstream nodes first
for node in network.iter().rev() {
for node in network.nodes() {
self.infer(node.stable_node_id, node)?;
}
Ok(())
@@ -292,9 +328,9 @@ impl TypingContext {
self.constructor.get(&node_id).copied()
}
// Returns the monitor node constructor for a given type {
pub fn monitor_constructor(&self, monitor_type: &Type) -> Option<MonitorConstructor> {
self.monitor_lookup.get(monitor_type).copied()
// Returns the cache node constructor for a given type {
pub fn cache_constructor(&self, cache_type: &Type) -> Option<CacheConstructor> {
self.cache_lookup.get(cache_type).copied()
}
/// Returns the type of a given node id if it exists
@@ -314,7 +350,7 @@ impl TypingContext {
ConstructionArgs::Value(ref v) => {
// assert!(matches!(node.input, ProtoNodeInput::None) || matches!(node.input, ProtoNodeInput::ManualComposition(ref x) if x == &concrete!(Context)));
// TODO: This should return a reference to the value
let types = NodeIOTypes::new(concrete!(Context), Type::Future(Box::new(v.ty())), vec![]);
let types = NodeIOTypes::new(concrete!(Context), Type::Future(Box::new(v.value.ty())), vec![]);
self.inferred.insert(node_id, types.clone());
return Ok(types);
}
@@ -323,10 +359,11 @@ impl TypingContext {
let inputs = construction_args
.inputs
.iter()
.map(|id| id.as_ref().unwrap().input_sni)
.map(|id| {
self.inferred
.get(id)
.ok_or_else(|| vec![GraphError::new(node, GraphErrorType::NodeNotFound(*id))])
.get(&id)
.ok_or_else(|| vec![GraphError::new(node, GraphErrorType::InputNodeNotFound(id))])
.map(|node| node.ty())
})
.collect::<Result<Vec<Type>, GraphErrors>>()?;
@@ -1,8 +1,9 @@
use dyn_any::StaticType;
use graphene_application_io::{ApplicationError, ApplicationIo, ResourceFuture, SurfaceHandle, SurfaceId};
use graphene_application_io::{ApplicationError, ApplicationIo, ApplicationIoValue, ResourceFuture, SurfaceHandle, SurfaceId};
#[cfg(target_arch = "wasm32")]
use js_sys::{Object, Reflect};
use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Arc;
#[cfg(target_arch = "wasm32")]
use std::sync::atomic::AtomicU64;
@@ -56,6 +57,8 @@ unsafe impl Sync for WindowWrapper {}
#[cfg(target_arch = "wasm32")]
unsafe impl Send for WindowWrapper {}
pub type WasmApplicationIoValue = ApplicationIoValue<WasmApplicationIo>;
#[derive(Debug, Default)]
pub struct WasmApplicationIo {
#[cfg(target_arch = "wasm32")]
@@ -156,20 +159,12 @@ unsafe impl StaticType for WasmApplicationIo {
type Static = WasmApplicationIo;
}
impl<'a> From<&'a WasmEditorApi> for &'a WasmApplicationIo {
fn from(editor_api: &'a WasmEditorApi) -> Self {
editor_api.application_io.as_ref().unwrap()
}
}
#[cfg(feature = "wgpu")]
impl<'a> From<&'a WasmApplicationIo> for &'a WgpuExecutor {
fn from(app_io: &'a WasmApplicationIo) -> Self {
app_io.gpu_executor.as_ref().unwrap()
}
}
pub type WasmEditorApi = graphene_application_io::EditorApi<WasmApplicationIo>;
impl ApplicationIo for WasmApplicationIo {
#[cfg(target_arch = "wasm32")]
type Surface = HtmlCanvasElement;