Rework the runtime delta experiment to the agreed shape: graph-craft structural deltas, an editor metadata wrapper, and diff-derived metadata ops

This commit is contained in:
Keavon Chambers
2026-07-28 01:20:01 -07:00
parent 3114c57ec2
commit c7d91fe7f2
8 changed files with 695 additions and 5 deletions

View File

@@ -92,7 +92,7 @@ impl Delta {
/// Op payload. Timestamps live on the wrapping `Delta` — one per delta, applied to all LWW-eligible
/// writes within. See `notes/document-format-collaboration.md`.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum RegistryDelta {
AddNode {
id: NodeId,
@@ -190,7 +190,7 @@ pub enum RegistryDelta {
}
/// `value: None` means remove. The timestamp comes from the wrapping `Delta`.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AttributeDelta {
pub key: String,
pub value: Option<serde_json::Value>,

View File

@@ -423,6 +423,27 @@ fn convert_node<M: NodeMetadataSource + ?Sized>(
})
}
pub fn encode_node_ui_attributes<M: NodeMetadataSource + ?Sized>(
attributes: &mut crate::Attributes,
metadata: &M,
metadata_path: &[RuntimeNodeId],
runtime_node_id: RuntimeNodeId,
timestamp: TimeStamp,
) -> Result<(), ConversionError> {
write_ui_attributes(attributes, metadata, metadata_path, runtime_node_id, timestamp)
}
pub fn encode_input_ui_attributes<M: NodeMetadataSource + ?Sized>(
attributes: &mut crate::Attributes,
metadata: &M,
metadata_path: &[RuntimeNodeId],
runtime_node_id: RuntimeNodeId,
input_index: usize,
timestamp: TimeStamp,
) -> Result<(), ConversionError> {
write_ui_input_attributes(attributes, metadata, metadata_path, runtime_node_id, input_index, timestamp)
}
fn write_ui_attributes<M: NodeMetadataSource + ?Sized>(
attributes: &mut crate::Attributes,
metadata: &M,
@@ -613,6 +634,13 @@ impl PathResolver {
child_path(owner.as_ref(), self.network_id(local_path), local_id).to_global_id(self.peer)
}
/// Converts one runtime input inside the network at `local_path` to its storage form, resolving
/// node references to their stable global IDs.
pub fn convert_input_at(&self, input: &GraphCraftNodeInput, local_path: &[RuntimeNodeId]) -> Result<NodeInput, ConversionError> {
let owner = self.owner_path(local_path);
convert_input(input, owner.as_ref(), self.network_id(local_path), self.peer)
}
/// The `NodePath` of the node owning the network at `local_path`, or `None` for the root network.
fn owner_path(&self, local_path: &[RuntimeNodeId]) -> Option<NodePath> {
let mut owner: Option<NodePath> = None;
@@ -703,10 +731,15 @@ impl<'m, M: NodeMetadataSource + ?Sized> ScopedConversion<'m, M> {
/// whole `TaggedValue`, which can be arbitrarily large. `resource_ref_shape_matches_serde` asserts
/// this stays in lockstep with the real serialization.
pub fn node_value_resource_refs(node: &Node) -> impl Iterator<Item = ResourceId> + '_ {
node.inputs.iter().filter_map(|slot| match &slot.input {
node.inputs.iter().filter_map(|slot| value_resource_ref(&slot.input))
}
/// The `TaggedValue::Resource` ID referenced by a stored value input, if any.
pub fn value_resource_ref(input: &NodeInput) -> Option<ResourceId> {
match input {
NodeInput::Value { value, .. } => value.get("Resource").and_then(|id| serde_json::from_value(id.clone()).ok()),
_ => None,
})
}
}
#[cfg(test)]

View File

@@ -29,7 +29,10 @@ pub use resources::*;
pub use session::*;
#[cfg(any(feature = "conversion", test))]
pub use from_runtime::{PathResolver, RuntimeConversion, ScopedConversion, convert_resource_entry, decode_declaration, encode_declaration, node_value_resource_refs};
pub use from_runtime::{
PathResolver, RuntimeConversion, ScopedConversion, convert_resource_entry, decode_declaration, encode_declaration, encode_input_ui_attributes, encode_node_ui_attributes, node_value_resource_refs,
value_resource_ref,
};
#[cfg(any(feature = "conversion", test))]
pub use metadata_source::{InputMetadataEntry, NetworkMetadataEntry, NoMetadata, NodeMetadataEntry, NodeMetadataSource, Position};
#[cfg(any(feature = "conversion", test))]

View File

@@ -2,6 +2,9 @@ mod caches;
#[cfg(test)]
mod characterization_tests;
mod deserialization;
pub mod editor_delta;
#[cfg(test)]
mod editor_delta_tests;
mod hit_tests;
mod layout;
mod memo_network;

View File

@@ -0,0 +1,382 @@
use super::{DocumentNodeMetadata, DocumentNodePersistentMetadata, LayerPosition, NodePosition, NodeTypePersistentMetadata};
use document_graph_storage::attr::node as node_attr;
use document_graph_storage::from_runtime::{ConversionError, DeclarationBytes};
use document_graph_storage::{AttributeDelta, Attributes, Implementation, NoMetadata, PathResolver, Position, Registry, RegistryDelta, ScopedConversion, TimeStamp};
use document_graph_storage::{convert_resource_entry, encode_input_ui_attributes, encode_node_ui_attributes, node_value_resource_refs, value_resource_ref};
use graph_craft::application_io::resource::{ResourceId, ResourceRegistry};
use graph_craft::document::NodeId;
use graph_craft::runtime_delta::RuntimeDelta;
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq)]
pub enum EditorDelta {
Graph(RuntimeDelta),
NodeMetadata {
network_path: Vec<NodeId>,
node_id: NodeId,
metadata: Box<DocumentNodePersistentMetadata>,
},
}
pub struct ConstructedOps {
pub ops: Vec<RegistryDelta>,
pub declaration_bytes: DeclarationBytes,
}
impl EditorDelta {
pub fn to_registry_deltas(&self, working: &Registry, resources: &ResourceRegistry, peer: document_graph_storage::PeerId) -> Result<ConstructedOps, ConversionError> {
let resolver = PathResolver::new(peer);
let mut ops = Vec::new();
let mut declaration_bytes = DeclarationBytes::new();
match self {
EditorDelta::Graph(RuntimeDelta::AddNode { network_path, node_id, node }) => {
declaration_bytes = construct_structural_additions(network_path, *node_id, node, working, resources, peer, &mut ops)?;
}
EditorDelta::Graph(RuntimeDelta::ReplaceNode { network_path, node_id, node }) => {
construct_removals(resolver.node_id(network_path, *node_id), working, &mut ops);
declaration_bytes = construct_structural_additions(network_path, *node_id, node, working, resources, peer, &mut ops)?;
}
EditorDelta::Graph(RuntimeDelta::RemoveNode { network_path, node_id }) => {
construct_removals(resolver.node_id(network_path, *node_id), working, &mut ops);
}
EditorDelta::Graph(RuntimeDelta::SetInput {
network_path,
node_id,
input_index,
input,
}) => {
ops.push(RegistryDelta::ChangeNodeInput {
id: resolver.node_id(network_path, *node_id),
index: (*input_index).try_into().map_err(|_| ConversionError::IndexOverflow(*input_index))?,
new_input: resolver.convert_input_at(input, network_path)?,
});
}
EditorDelta::Graph(RuntimeDelta::SetExport { network_path, export_index, input }) => {
ops.push(RegistryDelta::SetNetworkExport {
id: resolver.network_id(network_path),
index: (*export_index).try_into().map_err(|_| ConversionError::IndexOverflow(*export_index))?,
export: Some(resolver.convert_input_at(input, network_path)?),
});
}
EditorDelta::NodeMetadata { network_path, node_id, metadata } => {
construct_metadata_changes(network_path, *node_id, metadata, working, &resolver, &mut ops)?;
}
}
Ok(ConstructedOps { ops, declaration_bytes })
}
}
fn construct_structural_additions(
network_path: &[NodeId],
node_id: NodeId,
node: &graph_craft::document::DocumentNode,
working: &Registry,
resources: &ResourceRegistry,
peer: document_graph_storage::PeerId,
ops: &mut Vec<RegistryDelta>,
) -> Result<DeclarationBytes, ConversionError> {
let mut scoped = ScopedConversion::new(&NoMetadata, peer);
let mut scratch = Registry::default();
scoped.convert_node_at(&mut scratch, network_path, node_id, node, true)?;
let declaration_bytes = scoped.finish();
let mut networks: Vec<_> = scratch.networks.iter().collect();
networks.sort_by_key(|(id, _)| **id);
for (id, network) in networks {
ops.push(RegistryDelta::AddNetwork { id: *id, network: network.clone() });
}
let mut nodes: Vec<_> = scratch.node_instances.iter().collect();
nodes.sort_by_key(|(id, _)| **id);
for (id, node) in nodes {
ops.push(RegistryDelta::AddNode { id: *id, node: node.clone() });
}
let mut new_resources: Vec<_> = scratch.resources.iter().filter(|(id, _)| !working.resources.contains_key(id)).collect();
new_resources.sort_by_key(|(id, _)| **id);
for (id, entry) in new_resources {
ops.push(RegistryDelta::AddResource { id: *id, entry: entry.clone() });
}
let mut tagged: Vec<ResourceId> = scratch.node_instances.values().flat_map(node_value_resource_refs).collect();
tagged.sort();
tagged.dedup();
for id in tagged {
if !working.resources.contains_key(&id)
&& !scratch.resources.contains_key(&id)
&& let Some(entry) = convert_resource_entry(resources, id, peer)?
{
ops.push(RegistryDelta::AddResource { id, entry });
}
}
Ok(declaration_bytes)
}
fn construct_metadata_changes(
network_path: &[NodeId],
node_id: NodeId,
metadata: &DocumentNodePersistentMetadata,
working: &Registry,
resolver: &PathResolver,
ops: &mut Vec<RegistryDelta>,
) -> Result<(), ConversionError> {
let source = MetadataCopySource {
anchor_path: network_path,
anchor_id: node_id,
metadata,
};
let mut pending = vec![(network_path.to_vec(), node_id, metadata)];
while let Some((path, id, node_metadata)) = pending.pop() {
let global_id = resolver.node_id(&path, id);
let working_node = working.node_instances.get(&global_id);
let mut encoded = Attributes::new();
encode_node_ui_attributes(&mut encoded, &source, &path, id, TimeStamp::ORIGIN)?;
for delta in ui_attribute_deltas(working_node.map(|node| node.attributes()), &encoded) {
ops.push(RegistryDelta::ChangeNodeAttribute { id: global_id, delta });
}
for input_index in 0..node_metadata.input_metadata.len() {
let mut encoded = Attributes::new();
encode_input_ui_attributes(&mut encoded, &source, &path, id, input_index, TimeStamp::ORIGIN)?;
let current = working_node.and_then(|node| node.inputs().get(input_index)).map(|slot| &slot.attributes);
for delta in ui_attribute_deltas(current, &encoded) {
ops.push(RegistryDelta::ChangeNodeInputAttribute {
id: global_id,
index: input_index.try_into().map_err(|_| ConversionError::IndexOverflow(input_index))?,
delta,
});
}
}
if let Some(network_metadata) = &node_metadata.network_metadata {
let mut nested_path = path.clone();
nested_path.push(id);
let network_id = resolver.network_id(&nested_path);
let target = network_metadata.persistent_metadata.reference.clone().map(serde_json::Value::String);
let current = working
.networks
.get(&network_id)
.and_then(|network| network.attributes.get(node_attr::ui::REFERENCE))
.map(|value| value.value.clone());
if current != target {
ops.push(RegistryDelta::ChangeNetworkAttribute {
id: network_id,
delta: AttributeDelta {
key: node_attr::ui::REFERENCE.to_string(),
value: target,
},
});
}
for (child_id, child) in &network_metadata.persistent_metadata.node_metadata {
pending.push((nested_path.clone(), *child_id, &child.persistent_metadata));
}
}
}
Ok(())
}
fn ui_attribute_deltas(current: Option<&Attributes>, encoded: &Attributes) -> Vec<AttributeDelta> {
let owned = |key: &str| key.starts_with("ui::");
let mut deltas = Vec::new();
if let Some(current) = current {
for key in current.keys() {
if owned(key) && !encoded.contains_key(key) {
deltas.push(AttributeDelta { key: key.clone(), value: None });
}
}
}
for (key, value) in encoded {
let unchanged = current.and_then(|current| current.get(key)).is_some_and(|existing| existing.value == value.value);
if !unchanged {
deltas.push(AttributeDelta {
key: key.clone(),
value: Some(value.value.clone()),
});
}
}
deltas.sort_by(|a, b| a.key.cmp(&b.key));
deltas
}
fn construct_removals(node_id: document_graph_storage::NodeId, working: &Registry, ops: &mut Vec<RegistryDelta>) {
let mut removed_nodes = Vec::new();
let mut removed_networks = Vec::new();
collect_removal_closure(node_id, working, &mut removed_nodes, &mut removed_networks);
removed_nodes.sort();
removed_networks.sort();
for id in &removed_nodes {
ops.push(RegistryDelta::RemoveNode {
id: *id,
snapshot: working.node_instances[id].clone(),
});
}
for id in &removed_networks {
ops.push(RegistryDelta::RemoveNetwork {
id: *id,
snapshot: working.networks[id].clone(),
});
}
let removed_node_set: HashSet<_> = removed_nodes.iter().copied().collect();
let mut candidates: Vec<ResourceId> = removed_nodes
.iter()
.flat_map(|id| {
let node = &working.node_instances[id];
let declaration = match node.implementation() {
Implementation::ProtoNode(declaration) => Some(*declaration),
Implementation::Network(_) => None,
};
declaration.into_iter().chain(node_value_resource_refs(node))
})
.collect();
candidates.sort();
candidates.dedup();
for candidate in candidates {
let still_referenced = working
.node_instances
.iter()
.filter(|(id, _)| !removed_node_set.contains(id))
.any(|(_, node)| matches!(node.implementation(), Implementation::ProtoNode(declaration) if *declaration == candidate) || node_value_resource_refs(node).any(|id| id == candidate))
|| working
.networks
.values()
.any(|network| network.exports.iter().any(|slot| slot.target.as_ref().and_then(value_resource_ref) == Some(candidate)));
if !still_referenced && let Some(entry) = working.resources.get(&candidate) {
ops.push(RegistryDelta::RemoveResource {
id: candidate,
snapshot: entry.clone(),
});
}
}
}
fn collect_removal_closure(node_id: document_graph_storage::NodeId, working: &Registry, nodes: &mut Vec<document_graph_storage::NodeId>, networks: &mut Vec<document_graph_storage::NetworkId>) {
let Some(node) = working.node_instances.get(&node_id) else { return };
nodes.push(node_id);
if let &Implementation::Network(network_id) = node.implementation() {
networks.push(network_id);
for (child_id, child) in &working.node_instances {
if child.network() == network_id {
collect_removal_closure(*child_id, working, nodes, networks);
}
}
}
}
struct MetadataCopySource<'a> {
anchor_path: &'a [NodeId],
anchor_id: NodeId,
metadata: &'a DocumentNodePersistentMetadata,
}
impl MetadataCopySource<'_> {
fn metadata_for(&self, metadata_path: &[NodeId], node_id: NodeId) -> Option<&DocumentNodePersistentMetadata> {
let relative = metadata_path.strip_prefix(self.anchor_path)?;
let (mut current, rest) = match relative.split_first() {
None => return (node_id == self.anchor_id).then_some(self.metadata),
Some((first, rest)) if *first == self.anchor_id => (self.metadata, rest),
Some(_) => return None,
};
for step in rest {
current = child_metadata(current, *step)?;
}
child_metadata(current, node_id)
}
}
fn child_metadata(metadata: &DocumentNodePersistentMetadata, child_id: NodeId) -> Option<&DocumentNodePersistentMetadata> {
metadata
.network_metadata
.as_ref()
.and_then(|network| network.persistent_metadata.node_metadata.get(&child_id))
.map(|child: &DocumentNodeMetadata| &child.persistent_metadata)
}
impl document_graph_storage::NodeMetadataSource for MetadataCopySource<'_> {
fn position(&self, metadata_path: &[NodeId], node_id: NodeId) -> Option<Position> {
match &self.metadata_for(metadata_path, node_id)?.node_type_metadata {
NodeTypePersistentMetadata::Layer(layer) => Some(match layer.position {
LayerPosition::Absolute(offset) => Position::Absolute([offset.x, offset.y]),
LayerPosition::Stack(offset) => Position::Stack(offset),
}),
NodeTypePersistentMetadata::Node(node) => Some(match *node.position() {
NodePosition::Absolute(offset) => Position::Absolute([offset.x, offset.y]),
NodePosition::Chain => Position::Chain,
}),
}
}
fn is_layer(&self, metadata_path: &[NodeId], node_id: NodeId) -> bool {
self.metadata_for(metadata_path, node_id)
.is_some_and(|metadata| matches!(metadata.node_type_metadata, NodeTypePersistentMetadata::Layer(_)))
}
fn display_name(&self, metadata_path: &[NodeId], node_id: NodeId) -> Option<&str> {
self.metadata_for(metadata_path, node_id).map(|metadata| metadata.display_name.as_str())
}
fn locked(&self, metadata_path: &[NodeId], node_id: NodeId) -> bool {
self.metadata_for(metadata_path, node_id).is_some_and(|metadata| metadata.locked)
}
fn pinned(&self, metadata_path: &[NodeId], node_id: NodeId) -> bool {
self.metadata_for(metadata_path, node_id).is_some_and(|metadata| metadata.pinned)
}
fn output_names(&self, metadata_path: &[NodeId], node_id: NodeId) -> Vec<String> {
self.metadata_for(metadata_path, node_id).map(|metadata| metadata.output_names.clone()).unwrap_or_default()
}
fn input_name(&self, metadata_path: &[NodeId], node_id: NodeId, input_index: usize) -> Option<&str> {
self.metadata_for(metadata_path, node_id)
.and_then(|metadata| metadata.input_metadata.get(input_index))
.map(|input| input.persistent_metadata.input_name.as_str())
}
fn input_description(&self, metadata_path: &[NodeId], node_id: NodeId, input_index: usize) -> Option<&str> {
self.metadata_for(metadata_path, node_id)
.and_then(|metadata| metadata.input_metadata.get(input_index))
.map(|input| input.persistent_metadata.input_description.as_str())
}
fn widget_override(&self, metadata_path: &[NodeId], node_id: NodeId, input_index: usize) -> Option<&str> {
self.metadata_for(metadata_path, node_id)
.and_then(|metadata| metadata.input_metadata.get(input_index))
.and_then(|input| input.persistent_metadata.widget_override.as_deref())
}
fn input_data(&self, metadata_path: &[NodeId], node_id: NodeId, input_index: usize) -> std::collections::HashMap<String, serde_json::Value> {
self.metadata_for(metadata_path, node_id)
.and_then(|metadata| metadata.input_metadata.get(input_index))
.map(|input| input.persistent_metadata.input_data.clone())
.unwrap_or_default()
}
fn reference(&self, network_path: &[NodeId]) -> Option<&str> {
let (owner_path, owner_id) = network_path.split_last().map(|(last, rest)| (rest, *last))?;
self.metadata_for(owner_path, owner_id)?
.network_metadata
.as_ref()
.and_then(|network| network.persistent_metadata.reference.as_deref())
}
}

View File

@@ -0,0 +1,238 @@
use super::InputConnector;
use super::editor_delta::EditorDelta;
use super::storage_metadata::StorageMetadataView;
use crate::test_utils::test_prelude::*;
use document_graph_storage::delta::compute_deltas;
use document_graph_storage::{PeerId, Registry, RegistryDelta, Session};
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use graph_craft::runtime_delta::RuntimeDelta;
use graphene_std::uuid::NodeId;
const PEER: PeerId = PeerId(7);
fn rectangle_definition() -> DefinitionIdentifier {
DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::rectangle::IDENTIFIER)
}
fn convert(editor: &EditorTestUtils) -> Registry {
let document = editor.active_document();
Registry::convert_from_runtime(
document.network_interface.document_network(),
&StorageMetadataView::new(&document.network_interface),
&document.resources.registry,
PEER,
)
.expect("conversion should succeed")
.registry
}
fn construct(editor: &EditorTestUtils, delta: &EditorDelta, working: &Registry) -> Vec<RegistryDelta> {
delta
.to_registry_deltas(working, &editor.active_document().resources.registry, PEER)
.expect("construction should succeed")
.ops
}
fn node_metadata_delta(editor: &EditorTestUtils, node_id: NodeId) -> EditorDelta {
let metadata = editor
.active_document()
.network_interface
.node_metadata(&node_id, &[])
.expect("node metadata should exist")
.persistent_metadata
.clone();
EditorDelta::NodeMetadata {
network_path: Vec::new(),
node_id,
metadata: Box::new(metadata),
}
}
fn assert_same_stored_effect(working: &Registry, constructed: Vec<RegistryDelta>, diffed: Vec<RegistryDelta>, at: &str) {
let baseline = compute_deltas(&Registry::default(), working);
let mut from_construction = Session::with_peer(PEER);
from_construction.stage_computed_ops(baseline.clone()).expect("baseline should stage");
from_construction.stage_computed_ops(constructed).expect("constructed ops should stage");
let mut from_diff = Session::with_peer(PEER);
from_diff.stage_computed_ops(baseline).expect("baseline should stage");
from_diff.stage_computed_ops(diffed).expect("diffed ops should stage");
assert!(
from_construction.registry().value_equal(from_diff.registry()),
"Constructed ops must produce the same stored state as the whole-document diff: {at}\nresidual: {:#?}",
compute_deltas(from_construction.registry(), from_diff.registry())
);
}
#[tokio::test]
async fn set_input_value_constructs_the_exact_diff_op() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let node = editor.create_node_by_name(rectangle_definition()).await;
let working = convert(&editor);
let input = NodeInput::value(TaggedValue::F64(42.), false);
editor.active_document_mut().network_interface.set_input(&InputConnector::node(node, 1), input.clone(), &[]);
let delta = EditorDelta::Graph(RuntimeDelta::SetInput {
network_path: Vec::new(),
node_id: node,
input_index: 1,
input,
});
let constructed = construct(&editor, &delta, &working);
let diffed = compute_deltas(&working, &convert(&editor));
assert_eq!(constructed, diffed, "A value edit should construct exactly the diff's op");
}
#[tokio::test]
async fn wiring_and_export_edits_construct_the_exact_diff_ops() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let a = editor.create_node_by_name(rectangle_definition()).await;
let b = editor.create_node_by_name(rectangle_definition()).await;
let working = convert(&editor);
let wire = NodeInput::node(b, 0);
editor.active_document_mut().network_interface.set_input(&InputConnector::node(a, 1), wire.clone(), &[]);
let delta = EditorDelta::Graph(RuntimeDelta::SetInput {
network_path: Vec::new(),
node_id: a,
input_index: 1,
input: wire,
});
let constructed = construct(&editor, &delta, &working);
let diffed = compute_deltas(&working, &convert(&editor));
assert_eq!(constructed, diffed, "A wiring edit should construct exactly the diff's op");
let working = convert(&editor);
let export = NodeInput::node(a, 0);
editor.active_document_mut().network_interface.set_input(&InputConnector::Export(0), export.clone(), &[]);
let delta = EditorDelta::Graph(RuntimeDelta::SetExport {
network_path: Vec::new(),
export_index: 0,
input: export,
});
let constructed = construct(&editor, &delta, &working);
let diffed = compute_deltas(&working, &convert(&editor));
assert_eq!(constructed, diffed, "An export edit should construct exactly the diff's op");
}
#[tokio::test]
async fn adding_a_node_as_structure_plus_metadata_matches_the_diff() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let working = convert(&editor);
let template = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(&rectangle_definition())
.expect("rectangle definition")
.default_node_template();
let node_id = NodeId(0xDE17A);
let (document_node, metadata) = template.clone().into_parts();
editor.active_document_mut().network_interface.insert_node(node_id, template, &[]);
let deltas = [
EditorDelta::Graph(RuntimeDelta::AddNode {
network_path: Vec::new(),
node_id,
node: Box::new(document_node),
}),
EditorDelta::NodeMetadata {
network_path: Vec::new(),
node_id,
metadata: Box::new(metadata),
},
];
let constructed = deltas.iter().flat_map(|delta| construct(&editor, delta, &working)).collect();
let diffed = compute_deltas(&working, &convert(&editor));
assert_same_stored_effect(&working, constructed, diffed, "node addition");
}
#[tokio::test]
async fn metadata_edits_construct_the_exact_diff_ops() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let node = editor.create_node_by_name(rectangle_definition()).await;
let working = convert(&editor);
{
let network_interface = &mut editor.active_document_mut().network_interface;
network_interface.set_display_name(&node, "Renamed".to_string(), &[]);
network_interface.set_locked(&node, &[], true);
network_interface.set_pinned(&node, &[], true);
network_interface.shift_node(&node, glam::IVec2::new(3, 5), &[]);
}
let constructed = construct(&editor, &node_metadata_delta(&editor, node), &working);
let diffed = compute_deltas(&working, &convert(&editor));
assert_eq!(constructed, diffed, "Metadata edits should construct exactly the diff's attribute ops");
}
#[tokio::test]
async fn removing_a_nested_network_node_matches_the_diff() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.draw_rect(0., 0., 100., 100.).await;
editor
.handle_message(DocumentMessage::GroupSelectedLayers {
group_folder_type: crate::messages::portfolio::document::utility_types::misc::GroupFolderType::Layer,
})
.await;
let before: Vec<NodeId> = editor.active_document().network_interface.document_network().nodes.keys().copied().collect();
let working = convert(&editor);
let group = editor
.active_document()
.network_interface
.document_network()
.nodes
.iter()
.find(|(_, node)| matches!(node.implementation, graph_craft::document::DocumentNodeImplementation::Network(_)))
.map(|(id, _)| *id)
.expect("the group should be a network node");
editor.active_document_mut().network_interface.delete_nodes(vec![group], true, &[]);
let network = editor.active_document().network_interface.document_network().clone();
let mut deltas: Vec<EditorDelta> = before
.iter()
.filter(|id| !network.nodes.contains_key(id))
.map(|id| {
EditorDelta::Graph(RuntimeDelta::RemoveNode {
network_path: Vec::new(),
node_id: *id,
})
})
.collect();
for (index, export) in network.exports.iter().enumerate() {
deltas.push(EditorDelta::Graph(RuntimeDelta::SetExport {
network_path: Vec::new(),
export_index: index,
input: export.clone(),
}));
}
for (node_id, node) in &network.nodes {
for (index, input) in node.inputs.iter().enumerate() {
deltas.push(EditorDelta::Graph(RuntimeDelta::SetInput {
network_path: Vec::new(),
node_id: *node_id,
input_index: index,
input: input.clone(),
}));
}
}
let without_resource_ops = |ops: Vec<RegistryDelta>| {
ops.into_iter()
.filter(|op| !matches!(op, RegistryDelta::RemoveResource { .. } | RegistryDelta::AddResource { .. }))
.collect::<Vec<_>>()
};
let constructed = without_resource_ops(deltas.iter().flat_map(|delta| construct(&editor, delta, &working)).collect());
let diffed = without_resource_ops(compute_deltas(&working, &convert(&editor)));
assert_same_stored_effect(&working, constructed, diffed, "nested network removal");
}

View File

@@ -7,6 +7,7 @@ pub use core_types::{ProtoNodeIdentifier, Type, TypeDescriptor, concrete, descri
pub mod application_io;
pub mod document;
pub mod runtime_delta;
pub use document::{DocumentNode, NodeNetwork};
pub mod graphene_compiler;
pub mod proto;

View File

@@ -0,0 +1,30 @@
use crate::document::{DocumentNode, NodeId, NodeInput};
#[derive(Debug, Clone, PartialEq)]
pub enum RuntimeDelta {
AddNode {
network_path: Vec<NodeId>,
node_id: NodeId,
node: Box<DocumentNode>,
},
ReplaceNode {
network_path: Vec<NodeId>,
node_id: NodeId,
node: Box<DocumentNode>,
},
RemoveNode {
network_path: Vec<NodeId>,
node_id: NodeId,
},
SetInput {
network_path: Vec<NodeId>,
node_id: NodeId,
input_index: usize,
input: NodeInput,
},
SetExport {
network_path: Vec<NodeId>,
export_index: usize,
input: NodeInput,
},
}