facett-graph3d/tests/given_positions.rs
//! **`Layout3D::Given` — the host places the cloud, the crate draws it.**
//!
//! Every other [`Layout3D`] invents positions from the node INDEX or from the edge
//! set: a sphere, a helix, a lattice, a force blob. None of them can express "this
//! node is *here*, because the data says so" — so a consumer that already knows where
//! its nodes belong (korp's road junctions carry a lon/lat, and a metric that earns
//! the third axis) had no way in. Before this the only route was writing through the
//! `pub nodes` field and hoping nothing called [`Graph3D::layout`] again; a
//! [`Msg::SetLayout`] or an [`Graph3D::absorb`] silently overwrote the lot.
//!
//! The four guards below are the whole contract, and each is written so it FAILS if
//! the feature is a no-op:
//!
//! 1. the given positions are the ones drawn — asserted against *distinct* values, so
//! a layout that quietly re-ran cannot pass;
//! 2. a round trip through another layout and BACK restores them (the positions are
//! retained, not consumed);
//! 3. a SHORT position vector does not draw a half-placed lie — it falls back to
//! `Grid` and says so through the observable state + the `facet-graph3d-5` code;
//! 4. `absorb` on a `Given` cloud is the same fallback, because the new tail has no
//! given position either.
use facett_core::Facet;
use facett_graph3d::{Graph3D, Layout3D, Msg};
fn nodes(n: usize) -> Vec<(String, egui::Color32)> {
(0..n).map(|i| (format!("n{i}"), egui::Color32::from_rgb(10, 20, 30))).collect()
}
/// Positions no index-driven layout could ever produce: z climbs by a prime-ish step
/// and x/y sit on a line, so a sphere/helix/grid/force result differs in every slot.
fn given(n: usize) -> Vec<[f32; 3]> {
(0..n)
.map(|i| {
let t = i as f32 / (n.max(2) - 1) as f32;
[t * 2.0 - 1.0, 0.25, (i as f32 * 0.137).sin()]
})
.collect()
}
#[test]
fn given_positions_are_the_positions_drawn() {
let n = 32;
let want = given(n);
let g = Graph3D::new(nodes(n), vec![(0, 1), (1, 2)], Layout3D::Given).with_positions(want.clone());
assert_eq!(g.state().layout, Layout3D::Given, "the layout stayed Given");
for (i, (node, w)) in g.nodes.iter().zip(&want).enumerate() {
assert_eq!(node.pos, *w, "node {i} sits where the host put it");
}
// RED-when-broken: the positions must be genuinely distinct, or an identity
// comparison against a dead feature could pass on a shared constant.
let zs: std::collections::BTreeSet<u32> = g.nodes.iter().map(|nd| nd.pos[2].to_bits()).collect();
assert!(zs.len() > n / 2, "the given z axis carries {} distinct values, not a constant", zs.len());
assert_eq!(g.state_json()["layout"], "given");
assert_eq!(g.state_json()["given_positions"], n, "the state says how many positions the host supplied");
assert!(g.state_json()["layout_fallback"].is_null(), "a complete placement has no fallback");
assert!(g.state_json()["error_code"].is_null());
}
#[test]
fn a_round_trip_through_another_layout_restores_the_given_placement() {
let n = 24;
let want = given(n);
let mut g = Graph3D::new(nodes(n), vec![(0, 1)], Layout3D::Given).with_positions(want.clone());
g.update(Msg::SetLayout(Layout3D::Sphere));
assert!(
g.nodes.iter().zip(&want).any(|(nd, w)| nd.pos != *w),
"the sphere really did move the cloud (otherwise the restore below proves nothing)"
);
g.update(Msg::SetLayout(Layout3D::Given));
for (i, (node, w)) in g.nodes.iter().zip(&want).enumerate() {
assert_eq!(node.pos, *w, "node {i} came back to the host's position");
}
}
#[test]
fn a_short_placement_falls_back_to_the_lattice_and_says_so() {
let n = 20;
// One node short — exactly the shape that would otherwise draw 19 nodes where
// they belong and the 20th at the origin, which looks like data and is not.
let short = given(n - 1);
let g = Graph3D::new(nodes(n), vec![(0, 1), (2, 3)], Layout3D::Given).with_positions(short.clone());
let j = g.state_json();
assert_eq!(j["layout"], "given", "the CHOICE is still Given — the fallback is what was drawn");
assert_eq!(j["layout_fallback"], "grid");
assert_eq!(j["given_positions"], n - 1);
assert_eq!(j["error_code"], "facet-graph3d-5");
assert_eq!(g.severity(), facett_core::Severity::Warning, "a half-placement is a warning, not a silent draw");
// Nothing was left UNPLACED. A lattice cell may legitimately BE the origin, so the
// guard is distinctness: every node got its own cell, which an unplaced tail (all
// at `[0,0,0]`) could never satisfy.
let cells: std::collections::BTreeSet<[u32; 3]> =
g.nodes.iter().map(|nd| [nd.pos[0].to_bits(), nd.pos[1].to_bits(), nd.pos[2].to_bits()]).collect();
assert_eq!(cells.len(), n, "every node got its own lattice cell — none was left stacked at the origin");
assert!(
g.nodes.iter().zip(&short).any(|(nd, w)| nd.pos != *w),
"the drawn cloud is the lattice, not the short list"
);
}
#[test]
fn absorbing_into_a_given_cloud_falls_back_rather_than_stranding_the_tail() {
let n = 16;
let mut g = Graph3D::new(nodes(n), vec![(0, 1)], Layout3D::Given).with_positions(given(n));
g.absorb(nodes(4), vec![(n, n + 1)]);
assert_eq!(g.nodes.len(), n + 4);
assert_eq!(g.state_json()["error_code"], "facet-graph3d-5", "the grown tail has no given position");
assert_eq!(g.state_json()["layout_fallback"], "grid");
let cells: std::collections::BTreeSet<[u32; 3]> =
g.nodes.iter().map(|nd| [nd.pos[0].to_bits(), nd.pos[1].to_bits(), nd.pos[2].to_bits()]).collect();
assert_eq!(cells.len(), n + 4, "the grown cloud is a lattice, not a pile at the origin");
// Supplying the full placement again clears it — the fallback is a state, not a latch.
g.set_positions(given(n + 4));
assert!(g.state_json()["error_code"].is_null());
assert_eq!(g.state_json()["layout_fallback"], serde_json::Value::Null);
}