444 lines
16 KiB
Rust
444 lines
16 KiB
Rust
use super::*;
|
|
|
|
struct Distributed {
|
|
f: Fixture,
|
|
a: Id,
|
|
b: Id,
|
|
x: Id,
|
|
y: Id,
|
|
internal: Id,
|
|
}
|
|
|
|
fn distributed(layer: Layer, folded: bool) -> Distributed {
|
|
let mut f = fixture(layer);
|
|
let kind = f.p.elements[&f.w].kind();
|
|
let x = f.p.add_element(kind, layer, "X", Some(f.w)).unwrap();
|
|
let y = f.p.add_element(kind, layer, "Y", Some(f.w)).unwrap();
|
|
let outside = f.p.add_element(kind, layer, "Outside", None).unwrap();
|
|
let xn = f.p.place(f.d, x, xy(140., 200.));
|
|
let yn = f.p.place(f.d, y, xy(940., 200.));
|
|
f.p.place(f.d, outside, xy(1800., 800.));
|
|
f.p.relate(RelationKind::Allocation, "A owns X", f.a, x);
|
|
f.p.relate(RelationKind::Allocation, "B owns Y", f.b, y);
|
|
let exchange = match layer {
|
|
Layer::Oa => RelationKind::Interaction,
|
|
Layer::Sa => RelationKind::FunctionalExchange,
|
|
};
|
|
let internal = f.p.relate(exchange, "X to Y", x, y);
|
|
let external = f.p.relate(exchange, "Y to Outside", y, outside);
|
|
let d = f.p.diagrams.get_mut(&f.d).unwrap();
|
|
let a = d.nodes.values().find(|n| n.element == f.a).unwrap().id;
|
|
let b = d.nodes.values().find(|n| n.element == f.b).unwrap().id;
|
|
d.nodes.get_mut(&a).unwrap().collapsed = folded;
|
|
d.nodes.get_mut(&f.n).unwrap().position = xy(100., 800.);
|
|
d.routes = vec![route(internal), route(external)];
|
|
validate_candidate(&f.p).unwrap();
|
|
Distributed {
|
|
f,
|
|
a,
|
|
b,
|
|
x: xn,
|
|
y: yn,
|
|
internal,
|
|
}
|
|
}
|
|
|
|
fn identities(m: &Distributed, ids: &[Id]) -> BTreeMap<Id, Id> {
|
|
ids.iter()
|
|
.map(|id| (*id, m.f.p.diagrams[&m.f.d].nodes[id].element))
|
|
.collect()
|
|
}
|
|
|
|
fn expected(m: &Distributed, include_a: bool) -> Project {
|
|
let mut p = m.f.p.clone();
|
|
let d = p.diagrams.get_mut(&m.f.d).unwrap();
|
|
for (id, position) in [
|
|
(m.f.n, xy(500., 800.)),
|
|
(m.x, xy(540., 200.)),
|
|
(m.y, xy(1340., 200.)),
|
|
] {
|
|
d.nodes.get_mut(&id).unwrap().position = position;
|
|
}
|
|
if include_a {
|
|
d.nodes.get_mut(&m.a).unwrap().position = xy(500., 100.);
|
|
}
|
|
d.routes
|
|
.iter_mut()
|
|
.find(|r| r.edge == m.internal)
|
|
.unwrap()
|
|
.waypoints = vec![xy(820., 400.), xy(820., 550.)];
|
|
p
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_arrange_distributed_oa_sa_exact_scope_routes_source_history_and_anchor() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
for folded in [false, true] {
|
|
for include_a in [false, true] {
|
|
let m = distributed(layer, folded);
|
|
let members = if include_a {
|
|
vec![m.f.n, m.a]
|
|
} else {
|
|
vec![m.f.n]
|
|
};
|
|
let selected = identities(&m, &members);
|
|
for anchor in selected.keys().copied() {
|
|
let plan = arrange::capture(&m.f.p, m.f.d, &selected, anchor).unwrap();
|
|
assert_eq!(plan.selected(), &selected);
|
|
let mut affected = BTreeSet::from([m.f.n, m.x, m.y]);
|
|
if include_a {
|
|
affected.insert(m.a);
|
|
}
|
|
assert_eq!(
|
|
plan.affected().keys().copied().collect::<BTreeSet<_>>(),
|
|
affected
|
|
);
|
|
let requested = if anchor == m.a {
|
|
xy(503., 102.)
|
|
} else {
|
|
xy(503., 802.)
|
|
};
|
|
let placed =
|
|
arrange::place(&plan, requested, presentation(&m.f.p, m.f.d, None))
|
|
.unwrap();
|
|
assert_eq!(placed.delta, xy(400., 0.));
|
|
assert_eq!(placed.unfolded, None);
|
|
let command = arrange::finish(&plan, &m.f.p, &placed).unwrap().unwrap();
|
|
assert_eq!(roundtrip(&m.f.p, command), expected(&m, include_a));
|
|
// Explicit Arrange does not widen ordinary distributed assignment.
|
|
assert!(matches!(
|
|
plan_semantics(
|
|
&m.f.p,
|
|
&scope(&m.f),
|
|
Destination::Performer(m.f.a),
|
|
&BTreeMap::new()
|
|
),
|
|
Err(Refusal::MixedLayoutRequired)
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_arrange_performer_closure_does_not_acquire_incidental_semantic_root() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let m = distributed(layer, true);
|
|
let selected = identities(&m, &[m.a]);
|
|
let plan = arrange::capture(&m.f.p, m.f.d, &selected, m.a).unwrap();
|
|
assert_eq!(
|
|
plan.affected().keys().copied().collect::<BTreeSet<_>>(),
|
|
BTreeSet::from([m.a, m.x])
|
|
);
|
|
let placed =
|
|
arrange::place(&plan, xy(500., 100.), presentation(&m.f.p, m.f.d, None)).unwrap();
|
|
let mut expected = m.f.p.clone();
|
|
let d = expected.diagrams.get_mut(&m.f.d).unwrap();
|
|
d.nodes.get_mut(&m.a).unwrap().position = xy(500., 100.);
|
|
d.nodes.get_mut(&m.x).unwrap().position = xy(540., 200.);
|
|
// X-to-Y is now one-ended; all saved pins remain byte-for-byte unchanged.
|
|
assert_eq!(
|
|
roundtrip(
|
|
&m.f.p,
|
|
arrange::finish(&plan, &m.f.p, &placed).unwrap().unwrap()
|
|
),
|
|
expected
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_arrange_validates_all_original_members_before_normalizing() {
|
|
let mut m = distributed(Layer::Oa, false);
|
|
let selected = identities(&m, &[m.f.n, m.x]);
|
|
let plan = arrange::capture(&m.f.p, m.f.d, &selected, m.x).unwrap();
|
|
assert_eq!(plan.selected(), &selected);
|
|
assert_eq!(plan.affected().len(), 3);
|
|
let placed = arrange::place(&plan, xy(540., 200.), presentation(&m.f.p, m.f.d, None)).unwrap();
|
|
assert_eq!(
|
|
roundtrip(
|
|
&m.f.p,
|
|
arrange::finish(&plan, &m.f.p, &placed).unwrap().unwrap()
|
|
),
|
|
expected(&m, false)
|
|
);
|
|
let mut stale = selected.clone();
|
|
stale.insert(m.x, m.f.w);
|
|
assert!(arrange::capture(&m.f.p, m.f.d, &stale, m.f.n).is_err());
|
|
m.f.p
|
|
.diagrams
|
|
.get_mut(&m.f.d)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&m.a)
|
|
.unwrap()
|
|
.collapsed = true;
|
|
assert!(
|
|
arrange::capture(&m.f.p, m.f.d, &selected, m.f.n).is_err(),
|
|
"covered selected X is hidden and cannot disappear in normalization"
|
|
);
|
|
assert!(arrange::capture(&m.f.p, m.f.d, &BTreeMap::new(), m.f.n).is_err());
|
|
assert!(arrange::capture(&m.f.p, m.f.d, &identities(&m, &[m.f.n]), m.a).is_err());
|
|
assert!(arrange::capture(&m.f.p, m.f.second, &identities(&m, &[m.f.n]), m.f.n).is_err());
|
|
let foreign = Id::new_v4();
|
|
assert!(
|
|
arrange::capture(
|
|
&m.f.p,
|
|
m.f.d,
|
|
&BTreeMap::from([(m.f.n, m.f.w), (foreign, m.f.w)]),
|
|
m.f.n
|
|
)
|
|
.is_err()
|
|
);
|
|
m.f.p.diagrams.get_mut(&m.f.d).unwrap().projection = ProjectionKind::Overview;
|
|
assert!(arrange::capture(&m.f.p, m.f.d, &identities(&m, &[m.f.n]), m.f.n).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_arrange_simultaneous_roots_may_enter_each_others_vacated_bounds() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let mut f = fixture(layer);
|
|
let kind = f.p.elements[&f.w].kind();
|
|
let y = f.p.add_element(kind, layer, "Second root", None).unwrap();
|
|
let yn = f.p.place(f.d, y, xy(400., 500.));
|
|
let relation = f.p.relate(
|
|
if layer == Layer::Oa {
|
|
RelationKind::Interaction
|
|
} else {
|
|
RelationKind::FunctionalExchange
|
|
},
|
|
"Between roots",
|
|
f.w,
|
|
y,
|
|
);
|
|
f.p.diagrams.get_mut(&f.d).unwrap().routes = vec![route(relation)];
|
|
let context = presentation(&f.p, f.d, None);
|
|
let single = arrange::capture(&f.p, f.d, &BTreeMap::from([(f.n, f.w)]), f.n).unwrap();
|
|
assert!(
|
|
arrange::place(&single, xy(400., 500.), context).is_err(),
|
|
"singleton would collide with stationary Y"
|
|
);
|
|
let plan =
|
|
arrange::capture(&f.p, f.d, &BTreeMap::from([(f.n, f.w), (yn, y)]), f.n).unwrap();
|
|
let placed = arrange::place(&plan, xy(400., 500.), context).unwrap();
|
|
let mut expected = f.p.clone();
|
|
let d = expected.diagrams.get_mut(&f.d).unwrap();
|
|
d.nodes.get_mut(&f.n).unwrap().position = xy(400., 500.);
|
|
d.nodes.get_mut(&yn).unwrap().position = xy(700., 500.);
|
|
d.routes[0].waypoints = vec![xy(720., 400.), xy(720., 550.)];
|
|
assert_eq!(
|
|
roundtrip(
|
|
&f.p,
|
|
arrange::finish(&plan, &f.p, &placed).unwrap().unwrap()
|
|
),
|
|
expected
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_arrange_preserves_roles_and_does_not_invent_unplaced_descendants() {
|
|
let mut f = fixture(Layer::Oa);
|
|
let a = f.a;
|
|
role(&mut f, Some(a));
|
|
f.p.diagrams
|
|
.get_mut(&f.d)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&f.n)
|
|
.unwrap()
|
|
.position = xy(150., 200.);
|
|
let plan = arrange::capture(&f.p, f.d, &BTreeMap::from([(f.n, f.w)]), f.n).unwrap();
|
|
let placed = arrange::place(&plan, xy(160., 210.), presentation(&f.p, f.d, None)).unwrap();
|
|
let mut role_expected = f.p.clone();
|
|
role_expected
|
|
.diagrams
|
|
.get_mut(&f.d)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&f.n)
|
|
.unwrap()
|
|
.position = xy(160., 210.);
|
|
assert_eq!(
|
|
roundtrip(
|
|
&f.p,
|
|
arrange::finish(&plan, &f.p, &placed).unwrap().unwrap()
|
|
),
|
|
role_expected
|
|
);
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let mut m = distributed(layer, true);
|
|
let kind = m.f.p.elements[&m.f.w].kind();
|
|
m.f.p
|
|
.add_element(kind, layer, "Unplaced leaf", Some(m.f.w))
|
|
.unwrap();
|
|
let plan = arrange::capture(&m.f.p, m.f.d, &identities(&m, &[m.f.n]), m.f.n).unwrap();
|
|
assert_eq!(plan.affected().len(), 3);
|
|
let placed =
|
|
arrange::place(&plan, xy(500., 800.), presentation(&m.f.p, m.f.d, None)).unwrap();
|
|
assert_eq!(
|
|
roundtrip(
|
|
&m.f.p,
|
|
arrange::finish(&plan, &m.f.p, &placed).unwrap().unwrap()
|
|
),
|
|
expected(&m, false)
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_arrange_detects_hidden_moved_child_and_stationary_enclosure_growth() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
for hidden in [false, true] {
|
|
let mut m = distributed(layer, hidden);
|
|
let kind = m.f.p.elements[&m.f.w].kind();
|
|
let z = m.f.p.add_element(kind, layer, "Obstacle", None).unwrap();
|
|
let zn = m.f.p.place(
|
|
m.f.d,
|
|
z,
|
|
if hidden {
|
|
xy(550., 230.)
|
|
} else {
|
|
xy(1200., 100.)
|
|
},
|
|
);
|
|
// The independent candidate differs only by literal admitted edits.
|
|
// Establish which geometric case the real planner must reject.
|
|
let candidate = expected(&m, true);
|
|
let before = projection::project_view(&m.f.p, m.f.d);
|
|
let after = projection::project_view(&candidate, m.f.d);
|
|
let find =
|
|
|view: &ProjectedView, id| view.nodes.iter().find(|n| n.id == id).unwrap().clone();
|
|
if hidden {
|
|
assert!(!overlaps(&before, &after));
|
|
assert!(!after.nodes.iter().any(|n| n.id == m.x));
|
|
let mut diagnostic = candidate.diagrams[&m.f.d].clone();
|
|
diagnostic.nodes.get_mut(&m.a).unwrap().collapsed = false;
|
|
let expanded = projection::project_diagram(&candidate, &diagnostic);
|
|
assert_eq!(
|
|
overlap(&find(&expanded, m.x), &find(&expanded, zn)),
|
|
if layer == Layer::Oa { 13696. } else { 20116. }
|
|
);
|
|
} else {
|
|
assert_eq!(overlap(&find(&before, m.b), &find(&before, zn)), 0.);
|
|
assert_eq!(overlap(&find(&after, m.b), &find(&after, zn)), 21056.);
|
|
for id in [m.a, m.f.n, m.x, m.y] {
|
|
assert_eq!(overlap(&find(&after, id), &find(&after, zn)), 0.);
|
|
}
|
|
assert_eq!(
|
|
candidate.diagrams[&m.f.d].nodes[&m.b],
|
|
m.f.p.diagrams[&m.f.d].nodes[&m.b]
|
|
);
|
|
}
|
|
let plan =
|
|
arrange::capture(&m.f.p, m.f.d, &identities(&m, &[m.a, m.f.n]), m.f.n).unwrap();
|
|
assert!(matches!(
|
|
arrange::place(&plan, xy(500., 800.), presentation(&m.f.p, m.f.d, None)),
|
|
Err(Refusal::LayoutRequired(_))
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_arrange_preserves_existing_overlap_but_refuses_its_growth() {
|
|
let mut f = fixture(Layer::Oa);
|
|
let y =
|
|
f.p.add_element(Kind::Activity, Layer::Oa, "Existing overlap", None)
|
|
.unwrap();
|
|
let yn = f.p.place(f.d, y, xy(250., 500.));
|
|
let context = presentation(&f.p, f.d, None);
|
|
let single = arrange::capture(&f.p, f.d, &BTreeMap::from([(f.n, f.w)]), f.n).unwrap();
|
|
assert!(arrange::place(&single, xy(110., 500.), context).is_err());
|
|
let plan = arrange::capture(&f.p, f.d, &BTreeMap::from([(f.n, f.w), (yn, y)]), f.n).unwrap();
|
|
let placed = arrange::place(&plan, xy(400., 500.), context).unwrap();
|
|
let mut expected = f.p.clone();
|
|
let d = expected.diagrams.get_mut(&f.d).unwrap();
|
|
d.nodes.get_mut(&f.n).unwrap().position = xy(400., 500.);
|
|
d.nodes.get_mut(&yn).unwrap().position = xy(550., 500.);
|
|
assert_eq!(
|
|
roundtrip(
|
|
&f.p,
|
|
arrange::finish(&plan, &f.p, &placed).unwrap().unwrap()
|
|
),
|
|
expected
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_arrange_preserves_unrelated_folded_interior_diagnostic_boundary() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let mut m = distributed(layer, true);
|
|
let performer = if layer == Layer::Oa {
|
|
Kind::Actor
|
|
} else {
|
|
Kind::SystemActor
|
|
};
|
|
let d = m.f.p.add_element(performer, layer, "D", None).unwrap();
|
|
let z =
|
|
m.f.p
|
|
.add_element(m.f.p.elements[&m.f.w].kind(), layer, "Hidden Z", None)
|
|
.unwrap();
|
|
m.f.p.relate(RelationKind::Allocation, "D owns Z", d, z);
|
|
let dn = m.f.p.place(m.f.d, d, xy(2500., 100.));
|
|
m.f.p.place(m.f.d, z, xy(550., 230.));
|
|
m.f.p
|
|
.diagrams
|
|
.get_mut(&m.f.d)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&dn)
|
|
.unwrap()
|
|
.collapsed = true;
|
|
let plan = arrange::capture(&m.f.p, m.f.d, &identities(&m, &[m.a, m.f.n]), m.f.n).unwrap();
|
|
let placed =
|
|
arrange::place(&plan, xy(500., 800.), presentation(&m.f.p, m.f.d, None)).unwrap();
|
|
assert_eq!(
|
|
roundtrip(
|
|
&m.f.p,
|
|
arrange::finish(&plan, &m.f.p, &placed).unwrap().unwrap()
|
|
),
|
|
expected(&m, true)
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_arrange_noop_stale_tamper_target_and_all_selected_headers() {
|
|
let m = distributed(Layer::Sa, false);
|
|
let selected = identities(&m, &[m.a, m.f.n]);
|
|
let plan = arrange::capture(&m.f.p, m.f.d, &selected, m.f.n).unwrap();
|
|
let context = presentation(&m.f.p, m.f.d, None);
|
|
let noop = arrange::place(&plan, xy(103., 802.), context).unwrap();
|
|
assert!(arrange::finish(&plan, &m.f.p, &noop).unwrap().is_none());
|
|
let mut placed = arrange::place(&plan, xy(500., 800.), context).unwrap();
|
|
let mut stale = m.f.p.clone();
|
|
stale.name.push_str(" changed");
|
|
assert!(matches!(
|
|
arrange::finish(&plan, &stale, &placed),
|
|
Err(Refusal::StaleScope)
|
|
));
|
|
placed.diagram.nodes.get_mut(&m.b).unwrap().position.x += 1.;
|
|
assert!(matches!(
|
|
arrange::finish(&plan, &m.f.p, &placed),
|
|
Err(Refusal::StaleScope)
|
|
));
|
|
assert!(
|
|
arrange::place(
|
|
&plan,
|
|
xy(500., 800.),
|
|
presentation(&m.f.p, m.f.d, Some(m.f.b))
|
|
)
|
|
.is_err()
|
|
);
|
|
let mut clipped = context;
|
|
clipped.viewport.position = xy(400., 700.);
|
|
assert!(
|
|
arrange::place(&plan, xy(500., 800.), clipped).is_err(),
|
|
"anchor C visible, other selected A header clipped"
|
|
);
|
|
assert!(arrange::place(&plan, xy(f32::NAN, 800.), context).is_err());
|
|
assert!(arrange::place(&plan, xy(MAX_COORDINATE, 800.), context).is_err());
|
|
}
|