mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
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:
@@ -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>,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
30
node-graph/graph-craft/src/runtime_delta.rs
Normal file
30
node-graph/graph-craft/src/runtime_delta.rs
Normal 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,
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user