use crate::cycle::named_ref_of;
use crate::node::NodeCodec;
use crate::widget::gantz::OpenHeadState;
use crate::widget::graph_scene::NodeIndex;
use crate::{CreateNode, InspectEdge, PastePos, export, node::NamedRef};
use gantz_ca::{CommitAddr, DataGraph, GraphAddr, Name, NodeData};
use gantz_core::node::{self, GetNode};
use petgraph::visit::EdgeRef;
pub use session::{RevertCursor, redo, session_redo, session_undo, undo};
use std::collections::{BTreeSet, HashMap, HashSet};
use steel::steel_vm::engine::Engine;
pub use sync::{MergeHeadOutcome, SyncTipOutcome, merge_head, sync_remote_tip};
mod session;
mod sync;
fn node_id(ix: usize) -> egui_graph::NodeId {
egui_graph::NodeId::from_u64(ix as u64)
}
fn cascade_pos(center: egui::Pos2, i: usize) -> egui::Pos2 {
center + egui::vec2(20.0, 20.0) * i as f32
}
fn selection_centroid(view: &crate::SceneView, nodes: &HashSet<NodeIndex>) -> Option<egui::Pos2> {
let mut sum = egui::Vec2::ZERO;
let mut count = 0usize;
for &n in nodes {
if let Some(&pos) = view.layout.get(&node_id(n.index())) {
sum += pos.to_vec2();
count += 1;
}
}
(count > 0).then(|| egui::Pos2::new(sum.x / count as f32, sum.y / count as f32))
}
fn next_child_name(registry: &gantz_ca::Registry, parent: &Name) -> Name {
let mut n = 1u32;
loop {
let candidate = parent.child(n.to_string());
if registry.head(&candidate).is_none() {
return candidate;
}
n += 1;
}
}
pub fn branch_node(
registry: &mut gantz_ca::Registry,
timestamp: std::time::Duration,
graph: &mut DataGraph,
new_name: String,
ca: gantz_ca::ContentAddr,
path: &[node::Id],
) {
let graph_addr = GraphAddr::from(ca);
if registry.graph(&graph_addr).is_none() {
log::error!("BranchNode: graph not found for {graph_addr:?}");
return;
}
let parent = newest_commit_for_graph(registry, graph_addr);
let new_commit_ca = registry.commit_graph(timestamp, parent, graph_addr, || {
unreachable!("graph already exists in registry")
});
let name: Name = new_name.parse().expect("infallible");
registry.set_head(name.clone(), new_commit_ca);
let Some(&node_ix) = path.last() else {
log::error!("BranchNode: empty node path");
return;
};
let node_id = node::graph::NodeIx::new(node_ix);
let new_ref = match graph.node_weight(node_id).and_then(named_ref_of) {
Some(old) => old.ref_().retarget(graph_addr.into()),
None => node::Ref::new(graph_addr.into()),
};
let named_ref = NamedRef::new(name, new_ref);
let node_data = match gantz_core::data::erase_node_typed(&named_ref) {
Ok(node_data) => node_data,
Err(e) => {
log::error!("BranchNode: failed to erase the new `NamedRef`: {e}");
return;
}
};
if let Some(node) = graph.node_weight_mut(node_id) {
*node = node_data;
} else {
log::error!("BranchNode: node not found at index {node_ix}");
}
}
fn newest_commit_for_graph(
registry: &gantz_ca::Registry,
graph_addr: GraphAddr,
) -> Option<CommitAddr> {
registry
.commits()
.iter()
.filter(|(_, commit)| commit.graph == graph_addr)
.max_by_key(|(ca, commit)| (commit.timestamp, **ca))
.map(|(ca, _)| *ca)
}
pub fn copy_nodes(
registry: &gantz_ca::Registry,
graph: &DataGraph,
head_view: &crate::SceneView,
selection: &HashSet<NodeIndex>,
codec: &NodeCodec,
) -> Option<String> {
if selection.is_empty() {
return None;
}
let copied = export::copy(registry, graph, selection, &head_view.layout);
match export::copied_to_string(&copied, codec) {
Ok(text) => Some(text),
Err(e) => {
log::error!("CopyNodes: failed to serialize: {e}");
None
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn create_node(
registry: &gantz_ca::Registry,
editing: Option<&str>,
codec: &NodeCodec,
get_node: GetNode,
new_node: impl FnOnce(&str) -> Option<NodeData>,
graph: &mut DataGraph,
view: &mut crate::SceneView,
head_state: &mut OpenHeadState,
vm: &mut Engine,
cmd: CreateNode,
) -> Option<NodeIndex> {
let CreateNode { node_type, pos } = cmd;
if editing.is_some_and(|editing| {
let target: Name = node_type.parse().expect("infallible");
let editing: Name = editing.parse().expect("infallible");
crate::cycle::would_cycle(registry, &target, &editing)
}) {
log::warn!("CreateNode: '{node_type}' would create a reference cycle; skipping");
return None;
}
let Some(node) = new_node(&node_type) else {
log::error!("CreateNode: unknown node type: {node_type}");
return None;
};
let node_ix = graph.add_node(node);
match codec.reify_ui(&graph[node_ix]) {
Ok(inst) => {
let node_path = [node_ix.index()];
let reg_ctx = node::RegCtx::new(get_node, &node_path, vm);
inst.node.register(reg_ctx);
}
Err(e) => log::error!("CreateNode: cannot register '{node_type}' with the VM: {e}"),
}
let pos = pos.unwrap_or_else(|| view.camera.center);
let egui_id = node_id(node_ix.index());
view.layout.insert(egui_id, pos);
let sel = &mut head_state.scene.interaction.selection;
sel.nodes.clear();
sel.edges.clear();
sel.nodes.insert(node_ix);
Some(node_ix)
}
pub fn create_nested_graph(
registry: &mut gantz_ca::Registry,
timestamp: std::time::Duration,
graph: &mut DataGraph,
view: &mut crate::SceneView,
head_state: &mut OpenHeadState,
pos: Option<egui::Pos2>,
parent: &Name,
) -> Option<NodeIndex> {
let name = next_child_name(registry, parent);
let nested_graph = DataGraph::default();
let graph_ca = gantz_ca::graph_addr(&nested_graph);
registry.commit_graph_to_name(timestamp, graph_ca, || nested_graph, &name);
let named_ref = NamedRef::with_sync(name, node::Ref::new(graph_ca.into()));
let node_data = match gantz_core::data::erase_node_typed(&named_ref) {
Ok(node_data) => node_data,
Err(e) => {
log::error!("CreateNestedGraph: failed to erase the new `NamedRef`: {e}");
return None;
}
};
let node_ix = graph.add_node(node_data);
let pos = pos.unwrap_or_else(|| view.camera.center);
let egui_id = node_id(node_ix.index());
view.layout.insert(egui_id, pos);
let sel = &mut head_state.scene.interaction.selection;
sel.nodes.clear();
sel.edges.clear();
sel.nodes.insert(node_ix);
Some(node_ix)
}
#[allow(clippy::too_many_arguments)]
pub fn nest_nodes(
registry: &mut gantz_ca::Registry,
timestamp: std::time::Duration,
graph: &mut DataGraph,
vm: &mut Engine,
view: &mut crate::SceneView,
head_state: &mut OpenHeadState,
instances: &mut crate::node::NodeInstances,
nodes: &HashSet<NodeIndex>,
parent: &Name,
) -> Option<NodeIndex> {
if nodes.is_empty() {
return None;
}
let mut incoming: Vec<(NodeIndex, gantz_core::Edge, NodeIndex)> = Vec::new();
let mut outgoing: Vec<(NodeIndex, gantz_core::Edge, NodeIndex)> = Vec::new();
for edge in graph.edge_references() {
let src = edge.source();
let tgt = edge.target();
let sel_src = nodes.contains(&src);
let sel_tgt = nodes.contains(&tgt);
if !sel_src && sel_tgt {
incoming.push((tgt, edge.weight().clone(), src));
} else if sel_src && !sel_tgt {
outgoing.push((src, edge.weight().clone(), tgt));
}
}
incoming.sort_by_key(|&(dst, ref w, _)| (dst.index(), w.input.0));
outgoing.sort_by_key(|&(src, ref w, _)| (src.index(), w.output.0));
let pos = selection_centroid(view, nodes).unwrap_or(view.camera.center);
let mut nested = DataGraph::default();
let in_ixs: Vec<NodeIndex> = incoming
.iter()
.map(|_| {
let inlet =
gantz_core::data::erase_node_typed(&gantz_core::node::graph::Inlet::default())
.ok()?;
Some(nested.add_node(inlet))
})
.collect::<Option<_>>()?;
let out_ixs: Vec<NodeIndex> = outgoing
.iter()
.map(|_| {
let outlet =
gantz_core::data::erase_node_typed(&gantz_core::node::graph::Outlet::default())
.ok()?;
Some(nested.add_node(outlet))
})
.collect::<Option<_>>()?;
let subgraph = gantz_core::graph::extract_subgraph(graph, nodes);
let new_indices = gantz_core::graph::add_subgraph(&mut nested, &subgraph);
let sorted: BTreeSet<_> = nodes.iter().copied().collect();
let mut old_to_sub = HashMap::new();
for (old_ix, sub_ix) in sorted.iter().zip(subgraph.node_indices()) {
old_to_sub.insert(*old_ix, sub_ix);
}
let mut sub_to_nested = HashMap::new();
for (sub_ix, &nested_ix) in subgraph.node_indices().zip(new_indices.iter()) {
sub_to_nested.insert(sub_ix, nested_ix);
}
for (i, &(dst, ref w, _)) in incoming.iter().enumerate() {
let dst_nested = sub_to_nested[&old_to_sub[&dst]];
nested.add_edge(
in_ixs[i],
dst_nested,
gantz_core::Edge::new(node::Output(0), w.input),
);
}
for (j, &(src, ref w, _)) in outgoing.iter().enumerate() {
let src_nested = sub_to_nested[&old_to_sub[&src]];
nested.add_edge(
src_nested,
out_ixs[j],
gantz_core::Edge::new(w.output, node::Input(0)),
);
}
let graph_ca = gantz_ca::graph_addr(&nested);
let name = next_child_name(registry, parent);
registry.commit_graph_to_name(timestamp, graph_ca, || nested.clone(), &name);
let reindex = remove_nodes(
graph,
vm,
&mut view.layout,
&mut head_state.scene.interaction.selection,
instances,
nodes.iter().copied(),
);
let named_ref = NamedRef::with_sync(name, node::Ref::new(graph_ca.into()));
let node_data = gantz_core::data::erase_node_typed(&named_ref).ok()?;
let new_ix = graph.add_node(node_data);
for (i, &(_, ref w, external)) in incoming.iter().enumerate() {
let ext = reindex
.apply_to_index(external.index())
.expect("external boundary node must survive the cut");
graph.add_edge(
NodeIndex::new(ext),
new_ix,
gantz_core::Edge::new(w.output, node::Input(i as u16)),
);
}
for (j, &(_, ref w, external)) in outgoing.iter().enumerate() {
let ext = reindex
.apply_to_index(external.index())
.expect("external boundary node must survive the cut");
graph.add_edge(
new_ix,
NodeIndex::new(ext),
gantz_core::Edge::new(node::Output(j as u16), w.input),
);
}
view.layout.insert(node_id(new_ix.index()), pos);
let sel = &mut head_state.scene.interaction.selection;
sel.nodes.clear();
sel.edges.clear();
sel.nodes.insert(new_ix);
Some(new_ix)
}
#[derive(Clone, Copy, Debug)]
pub struct RemoveOp {
pub removed: usize,
pub moved_from: Option<usize>,
}
#[derive(Clone, Debug, Default)]
pub struct Reindex(pub Vec<RemoveOp>);
impl Reindex {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn apply_to_index(&self, mut ix: usize) -> Option<usize> {
for op in &self.0 {
if ix == op.removed {
return None;
}
if op.moved_from == Some(ix) {
ix = op.removed;
}
}
Some(ix)
}
}
pub fn remove_nodes(
graph: &mut DataGraph,
vm: &mut Engine,
layout: &mut egui_graph::Layout,
selection: &mut crate::widget::graph_scene::Selection,
instances: &mut crate::node::NodeInstances,
nodes: impl IntoIterator<Item = NodeIndex>,
) -> Reindex {
let mut targets: Vec<NodeIndex> = nodes.into_iter().collect();
targets.sort_unstable_by_key(|n| std::cmp::Reverse(n.index()));
targets.dedup();
let mut ops = Vec::new();
for t in targets {
if graph.node_weight(t).is_none() {
continue;
}
let last = graph.node_count() - 1;
let _ = node::state::remove_value(vm, &[t.index()]);
layout.remove(&node_id(t.index()));
selection.nodes.remove(&t);
graph.remove_node(t);
let moved_from = (t.index() != last).then_some(last);
if let Some(last) = moved_from {
let _ = node::state::move_value(vm, &[last], &[t.index()]);
if let Some(pos) = layout.remove(&node_id(last)) {
layout.insert(node_id(t.index()), pos);
}
if selection.nodes.remove(&NodeIndex::new(last)) {
selection.nodes.insert(t);
}
}
ops.push(RemoveOp {
removed: t.index(),
moved_from,
});
}
if !ops.is_empty() {
selection.edges.clear();
}
let reindex = Reindex(ops);
instances.apply_reindex(&reindex);
reindex
}
pub fn cut_nodes(
registry: &gantz_ca::Registry,
graph: &mut DataGraph,
vm: &mut Engine,
head_view: &mut crate::SceneView,
selection: &mut crate::widget::graph_scene::Selection,
instances: &mut crate::node::NodeInstances,
nodes: &HashSet<NodeIndex>,
codec: &NodeCodec,
) -> Option<String> {
let text = copy_nodes(registry, graph, head_view, nodes, codec)?;
remove_nodes(
graph,
vm,
&mut head_view.layout,
selection,
instances,
nodes.iter().copied(),
);
Some(text)
}
pub fn inspect_edge(
codec: &NodeCodec,
get_node: GetNode,
new_inspect: impl FnOnce() -> Option<NodeData>,
graph: &mut DataGraph,
view: &mut crate::SceneView,
vm: &mut Engine,
cmd: InspectEdge,
) {
let InspectEdge { edge, pos } = cmd;
let Some((src_node, dst_node)) = graph.edge_endpoints(edge) else {
log::error!("InspectEdge: edge not found");
return;
};
let edge_weight = *graph.edge_weight(edge).unwrap();
graph.remove_edge(edge);
let Some(inspect_node) = new_inspect() else {
log::error!("InspectEdge: could not create inspect node");
return;
};
let inspect_id = graph.add_node(inspect_node);
match codec.reify_ui(&graph[inspect_id]) {
Ok(inst) => {
let node_path = [inspect_id.index()];
let reg_ctx = node::RegCtx::new(get_node, &node_path, vm);
inst.node.register(reg_ctx);
}
Err(e) => log::error!("InspectEdge: cannot register the inspect node with the VM: {e}"),
}
graph.add_edge(
src_node,
inspect_id,
gantz_core::Edge::new(edge_weight.output, node::Input(0)),
);
graph.add_edge(
inspect_id,
dst_node,
gantz_core::Edge::new(node::Output(0), edge_weight.input),
);
let node_id = node_id(inspect_id.index());
view.layout.insert(node_id, pos);
}
#[allow(clippy::too_many_arguments)]
pub fn paste(
registry: &mut gantz_ca::Registry,
editing: Option<&str>,
graph: &mut DataGraph,
head_view: &mut crate::SceneView,
head_state: &mut OpenHeadState,
text: &str,
pos: &PastePos,
codec: &NodeCodec,
) -> bool {
let copied: export::Copied = match export::copied_from_str(text, codec) {
Ok(c) => c,
Err(e) => {
log::debug!("Clipboard does not contain a valid gantz payload: {e}");
return false;
}
};
if let Some(editing) = editing {
let editing: Name = editing.parse().expect("infallible");
if let Some(named) = copied
.graph
.node_weights()
.filter_map(named_ref_of)
.find(|nr| crate::cycle::would_cycle(registry, nr.name(), &editing))
{
log::warn!(
"Paste: '{}' would create a reference cycle in '{editing}'; skipping paste",
named.name()
);
return false;
}
}
let offset = crate::resolve_paste_offset(pos, &copied.positions);
let new_indices = export::paste(registry, graph, &mut head_view.layout, &copied, offset);
head_state.scene.interaction.selection.nodes = new_indices.into_iter().collect();
head_state.scene.interaction.selection.edges.clear();
true
}
pub fn duplicate_nodes(
registry: &mut gantz_ca::Registry,
editing: Option<&str>,
graph: &mut DataGraph,
head_view: &mut crate::SceneView,
head_state: &mut OpenHeadState,
nodes: &HashSet<NodeIndex>,
codec: &NodeCodec,
) -> bool {
let Some(text) = copy_nodes(registry, graph, head_view, nodes, codec) else {
return false;
};
paste(
registry,
editing,
graph,
head_view,
head_state,
&text,
&PastePos::Offset(egui::vec2(20.0, 20.0)),
codec,
)
}
pub fn carry_layout(
live: &crate::SceneView,
matching: &gantz_ca::Matching,
new_node_count: usize,
) -> crate::SceneView {
let mut view = crate::SceneView {
camera: live.camera,
layout: Default::default(),
};
for (&old_ix, &new_ix) in matching {
if let Some(pos) = live.layout.get(&node_id(old_ix)) {
view.layout.insert(node_id(new_ix), *pos);
}
}
if view.layout.is_empty() {
return view;
}
let unmapped: Vec<usize> = (0..new_node_count)
.filter(|&ix| !view.layout.contains_key(&node_id(ix)))
.collect();
for (i, ix) in unmapped.into_iter().enumerate() {
let pos = cascade_pos(view.camera.center, i);
view.layout.insert(node_id(ix), pos);
}
view
}
pub fn merged_view(
node_srcs: &[gantz_ca::merge::NodeSrc],
first_view: Option<&crate::SceneView>,
second_view: Option<&crate::SceneView>,
) -> crate::SceneView {
let camera = first_view
.or(second_view)
.map(|v| v.camera)
.unwrap_or_default();
let mut view = crate::SceneView {
camera,
layout: Default::default(),
};
let mut missing = Vec::new();
for (m, src) in node_srcs.iter().enumerate() {
let pos = src
.ours
.and_then(|o| first_view.and_then(|v| v.layout.get(&node_id(o)).copied()))
.or_else(|| {
src.theirs
.and_then(|t| second_view.and_then(|v| v.layout.get(&node_id(t)).copied()))
});
match pos {
Some(pos) => {
view.layout.insert(node_id(m), pos);
}
None => missing.push(m),
}
}
if view.layout.is_empty() {
return view;
}
for (i, m) in missing.into_iter().enumerate() {
let pos = cascade_pos(view.camera.center, i);
view.layout.insert(node_id(m), pos);
}
view
}
pub fn commit_layout(
registry: &mut gantz_ca::Registry,
timestamp: gantz_ca::Timestamp,
head: &mut gantz_ca::Head,
live: &crate::SceneView,
) -> Option<CommitAddr> {
let head_commit_ca = registry.head_commit_ca(head)?;
let baseline = crate::section::view(registry, &head_commit_ca)?;
if baseline.layout == live.layout {
return None;
}
let graph_addr = registry.commits().get(&head_commit_ca)?.graph;
Some(registry.commit_graph_to_head(
timestamp,
graph_addr,
|| unreachable!("layout commit reuses an existing graph"),
head,
))
}
#[cfg(test)]
pub(crate) mod test_util {
use super::*;
use gantz_ca::Datum;
pub(crate) fn nd(v: u32) -> NodeData {
let mut n = NodeData {
tag: "Num".to_string(),
data: Datum::Map(vec![("v".to_string(), Datum::I64(v as i64))]),
refs: vec![],
blobs: vec![],
};
n.canonicalize();
n
}
pub(crate) fn value(n: &NodeData) -> u32 {
match n.data.get("v") {
Some(&Datum::I64(v)) => v as u32,
_ => panic!("not an `nd`-built node: {n:?}"),
}
}
pub(crate) fn test_graph(nodes: &[u32]) -> DataGraph {
let mut g = DataGraph::default();
for &n in nodes {
g.add_node(nd(n));
}
g
}
pub(crate) fn commit_test_graph(
reg: &mut gantz_ca::Registry,
secs: u64,
parent: Option<CommitAddr>,
graph: &DataGraph,
) -> CommitAddr {
let ga = gantz_ca::graph_addr(graph);
let dg = graph.clone();
reg.commit_graph(std::time::Duration::from_secs(secs), parent, ga, || dg)
}
pub(crate) fn diverged_registry(
base: &[u32],
ours: &[u32],
theirs: &[u32],
) -> (gantz_ca::Registry, gantz_ca::Head) {
let mut reg = gantz_ca::Registry::default();
let base_ca = commit_test_graph(&mut reg, 1, None, &test_graph(base));
let ours_ca = commit_test_graph(&mut reg, 2, Some(base_ca), &test_graph(ours));
let theirs_ca = commit_test_graph(&mut reg, 3, Some(base_ca), &test_graph(theirs));
reg.set_head("alpha".parse().unwrap(), ours_ca);
reg.set_head("beta".parse().unwrap(), theirs_ca);
(reg, gantz_ca::Head::Branch("alpha".parse().unwrap()))
}
}
#[cfg(test)]
mod tests {
use super::test_util::*;
use super::*;
use crate::widget::graph_scene::Selection;
use gantz_core::node::graph::NodeIx;
#[test]
fn remove_nodes_migrates_layout_and_selection() {
let mut graph = DataGraph::default();
for w in 10u32..15 {
graph.add_node(nd(w));
}
let mut layout = egui_graph::Layout::default();
for i in 0..5 {
layout.insert(node_id(i), egui::pos2(i as f32, 0.0));
}
let mut selection = Selection::default();
selection.nodes.insert(NodeIx::new(4));
let mut vm = Engine::new_base();
let codec = crate::test_node::codec();
let mut instances = crate::node::NodeInstances::default();
let datas: Vec<_> = (0..5)
.map(|i| {
gantz_core::data::erase_node_typed(
&gantz_core::node::Expr::new(format!("(+ $l {i})")).unwrap(),
)
.unwrap()
})
.collect();
for (i, d) in datas.iter().enumerate() {
let entry = instances.take(&codec, i, d).unwrap();
instances.put(i, entry);
}
let reindex = remove_nodes(
&mut graph,
&mut vm,
&mut layout,
&mut selection,
&mut instances,
[NodeIx::new(1)],
);
assert!(!reindex.is_empty());
assert_eq!(reindex.apply_to_index(4), Some(1));
assert_eq!(reindex.apply_to_index(1), None);
assert_eq!(graph.node_count(), 4);
assert_eq!(value(&graph[NodeIx::new(1)]), 14);
assert_eq!(layout.len(), 4);
assert_eq!(layout.get(&node_id(1)).copied(), Some(egui::pos2(4.0, 0.0)));
assert!(!layout.contains_key(&node_id(4)));
assert_eq!(
selection.nodes.iter().copied().collect::<Vec<_>>(),
vec![NodeIx::new(1)],
);
assert_eq!(instances.len(), 4);
assert!(instances.peek(1, &datas[4]).is_some());
assert!(instances.peek(1, &datas[1]).is_none());
assert!(instances.peek(4, &datas[4]).is_none());
}
#[test]
fn carry_layout_remaps_and_places_new_nodes() {
let mut live = crate::SceneView::default();
live.camera.center = egui::pos2(100.0, 50.0);
live.layout.insert(node_id(0), egui::pos2(1.0, 1.0));
live.layout.insert(node_id(1), egui::pos2(2.0, 2.0));
live.layout.insert(node_id(2), egui::pos2(3.0, 3.0));
let matching: gantz_ca::Matching = [(0, 1), (2, 0)].into_iter().collect();
let view = carry_layout(&live, &matching, 3);
assert_eq!(view.camera, live.camera);
assert_eq!(view.layout.len(), 3);
assert_eq!(
view.layout.get(&node_id(1)).copied(),
Some(egui::pos2(1.0, 1.0))
);
assert_eq!(
view.layout.get(&node_id(0)).copied(),
Some(egui::pos2(3.0, 3.0))
);
assert_eq!(
view.layout.get(&node_id(2)).copied(),
Some(egui::pos2(100.0, 50.0))
);
}
#[test]
fn carry_layout_empty_live_yields_empty() {
let live = crate::SceneView::default();
let matching: gantz_ca::Matching = [(0, 0)].into_iter().collect();
let view = carry_layout(&live, &matching, 4);
assert!(view.layout.is_empty());
}
#[test]
fn carry_layout_no_carried_positions_yields_empty() {
let mut live = crate::SceneView::default();
live.layout.insert(node_id(5), egui::pos2(1.0, 1.0));
let matching = gantz_ca::Matching::new();
let view = carry_layout(&live, &matching, 3);
assert!(view.layout.is_empty());
}
#[test]
fn merged_view_sources_ours_then_theirs_then_cascade() {
let src = |ours: Option<usize>, theirs: Option<usize>| gantz_ca::merge::NodeSrc {
base: None,
ours,
theirs,
};
let mut first = crate::SceneView::default();
first.camera.center = egui::pos2(10.0, 10.0);
first.layout.insert(node_id(0), egui::pos2(1.0, 0.0));
let mut second = crate::SceneView::default();
second.layout.insert(node_id(0), egui::pos2(2.0, 0.0));
second.layout.insert(node_id(1), egui::pos2(3.0, 0.0));
let srcs = [
src(Some(0), Some(0)),
src(None, Some(1)),
src(Some(9), None),
];
let view = merged_view(&srcs, Some(&first), Some(&second));
assert_eq!(view.camera, first.camera);
assert_eq!(
view.layout.get(&node_id(0)).copied(),
Some(egui::pos2(1.0, 0.0))
);
assert_eq!(
view.layout.get(&node_id(1)).copied(),
Some(egui::pos2(3.0, 0.0))
);
assert_eq!(
view.layout.get(&node_id(2)).copied(),
Some(egui::pos2(10.0, 10.0))
);
}
#[test]
fn merged_view_no_views_yields_empty() {
let srcs = [gantz_ca::merge::NodeSrc {
base: None,
ours: Some(0),
theirs: None,
}];
let view = merged_view(&srcs, None, None);
assert!(view.layout.is_empty());
}
fn edge(o: u16, i: u16) -> gantz_core::Edge {
gantz_core::Edge::new(node::Output(o), node::Input(i))
}
fn nest_graph() -> DataGraph {
let mut graph = test_graph(&[10, 11, 12, 13, 14]);
graph.add_edge(NodeIx::new(0), NodeIx::new(1), edge(0, 0));
graph.add_edge(NodeIx::new(2), NodeIx::new(3), edge(0, 0));
graph.add_edge(NodeIx::new(1), NodeIx::new(3), edge(0, 0));
graph.add_edge(NodeIx::new(3), NodeIx::new(4), edge(0, 0));
graph
}
#[test]
fn nest_nodes_moves_subgraph_and_rewires_parent() {
let mut registry = gantz_ca::Registry::default();
let mut graph = nest_graph();
let mut vm = Engine::new_base();
let mut view = crate::SceneView::default();
view.camera.center = egui::pos2(100.0, 50.0);
let mut head_state = OpenHeadState::default();
let selection: HashSet<_> = [NodeIx::new(1), NodeIx::new(3)].into_iter().collect();
head_state.scene.interaction.selection.nodes = selection.clone();
let mut instances = crate::node::NodeInstances::default();
let parent: Name = "alpha".parse().unwrap();
let new_ix = nest_nodes(
&mut registry,
std::time::Duration::from_secs(1),
&mut graph,
&mut vm,
&mut view,
&mut head_state,
&mut instances,
&selection,
&parent,
)
.unwrap();
let weight = &graph[new_ix];
let named = named_ref_of(weight).expect("new node must be a NamedRef");
assert_eq!(named.name().to_string(), "alpha:1");
assert!(named.is_nested());
assert_eq!(graph.node_count(), 4);
assert!(graph.find_edge(NodeIx::new(0), new_ix).is_some());
assert!(graph.find_edge(NodeIx::new(2), new_ix).is_some());
assert!(graph.find_edge(new_ix, NodeIx::new(1)).is_some());
let nested = registry
.commit_graph_ref(®istry.head(&"alpha:1".parse().unwrap()).unwrap())
.unwrap();
assert_eq!(nested.node_count(), 5);
assert!(nested.find_edge(NodeIx::new(0), NodeIx::new(3)).is_some());
assert!(nested.find_edge(NodeIx::new(1), NodeIx::new(4)).is_some());
assert!(nested.find_edge(NodeIx::new(3), NodeIx::new(4)).is_some());
assert!(nested.find_edge(NodeIx::new(4), NodeIx::new(2)).is_some());
assert_eq!(
head_state
.scene
.interaction
.selection
.nodes
.iter()
.copied()
.collect::<Vec<_>>(),
vec![new_ix],
);
}
#[test]
fn nest_nodes_without_boundaries_has_no_sockets() {
let mut registry = gantz_ca::Registry::default();
let mut graph = DataGraph::default();
let a = graph.add_node(nd(0));
let b = graph.add_node(nd(1));
graph.add_edge(a, b, edge(0, 0));
let mut vm = Engine::new_base();
let mut view = crate::SceneView::default();
let mut head_state = OpenHeadState::default();
let selection: HashSet<_> = [a, b].into_iter().collect();
head_state.scene.interaction.selection.nodes = selection.clone();
let mut instances = crate::node::NodeInstances::default();
let parent: Name = "alpha".parse().unwrap();
let new_ix = nest_nodes(
&mut registry,
std::time::Duration::from_secs(1),
&mut graph,
&mut vm,
&mut view,
&mut head_state,
&mut instances,
&selection,
&parent,
)
.unwrap();
assert_eq!(graph.node_count(), 1);
assert!(graph.node_weight(new_ix).is_some());
let nested = registry
.commit_graph_ref(®istry.head(&"alpha:1".parse().unwrap()).unwrap())
.unwrap();
assert_eq!(nested.node_count(), 2);
}
#[test]
fn nest_nodes_empty_selection_is_a_noop() {
let mut registry = gantz_ca::Registry::default();
let mut graph = test_graph(&[10, 11]);
let mut vm = Engine::new_base();
let mut view = crate::SceneView::default();
let mut head_state = OpenHeadState::default();
let mut instances = crate::node::NodeInstances::default();
let parent: Name = "alpha".parse().unwrap();
let result = nest_nodes(
&mut registry,
std::time::Duration::from_secs(1),
&mut graph,
&mut vm,
&mut view,
&mut head_state,
&mut instances,
&HashSet::new(),
&parent,
);
assert!(result.is_none());
assert_eq!(graph.node_count(), 2);
}
}