mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 06:38:03 +08:00
381 lines
16 KiB
Rust
381 lines
16 KiB
Rust
use std::borrow::Cow;
|
|
use std::collections::HashMap;
|
|
|
|
use core_types::memo::MemoHash;
|
|
use core_types::uuid::NodeId as RuntimeNodeId;
|
|
use graph_craft::document::value::TaggedValue;
|
|
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput as GraphCraftNodeInput, NodeNetwork};
|
|
use graph_craft::{ProtoNodeIdentifier, Type, concrete};
|
|
use rustc_hash::{FxHashMap, FxHashSet};
|
|
|
|
use crate::attr::*;
|
|
use crate::metadata_source::{InputMetadataEntry, NetworkMetadataEntry, NodeMetadataEntry};
|
|
use crate::{AttributesRead, Implementation, NetworkId, Node, NodeId, NodeInput, Position, ProtoNode, ROOT_NETWORK, Registry, ResourceId};
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum ConversionError {
|
|
#[error("Network {0} not found")]
|
|
NetworkNotFound(NetworkId),
|
|
#[error("Node {0} not found")]
|
|
NodeNotFound(NodeId),
|
|
#[error("ProtoNode declaration {0} not found in provided declarations")]
|
|
DeclarationNotFound(ResourceId),
|
|
#[error("Deserialization error: {0}")]
|
|
DeserializationError(String),
|
|
#[error("Network {network} has two nodes mapping to runtime ID {runtime_id}")]
|
|
DuplicateRuntimeNodeId { network: NetworkId, runtime_id: u64 },
|
|
#[error("Network {network} references node {referenced}, which lives in a different network")]
|
|
CrossNetworkReference { network: NetworkId, referenced: NodeId },
|
|
#[error("Scope injection {key:?} in network {network} references node {referenced}, which is missing or in a different network")]
|
|
DanglingScopeInjection { network: NetworkId, key: String, referenced: NodeId },
|
|
#[error("Network {0} is reachable from itself through nested implementations, forming a cycle")]
|
|
CyclicNetwork(NetworkId),
|
|
}
|
|
|
|
/// Resolved proto-node declarations, keyed by the `ResourceId` that `Implementation::ProtoNode`
|
|
/// references. The caller resolves these from its byte store (`ResourceId` → `ResourceHash` →
|
|
/// stored `ProtoNode` bytes) before converting, since `document-graph-storage` holds only references.
|
|
pub type Declarations = std::collections::HashMap<ResourceId, ProtoNode>;
|
|
|
|
impl Registry {
|
|
/// Returns the network plus per-node metadata entries (one per node carrying any `ui::*` attribute).
|
|
pub fn to_runtime_with_metadata(&self, declarations: &Declarations) -> Result<(NodeNetwork, Vec<NodeMetadataEntry>), ConversionError> {
|
|
let (network, node_entries, _) = self.to_runtime_with_full_metadata(declarations)?;
|
|
Ok((network, node_entries))
|
|
}
|
|
|
|
/// Like `to_runtime_with_metadata` but also returns per-network entries (navigation, previewing).
|
|
/// Used by the editor's full-rebuild path.
|
|
pub fn to_runtime_with_full_metadata(&self, declarations: &Declarations) -> Result<(NodeNetwork, Vec<NodeMetadataEntry>, Vec<NetworkMetadataEntry>), ConversionError> {
|
|
let mut node_metadata = Some(Vec::new());
|
|
let mut network_metadata = Some(Vec::new());
|
|
|
|
// Group nodes by their owning network in one pass, so each `convert_network` call (one per
|
|
// network, including nested ones) takes its node list by lookup instead of rescanning the whole
|
|
// flat `node_instances` map, which would be quadratic on graphs with many networks.
|
|
let mut nodes_by_network: FxHashMap<NetworkId, Vec<(NodeId, &Node)>> = FxHashMap::default();
|
|
for (&global_id, node) in &self.node_instances {
|
|
nodes_by_network.entry(node.network).or_default().push((global_id, node));
|
|
}
|
|
|
|
let context = ConversionContext {
|
|
registry: self,
|
|
declarations,
|
|
nodes_by_network,
|
|
};
|
|
|
|
// Reject cycles up front so the recursive conversion below can assume the network reference
|
|
// graph is acyclic and never blow the stack on a self-referential `Implementation::Network`.
|
|
detect_network_cycle(&context, ROOT_NETWORK)?;
|
|
|
|
let network = convert_network(&context, ROOT_NETWORK, &[], &mut node_metadata, &mut network_metadata)?;
|
|
Ok((network, node_metadata.expect("seeded above"), network_metadata.expect("seeded above")))
|
|
}
|
|
|
|
/// Rebuild the runtime [`ResourceRegistry`](graphene_resource::ResourceRegistry) from the stored
|
|
/// `resources`. Each entry's source chain is restored in priority order (the chain is kept
|
|
/// sorted by key) with bodies decoded from their type-erased `serde_json::Value` form back to
|
|
/// `DataSource`; the resolved hash, if any, is restored last. Inverse of `convert_resources` in
|
|
/// `from_runtime`.
|
|
pub fn to_resource_registry(&self) -> Result<graphene_resource::ResourceRegistry, ConversionError> {
|
|
let mut registry = graphene_resource::ResourceRegistry::new();
|
|
|
|
for (id, entry) in &self.resources {
|
|
for (_, source) in &entry.sources {
|
|
let decoded: graphene_resource::DataSource = serde_json::from_value(source.source.clone()).map_err(|error| ConversionError::DeserializationError(error.to_string()))?;
|
|
registry.push_source_back(id, decoded);
|
|
}
|
|
if let Some(hash) = entry.hash {
|
|
registry.resolve(id, hash);
|
|
}
|
|
}
|
|
|
|
Ok(registry)
|
|
}
|
|
}
|
|
|
|
/// Immutable shared context threaded through the recursive conversion. `nodes_by_network` is the
|
|
/// one-pass grouping of `registry.node_instances` by owning network, so each network's nodes are an
|
|
/// O(1) lookup rather than a full rescan.
|
|
struct ConversionContext<'a> {
|
|
registry: &'a Registry,
|
|
declarations: &'a Declarations,
|
|
nodes_by_network: FxHashMap<NetworkId, Vec<(NodeId, &'a Node)>>,
|
|
}
|
|
|
|
/// Converts a single network. Recurses through `Implementation::Network` owning nodes.
|
|
///
|
|
/// **ID remapping:** Registry uses globally hashed IDs; runtime networks need local IDs. We pull
|
|
/// the original local ID from `attr::ORIGINAL_NODE_ID` on each node and on each `NodeInput::Node`
|
|
/// reference. References only point within the same network, so per-network lookup suffices.
|
|
///
|
|
/// **Exports:** the storage-side `Vec<ExportSlot>` is sparse (`None` slots are valid). Compacted
|
|
/// here into the runtime's dense `Vec<NodeInput>` — slot stability is a storage-side concern.
|
|
///
|
|
/// `metadata_path` is the owning-node chain naming *this* network (empty for the root).
|
|
/// Walk the network reference graph (edges are `Implementation::Network` references between a
|
|
/// network and the networks its nodes embed) and reject any cycle, so the recursive `convert_network`
|
|
/// can't recurse forever and overflow the stack. Iterative DFS with an explicit stack and a gray set
|
|
/// for the active path; a child already on the active path is a back edge, i.e. a cycle.
|
|
fn detect_network_cycle(context: &ConversionContext, root: NetworkId) -> Result<(), ConversionError> {
|
|
// Networks reachable from `root` that referenced networks, used by an embedded node, are pushed in
|
|
// reverse so the natural processing order matches a recursive walk. `Enter`/`Leave` frames let us
|
|
// maintain the gray (active-path) set with an explicit stack.
|
|
enum Frame {
|
|
Enter(NetworkId),
|
|
Leave(NetworkId),
|
|
}
|
|
|
|
let mut stack = vec![Frame::Enter(root)];
|
|
let mut on_path: FxHashSet<NetworkId> = FxHashSet::default();
|
|
let mut fully_explored: FxHashSet<NetworkId> = FxHashSet::default();
|
|
|
|
while let Some(frame) = stack.pop() {
|
|
match frame {
|
|
Frame::Leave(network_id) => {
|
|
on_path.remove(&network_id);
|
|
fully_explored.insert(network_id);
|
|
}
|
|
Frame::Enter(network_id) => {
|
|
if fully_explored.contains(&network_id) {
|
|
continue;
|
|
}
|
|
if !on_path.insert(network_id) {
|
|
return Err(ConversionError::CyclicNetwork(network_id));
|
|
}
|
|
|
|
stack.push(Frame::Leave(network_id));
|
|
|
|
for &(_, node) in context.nodes_by_network.get(&network_id).map(Vec::as_slice).unwrap_or_default() {
|
|
if let Implementation::Network(child) = node.implementation {
|
|
stack.push(Frame::Enter(child));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn convert_network(
|
|
context: &ConversionContext,
|
|
network_id: NetworkId,
|
|
metadata_path: &[RuntimeNodeId],
|
|
node_collector: &mut Option<Vec<NodeMetadataEntry>>,
|
|
network_collector: &mut Option<Vec<NetworkMetadataEntry>>,
|
|
) -> Result<NodeNetwork, ConversionError> {
|
|
let network = context.registry.networks.get(&network_id).ok_or(ConversionError::NetworkNotFound(network_id))?;
|
|
|
|
if let Some(collector) = network_collector.as_mut() {
|
|
collector.push(extract_network_metadata(&network.attributes, metadata_path, network_id));
|
|
}
|
|
|
|
let mut nodes: FxHashMap<RuntimeNodeId, DocumentNode> = FxHashMap::default();
|
|
for &(global_id, node) in context.nodes_by_network.get(&network_id).map(Vec::as_slice).unwrap_or_default() {
|
|
let local_id = node.attributes.get(node::ORIGINAL_NODE_ID).and_then(|v| v.value.as_u64()).unwrap_or(global_id.0);
|
|
let runtime_id = RuntimeNodeId(local_id);
|
|
|
|
if let Some(collector) = node_collector.as_mut()
|
|
&& let Some(entry) = extract_ui_metadata(node, metadata_path, runtime_id)
|
|
{
|
|
collector.push(entry);
|
|
}
|
|
|
|
let doc_node = convert_node(context, node, metadata_path, runtime_id, node_collector, network_collector)?;
|
|
|
|
// Two storage nodes resolving to the same runtime ID would silently collapse into one on
|
|
// insert, dropping a node from the reconstructed graph.
|
|
if nodes.insert(runtime_id, doc_node).is_some() {
|
|
return Err(ConversionError::DuplicateRuntimeNodeId {
|
|
network: network_id,
|
|
runtime_id: local_id,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Input attributes aren't round-tripped for exports — Reflection/Import inputs don't appear there.
|
|
let empty_attrs = crate::Attributes::new();
|
|
let exports: Vec<GraphCraftNodeInput> = network
|
|
.exports
|
|
.iter()
|
|
.filter_map(|slot| slot.target.as_ref())
|
|
.map(|input| convert_input(context.registry, network_id, input, &empty_attrs))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
let scope_injections = read_scope_injections(context.registry, network_id, &network.attributes)?;
|
|
|
|
Ok(NodeNetwork {
|
|
exports,
|
|
nodes,
|
|
scope_injections,
|
|
generated: false,
|
|
})
|
|
}
|
|
|
|
/// Rebuild a network's `scope_injections` from its serialized attribute blob, resolving each stored
|
|
/// storage node ID back to its runtime-local ID. Mirrors `from_runtime::write_scope_injections`.
|
|
fn read_scope_injections(registry: &Registry, network_id: NetworkId, attributes: &crate::Attributes) -> Result<FxHashMap<String, (RuntimeNodeId, Type)>, ConversionError> {
|
|
let Some(stored) = attributes.get_typed::<HashMap<String, (NodeId, Type)>>(network::SCOPE_INJECTIONS) else {
|
|
return Ok(FxHashMap::default());
|
|
};
|
|
|
|
stored
|
|
.into_iter()
|
|
.map(|(key, (storage_id, ty))| {
|
|
// The injection must point at a node in this same network, like any `NodeInput::Node`.
|
|
let referenced = registry.node_instances.get(&storage_id).filter(|node| node.network == network_id);
|
|
let Some(referenced) = referenced else {
|
|
return Err(ConversionError::DanglingScopeInjection {
|
|
network: network_id,
|
|
key,
|
|
referenced: storage_id,
|
|
});
|
|
};
|
|
|
|
let local_id = referenced.attributes.get(node::ORIGINAL_NODE_ID).and_then(|v| v.value.as_u64()).unwrap_or(storage_id.0);
|
|
Ok((key, (RuntimeNodeId(local_id), ty)))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Returns `None` when the node has no `ui::*` attributes at all so callers don't end up with
|
|
/// empty entries for unconverted-from-runtime nodes. `input_metadata` is always sized to match
|
|
/// `node.inputs.len()` for a strict slot-by-slot rebuild; empty slots use `InputMetadataEntry::default()`.
|
|
fn extract_ui_metadata(node: &crate::Node, network_path: &[RuntimeNodeId], local_id: RuntimeNodeId) -> Option<NodeMetadataEntry> {
|
|
let position: Option<Position> = node.attributes.get_typed(node::ui::POSITION);
|
|
let is_layer = node.attributes.get_or(node::ui::IS_LAYER, false);
|
|
let display_name: Option<String> = node.attributes.get_typed(node::ui::DISPLAY_NAME);
|
|
let locked = node.attributes.get_or(node::ui::LOCKED, false);
|
|
let pinned = node.attributes.get_or(node::ui::PINNED, false);
|
|
let output_names: Vec<String> = node.attributes.get_or_default(node::ui::OUTPUT_NAMES);
|
|
|
|
let input_metadata: Vec<InputMetadataEntry> = node.inputs.iter().map(|slot| &slot.attributes).map(extract_input_metadata).collect();
|
|
|
|
let entry = NodeMetadataEntry {
|
|
network_path: network_path.to_vec(),
|
|
local_id,
|
|
position,
|
|
is_layer,
|
|
display_name,
|
|
locked,
|
|
pinned,
|
|
input_metadata,
|
|
output_names,
|
|
};
|
|
(!entry.is_empty()).then_some(entry)
|
|
}
|
|
|
|
fn extract_network_metadata(attributes: &crate::Attributes, network_path: &[RuntimeNodeId], network_id: NetworkId) -> NetworkMetadataEntry {
|
|
NetworkMetadataEntry {
|
|
network_path: network_path.to_vec(),
|
|
network_id,
|
|
reference: attributes.get_typed(node::ui::REFERENCE),
|
|
}
|
|
}
|
|
|
|
/// Reassembles `input_data` by scanning every attribute under `ui::input_data::` and stripping the prefix.
|
|
fn extract_input_metadata(attributes: &crate::Attributes) -> InputMetadataEntry {
|
|
let input_data: HashMap<String, serde_json::Value> = attributes
|
|
.iter()
|
|
.filter_map(|(key, value)| key.strip_prefix(node::input::ui::DATA_PREFIX).map(|sub_key| (sub_key.to_owned(), value.value.clone())))
|
|
.collect();
|
|
|
|
InputMetadataEntry {
|
|
input_name: attributes.get_typed(node::input::ui::NAME),
|
|
input_description: attributes.get_typed(node::input::ui::DESCRIPTION),
|
|
widget_override: attributes.get_typed(node::input::ui::WIDGET_OVERRIDE),
|
|
input_data,
|
|
}
|
|
}
|
|
|
|
fn convert_node(
|
|
context: &ConversionContext,
|
|
node: &crate::Node,
|
|
metadata_path: &[RuntimeNodeId],
|
|
runtime_node_id: RuntimeNodeId,
|
|
node_collector: &mut Option<Vec<NodeMetadataEntry>>,
|
|
network_collector: &mut Option<Vec<NetworkMetadataEntry>>,
|
|
) -> Result<DocumentNode, ConversionError> {
|
|
let inputs = node
|
|
.inputs
|
|
.iter()
|
|
.map(|slot| convert_input(context.registry, node.network, &slot.input, &slot.attributes))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
// Defaults must match `DocumentNode::default()` (and the `set_if_not_default` calls in `from_runtime`).
|
|
Ok(DocumentNode {
|
|
inputs,
|
|
call_argument: node.attributes.get_or(node::CALL_ARGUMENT, concrete!(core_types::Context)),
|
|
implementation: convert_implementation(context, &node.implementation, metadata_path, runtime_node_id, node_collector, network_collector)?,
|
|
visible: node.attributes.get_or(node::VISIBLE, true),
|
|
skip_deduplication: node.attributes.get_or(node::SKIP_DEDUPLICATION, false),
|
|
// Regenerated during compilation; not stored.
|
|
context_features: Default::default(),
|
|
original_location: Default::default(),
|
|
})
|
|
}
|
|
|
|
fn convert_input(registry: &Registry, network_id: NetworkId, input: &NodeInput, input_attributes: &crate::Attributes) -> Result<GraphCraftNodeInput, ConversionError> {
|
|
Ok(match input {
|
|
NodeInput::Node { id: node_id, index: output_index } => {
|
|
let referenced = registry.node_instances.get(node_id).ok_or(ConversionError::NodeNotFound(*node_id))?;
|
|
|
|
// Runtime references are local to one network. A cross-network reference would remap to a
|
|
// local ID that doesn't exist in the current runtime network, so reject it.
|
|
if referenced.network != network_id {
|
|
return Err(ConversionError::CrossNetworkReference {
|
|
network: network_id,
|
|
referenced: *node_id,
|
|
});
|
|
}
|
|
|
|
let local_id = referenced.attributes.get(node::ORIGINAL_NODE_ID).and_then(|v| v.value.as_u64()).unwrap_or(node_id.0);
|
|
GraphCraftNodeInput::Node {
|
|
node_id: RuntimeNodeId(local_id),
|
|
output_index: *output_index as usize,
|
|
}
|
|
}
|
|
NodeInput::Value { value, exposed } => {
|
|
let tagged_value: TaggedValue = serde_json::from_value(value.clone()).map_err(|e| ConversionError::DeserializationError(format!("TaggedValue: {e:?}")))?;
|
|
GraphCraftNodeInput::Value {
|
|
tagged_value: MemoHash::new(tagged_value),
|
|
exposed: *exposed,
|
|
}
|
|
}
|
|
NodeInput::Scope(s) => GraphCraftNodeInput::Scope(s.clone()),
|
|
NodeInput::Import { index: import_idx } => GraphCraftNodeInput::Import {
|
|
import_type: input_attributes.get_or(node::input::IMPORT_TYPE, Type::Generic(Cow::Borrowed("T"))),
|
|
import_index: *import_idx as usize,
|
|
},
|
|
NodeInput::Reflection => GraphCraftNodeInput::Reflection(
|
|
input_attributes
|
|
.get_typed(node::REFLECTION_METADATA)
|
|
.ok_or_else(|| ConversionError::DeserializationError("Missing reflection_metadata in input_attributes".to_string()))?,
|
|
),
|
|
NodeInput::Other => return Err(ConversionError::DeserializationError("Cannot convert NodeInput::Other to a runtime input".to_string())),
|
|
})
|
|
}
|
|
|
|
fn convert_implementation(
|
|
context: &ConversionContext,
|
|
implementation: &Implementation,
|
|
parent_metadata_path: &[RuntimeNodeId],
|
|
owning_runtime_id: RuntimeNodeId,
|
|
node_collector: &mut Option<Vec<NodeMetadataEntry>>,
|
|
network_collector: &mut Option<Vec<NetworkMetadataEntry>>,
|
|
) -> Result<DocumentNodeImplementation, ConversionError> {
|
|
Ok(match implementation {
|
|
Implementation::ProtoNode(id) => {
|
|
let proto = context.declarations.get(id).ok_or(ConversionError::DeclarationNotFound(*id))?;
|
|
DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::with_owned_string(proto.identifier.clone()))
|
|
}
|
|
Implementation::Network(net_id) => {
|
|
let mut child_path = Vec::with_capacity(parent_metadata_path.len() + 1);
|
|
child_path.extend_from_slice(parent_metadata_path);
|
|
child_path.push(owning_runtime_id);
|
|
DocumentNodeImplementation::Network(convert_network(context, *net_id, &child_path, node_collector, network_collector)?)
|
|
}
|
|
})
|
|
}
|