mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-30 07:48:12 +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
|
/// Op payload. Timestamps live on the wrapping `Delta` — one per delta, applied to all LWW-eligible
|
||||||
/// writes within. See `notes/document-format-collaboration.md`.
|
/// writes within. See `notes/document-format-collaboration.md`.
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum RegistryDelta {
|
pub enum RegistryDelta {
|
||||||
AddNode {
|
AddNode {
|
||||||
id: NodeId,
|
id: NodeId,
|
||||||
@@ -190,7 +190,7 @@ pub enum RegistryDelta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// `value: None` means remove. The timestamp comes from the wrapping `Delta`.
|
/// `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 struct AttributeDelta {
|
||||||
pub key: String,
|
pub key: String,
|
||||||
pub value: Option<serde_json::Value>,
|
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>(
|
fn write_ui_attributes<M: NodeMetadataSource + ?Sized>(
|
||||||
attributes: &mut crate::Attributes,
|
attributes: &mut crate::Attributes,
|
||||||
metadata: &M,
|
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)
|
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.
|
/// 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> {
|
fn owner_path(&self, local_path: &[RuntimeNodeId]) -> Option<NodePath> {
|
||||||
let mut owner: Option<NodePath> = None;
|
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
|
/// whole `TaggedValue`, which can be arbitrarily large. `resource_ref_shape_matches_serde` asserts
|
||||||
/// this stays in lockstep with the real serialization.
|
/// this stays in lockstep with the real serialization.
|
||||||
pub fn node_value_resource_refs(node: &Node) -> impl Iterator<Item = ResourceId> + '_ {
|
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()),
|
NodeInput::Value { value, .. } => value.get("Resource").and_then(|id| serde_json::from_value(id.clone()).ok()),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -29,7 +29,10 @@ pub use resources::*;
|
|||||||
pub use session::*;
|
pub use session::*;
|
||||||
|
|
||||||
#[cfg(any(feature = "conversion", test))]
|
#[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))]
|
#[cfg(any(feature = "conversion", test))]
|
||||||
pub use metadata_source::{InputMetadataEntry, NetworkMetadataEntry, NoMetadata, NodeMetadataEntry, NodeMetadataSource, Position};
|
pub use metadata_source::{InputMetadataEntry, NetworkMetadataEntry, NoMetadata, NodeMetadataEntry, NodeMetadataSource, Position};
|
||||||
#[cfg(any(feature = "conversion", test))]
|
#[cfg(any(feature = "conversion", test))]
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ mod caches;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod characterization_tests;
|
mod characterization_tests;
|
||||||
mod deserialization;
|
mod deserialization;
|
||||||
|
pub mod editor_delta;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod editor_delta_tests;
|
||||||
mod hit_tests;
|
mod hit_tests;
|
||||||
mod layout;
|
mod layout;
|
||||||
mod memo_network;
|
mod memo_network;
|
||||||
|
|||||||
+382
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
+238
@@ -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 application_io;
|
||||||
pub mod document;
|
pub mod document;
|
||||||
|
pub mod runtime_delta;
|
||||||
pub use document::{DocumentNode, NodeNetwork};
|
pub use document::{DocumentNode, NodeNetwork};
|
||||||
pub mod graphene_compiler;
|
pub mod graphene_compiler;
|
||||||
pub mod proto;
|
pub mod proto;
|
||||||
|
|||||||
@@ -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