Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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);
}