Probe dependency-aware ordering across container boundaries
This commit is contained in:
@@ -5,6 +5,8 @@ use oa_core::{
|
|||||||
};
|
};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
#[path = "auto_layout_probe/boundary_fixtures.rs"]
|
||||||
|
mod boundary_fixtures;
|
||||||
#[path = "auto_layout_probe/clearance.rs"]
|
#[path = "auto_layout_probe/clearance.rs"]
|
||||||
mod clearance;
|
mod clearance;
|
||||||
#[path = "auto_layout_probe/condensed.rs"]
|
#[path = "auto_layout_probe/condensed.rs"]
|
||||||
@@ -411,6 +413,7 @@ fn metrics(project: &Project, diagram: Id) -> Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
struct Builder {
|
struct Builder {
|
||||||
project: Project,
|
project: Project,
|
||||||
diagram: Id,
|
diagram: Id,
|
||||||
@@ -1252,17 +1255,35 @@ fn repair_placement_cli(project_file: &std::path::Path, diagram_text: &std::ffi:
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn scc_observe(name: &str, f: Builder) -> Value {
|
fn scc_observe(name: &str, f: Builder) -> Value {
|
||||||
|
placement_observe(name, f, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placement_observe(name: &str, f: Builder, boundary_order: bool) -> Value {
|
||||||
let before = f.project;
|
let before = f.project;
|
||||||
let diagram = f.diagram;
|
let diagram = f.diagram;
|
||||||
validate_candidate(&before).expect("Validate SCC input");
|
validate_candidate(&before).expect("Validate SCC input");
|
||||||
let candidate = match condensed::candidate_with_diagnostics(&before, diagram) {
|
let with_diagnostics = |p: &Project| {
|
||||||
|
if boundary_order {
|
||||||
|
condensed::boundary_candidate_with_diagnostics(p, diagram)
|
||||||
|
} else {
|
||||||
|
condensed::candidate_with_diagnostics(p, diagram)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let propose = |p: &Project| {
|
||||||
|
if boundary_order {
|
||||||
|
condensed::boundary_candidate_with_diagnostics(p, diagram).map(|(p, _)| p)
|
||||||
|
} else {
|
||||||
|
condensed::candidate(p, diagram)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let candidate = match with_diagnostics(&before) {
|
||||||
Err(error) => json!({"supported":false,"error":error.to_string()}),
|
Err(error) => json!({"supported":false,"error":error.to_string()}),
|
||||||
Ok((after, plan)) => {
|
Ok((after, plan)) => {
|
||||||
let same = condensed::candidate(&before, diagram);
|
let same = propose(&before);
|
||||||
let mut repeated = after.clone();
|
let mut repeated = after.clone();
|
||||||
let mut stability = Vec::new();
|
let mut stability = Vec::new();
|
||||||
for pass in 2..=10 {
|
for pass in 2..=10 {
|
||||||
match condensed::candidate(&repeated, diagram) {
|
match propose(&repeated) {
|
||||||
Ok(next) => {
|
Ok(next) => {
|
||||||
stability.push(json!({"pass":pass,"equal_previous":next==repeated,"equal_first":next==after}));
|
stability.push(json!({"pass":pass,"equal_previous":next==repeated,"equal_first":next==after}));
|
||||||
repeated = next;
|
repeated = next;
|
||||||
@@ -1283,7 +1304,11 @@ fn scc_observe(name: &str, f: Builder) -> Value {
|
|||||||
"before":metrics(&before,diagram),"candidate":candidate})
|
"before":metrics(&before,diagram),"candidate":candidate})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scc_placement_cli(project_file: &std::path::Path, diagram_text: &std::ffi::OsStr) {
|
fn scc_placement_cli(
|
||||||
|
project_file: &std::path::Path,
|
||||||
|
diagram_text: &std::ffi::OsStr,
|
||||||
|
boundary_order: bool,
|
||||||
|
) {
|
||||||
let project: Project =
|
let project: Project =
|
||||||
serde_json::from_slice(&std::fs::read(project_file).expect("Read Project"))
|
serde_json::from_slice(&std::fs::read(project_file).expect("Read Project"))
|
||||||
.expect("Decode Project");
|
.expect("Decode Project");
|
||||||
@@ -1294,14 +1319,15 @@ fn scc_placement_cli(project_file: &std::path::Path, diagram_text: &std::ffi::Os
|
|||||||
.expect("Diagram UUID");
|
.expect("Diagram UUID");
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
scc_observe(
|
placement_observe(
|
||||||
"external_case",
|
"external_case",
|
||||||
Builder {
|
Builder {
|
||||||
project,
|
project,
|
||||||
diagram,
|
diagram,
|
||||||
next: 0,
|
next: 0,
|
||||||
witness: None
|
witness: None
|
||||||
}
|
},
|
||||||
|
boundary_order,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1416,7 +1442,11 @@ fn main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if args.len() == 4 && args[1] == "--scc-placement" {
|
if args.len() == 4 && args[1] == "--scc-placement" {
|
||||||
scc_placement_cli(std::path::Path::new(&args[2]), &args[3]);
|
scc_placement_cli(std::path::Path::new(&args[2]), &args[3], false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if args.len() == 4 && args[1] == "--boundary-placement" {
|
||||||
|
scc_placement_cli(std::path::Path::new(&args[2]), &args[3], true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if args.len() == 2 && args[1] == "--scc-fixtures" {
|
if args.len() == 2 && args[1] == "--scc-fixtures" {
|
||||||
@@ -1425,10 +1455,27 @@ fn main() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if args.len() == 2 && args[1] == "--boundary-fixtures" {
|
||||||
|
for (name, f) in fixtures() {
|
||||||
|
println!("{}", placement_observe(name, f, true));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if args.len() == 2 && args[1] == "--boundary-extra-fixtures" {
|
||||||
|
for (name, f) in boundary_fixtures::fixtures() {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
json!({"fixture":name,
|
||||||
|
"baseline":scc_observe(name, f.clone()),
|
||||||
|
"boundary":placement_observe(name, f, true)})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
args.len(),
|
args.len(),
|
||||||
1,
|
1,
|
||||||
"Usage: auto_layout_probe [--route-corpus MANIFEST | --arrange-corpus MANIFEST | --import-placement PROJECT UUID PLAN | --repair-placement PROJECT UUID | --scc-placement PROJECT UUID | --scc-fixtures]"
|
"Usage: auto_layout_probe [--route-corpus MANIFEST | --arrange-corpus MANIFEST | --import-placement PROJECT UUID PLAN | --repair-placement PROJECT UUID | --scc-placement PROJECT UUID | --scc-fixtures | --boundary-placement PROJECT UUID | --boundary-fixtures | --boundary-extra-fixtures]"
|
||||||
);
|
);
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
//! Independent structural falsifiers for incoming boundary ordering.
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn pair(f: &mut Builder, name: &str, x: f32) -> (Id, Id, Id) {
|
||||||
|
let parent = f.function(name, None, x, 40.0);
|
||||||
|
let a = f.function(&format!("{name} A"), Some(parent), x + 60.0, 130.0);
|
||||||
|
let b = f.function(&format!("{name} B"), Some(parent), x + 60.0, 330.0);
|
||||||
|
(parent, a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn fixtures() -> Vec<(&'static str, Builder)> {
|
||||||
|
let mut all = Vec::new();
|
||||||
|
let mut f = Builder::new("boundary_opposing_parents");
|
||||||
|
let (_, la, lb) = pair(&mut f, "Left", 40.0);
|
||||||
|
let (_, ra, rb) = pair(&mut f, "Right", 820.0);
|
||||||
|
for (name, a, b) in [
|
||||||
|
("Reverse upper", la, rb),
|
||||||
|
("Reverse lower", lb, ra),
|
||||||
|
("Return upper", ra, la),
|
||||||
|
("Return lower", rb, lb),
|
||||||
|
] {
|
||||||
|
f.exchange(name, a, b);
|
||||||
|
}
|
||||||
|
all.push(("boundary_opposing_parents", f));
|
||||||
|
|
||||||
|
let mut f = Builder::new("boundary_three_parent_chain");
|
||||||
|
let (_, la, lb) = pair(&mut f, "Left", 40.0);
|
||||||
|
let (middle, ma, mb) = pair(&mut f, "Middle", 820.0);
|
||||||
|
let (_, ra, rb) = pair(&mut f, "Right", 1600.0);
|
||||||
|
for (name, a, b) in [
|
||||||
|
("Left upper", la, mb),
|
||||||
|
("Left lower", lb, ma),
|
||||||
|
("Middle upper", mb, ra),
|
||||||
|
("Middle lower", ma, rb),
|
||||||
|
] {
|
||||||
|
f.exchange(name, a, b);
|
||||||
|
}
|
||||||
|
let mut translated = f.clone();
|
||||||
|
for element in [middle, ma, mb] {
|
||||||
|
let node = translated.node(element);
|
||||||
|
let position = &mut translated
|
||||||
|
.project
|
||||||
|
.diagrams
|
||||||
|
.get_mut(&translated.diagram)
|
||||||
|
.unwrap()
|
||||||
|
.nodes
|
||||||
|
.get_mut(&node)
|
||||||
|
.unwrap()
|
||||||
|
.position;
|
||||||
|
position.x += 300.0;
|
||||||
|
position.y += 500.0;
|
||||||
|
}
|
||||||
|
let mut permuted = f.clone();
|
||||||
|
let ids: Vec<_> = permuted.project.relations.keys().copied().collect();
|
||||||
|
for (i, id) in ids.iter().enumerate() {
|
||||||
|
reidentify(
|
||||||
|
&mut permuted.project,
|
||||||
|
*id,
|
||||||
|
Id::from_u128(1_000_000 + i as u128),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
for (i, id) in ids.iter().rev().enumerate() {
|
||||||
|
reidentify(
|
||||||
|
&mut permuted.project,
|
||||||
|
Id::from_u128(1_000_000 + i as u128),
|
||||||
|
*id,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
all.push(("boundary_three_parent_chain", f));
|
||||||
|
all.push(("boundary_three_parent_translated", translated));
|
||||||
|
all.push(("boundary_three_parent_edge_ids_reversed", permuted));
|
||||||
|
|
||||||
|
let mut f = Builder::new("boundary_two_local_sccs");
|
||||||
|
let (_, sa, sb) = pair(&mut f, "Source", 40.0);
|
||||||
|
let target = f.function("Target", None, 820.0, 40.0);
|
||||||
|
let x1 = f.function("X1", Some(target), 880.0, 130.0);
|
||||||
|
let x2 = f.function("X2", Some(target), 1200.0, 130.0);
|
||||||
|
let y1 = f.function("Y1", Some(target), 880.0, 330.0);
|
||||||
|
let y2 = f.function("Y2", Some(target), 1200.0, 330.0);
|
||||||
|
let z = f.function("Z", Some(target), 1520.0, 230.0);
|
||||||
|
for (name, a, b) in [
|
||||||
|
("X forward", x1, x2),
|
||||||
|
("X back", x2, x1),
|
||||||
|
("Y forward", y1, y2),
|
||||||
|
("Y back", y2, y1),
|
||||||
|
("X result", x2, z),
|
||||||
|
("Y result", y2, z),
|
||||||
|
("Upper boundary", sa, y1),
|
||||||
|
("Lower boundary", sb, x1),
|
||||||
|
] {
|
||||||
|
f.exchange(name, a, b);
|
||||||
|
}
|
||||||
|
all.push(("boundary_two_local_sccs", f));
|
||||||
|
|
||||||
|
let mut f = super::fixtures()
|
||||||
|
.into_iter()
|
||||||
|
.find(|(name, _)| *name == "two_containers_reversed_children")
|
||||||
|
.unwrap()
|
||||||
|
.1;
|
||||||
|
let relations: Vec<_> = f.project.relations.values().cloned().collect();
|
||||||
|
for relation in relations {
|
||||||
|
if let RelationBody::FunctionalExchange {
|
||||||
|
source_port,
|
||||||
|
target_port,
|
||||||
|
..
|
||||||
|
} = relation.body
|
||||||
|
{
|
||||||
|
for (port, side, offset) in [
|
||||||
|
(source_port, PortAnchorSide::Right, 0.3),
|
||||||
|
(target_port, PortAnchorSide::Left, 0.7),
|
||||||
|
] {
|
||||||
|
let owner = f.project.elements[&port].parent.unwrap();
|
||||||
|
let node = f.node(owner);
|
||||||
|
f.project = prepare_command(
|
||||||
|
&f.project,
|
||||||
|
&Command::SetPortAnchor {
|
||||||
|
diagram: f.diagram,
|
||||||
|
node,
|
||||||
|
anchor: PortAnchor { port, side, offset },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.project;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
all.push(("boundary_two_containers_anchored", f));
|
||||||
|
all
|
||||||
|
}
|
||||||
@@ -7,6 +7,9 @@ const LIMIT: usize = 20_000_000;
|
|||||||
const PAD: f32 = 32.0;
|
const PAD: f32 = 32.0;
|
||||||
const SPAN: f32 = 100_000.0;
|
const SPAN: f32 = 100_000.0;
|
||||||
|
|
||||||
|
#[path = "condensed/boundary.rs"]
|
||||||
|
mod boundary;
|
||||||
|
|
||||||
fn error(text: &str) -> ModelError {
|
fn error(text: &str) -> ModelError {
|
||||||
ModelError(format!("SCC placement: {text}"))
|
ModelError(format!("SCC placement: {text}"))
|
||||||
}
|
}
|
||||||
@@ -161,6 +164,21 @@ pub fn candidate(project: &Project, diagram: Id) -> Result<Project, ModelError>
|
|||||||
pub fn candidate_with_diagnostics(
|
pub fn candidate_with_diagnostics(
|
||||||
project: &Project,
|
project: &Project,
|
||||||
diagram: Id,
|
diagram: Id,
|
||||||
|
) -> Result<(Project, serde_json::Value), ModelError> {
|
||||||
|
candidate_policy(project, diagram, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn boundary_candidate_with_diagnostics(
|
||||||
|
project: &Project,
|
||||||
|
diagram: Id,
|
||||||
|
) -> Result<(Project, serde_json::Value), ModelError> {
|
||||||
|
candidate_policy(project, diagram, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn candidate_policy(
|
||||||
|
project: &Project,
|
||||||
|
diagram: Id,
|
||||||
|
order_boundaries: bool,
|
||||||
) -> Result<(Project, serde_json::Value), ModelError> {
|
) -> Result<(Project, serde_json::Value), ModelError> {
|
||||||
let d = project
|
let d = project
|
||||||
.diagrams
|
.diagrams
|
||||||
@@ -217,6 +235,7 @@ pub fn candidate_with_diagnostics(
|
|||||||
let mut sizes: BTreeMap<_, _> = view.nodes.iter().map(|n| (n.id, n.size)).collect();
|
let mut sizes: BTreeMap<_, _> = view.nodes.iter().map(|n| (n.id, n.size)).collect();
|
||||||
let mut offsets = BTreeMap::new();
|
let mut offsets = BTreeMap::new();
|
||||||
let mut diagnostics = Vec::new();
|
let mut diagnostics = Vec::new();
|
||||||
|
let mut boundary_groups = Vec::new();
|
||||||
let mut groups: Vec<_> = children.keys().copied().collect();
|
let mut groups: Vec<_> = children.keys().copied().collect();
|
||||||
groups.sort_by_key(|p| std::cmp::Reverse(p.map_or(0, |id| depth[&id] + 1)));
|
groups.sort_by_key(|p| std::cmp::Reverse(p.map_or(0, |id| depth[&id] + 1)));
|
||||||
for parent in groups {
|
for parent in groups {
|
||||||
@@ -361,6 +380,7 @@ pub fn candidate_with_diagnostics(
|
|||||||
height: 0.0,
|
height: 0.0,
|
||||||
};
|
};
|
||||||
let mut prior_order = vec![0usize; comps.len()];
|
let mut prior_order = vec![0usize; comps.len()];
|
||||||
|
let mut bands = Vec::new();
|
||||||
for rank in 0..=max_rank {
|
for rank in 0..=max_rank {
|
||||||
let mut layer: Vec<_> = (0..comps.len()).filter(|&c| ranks[c] == rank).collect();
|
let mut layer: Vec<_> = (0..comps.len()).filter(|&c| ranks[c] == rank).collect();
|
||||||
// Fixed one-pass proximity order, including undirected links, without
|
// Fixed one-pass proximity order, including undirected links, without
|
||||||
@@ -426,6 +446,12 @@ pub fn candidate_with_diagnostics(
|
|||||||
}
|
}
|
||||||
let mut y = 0.0;
|
let mut y = 0.0;
|
||||||
let mut width = 0.0_f32;
|
let mut width = 0.0_f32;
|
||||||
|
if order_boundaries && parent.is_some() {
|
||||||
|
bands.push(boundary::Band {
|
||||||
|
components: layer.clone(),
|
||||||
|
gap: cut.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
for (i, c) in layer.into_iter().enumerate() {
|
for (i, c) in layer.into_iter().enumerate() {
|
||||||
prior_order[c] = i;
|
prior_order[c] = i;
|
||||||
block_positions[c] = Position { x, y };
|
block_positions[c] = Position { x, y };
|
||||||
@@ -464,6 +490,25 @@ pub fn candidate_with_diagnostics(
|
|||||||
let size = sizes.get_mut(&id).unwrap();
|
let size = sizes.get_mut(&id).unwrap();
|
||||||
size.width = size.width.max(total.width + 2.0 * PAD);
|
size.width = size.width.max(total.width + 2.0 * PAD);
|
||||||
size.height = size.height.max(total.height + header + 2.0 * PAD);
|
size.height = size.height.max(total.height + header + 2.0 * PAD);
|
||||||
|
if order_boundaries {
|
||||||
|
boundary_groups.push(boundary::Group {
|
||||||
|
parent: id,
|
||||||
|
blocks: comps
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(c, members)| boundary::Block {
|
||||||
|
members: members.iter().map(|&i| ids[i]).collect(),
|
||||||
|
size: blocks[c],
|
||||||
|
position: block_positions[c],
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
bands,
|
||||||
|
undirected: undirected
|
||||||
|
.iter()
|
||||||
|
.map(|(a, b)| (owner[index[a]], owner[index[b]]))
|
||||||
|
.collect(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let lifted: Vec<_> = arcs
|
let lifted: Vec<_> = arcs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -472,6 +517,27 @@ pub fn candidate_with_diagnostics(
|
|||||||
.collect();
|
.collect();
|
||||||
diagnostics.push(serde_json::json!({"visible_parent":parent,"components":component_diagnostics,"directed_lifted_edges":lifted,"undirected_lifted_pairs":undirected,"block_positions":block_positions,"bounds":total,"policy":"SCCs of sibling-subtree quotient, not global semantic SCCs. Undirected edges affect local gap and deterministic proximity only; no rank direction invented."}));
|
diagnostics.push(serde_json::json!({"visible_parent":parent,"components":component_diagnostics,"directed_lifted_edges":lifted,"undirected_lifted_pairs":undirected,"block_positions":block_positions,"bounds":total,"policy":"SCCs of sibling-subtree quotient, not global semantic SCCs. Undirected edges affect local gap and deterministic proximity only; no rank direction invented."}));
|
||||||
}
|
}
|
||||||
|
let boundary_report = if order_boundaries {
|
||||||
|
let report = boundary::reorder(
|
||||||
|
&mut boundary_groups,
|
||||||
|
&parents,
|
||||||
|
&mut offsets,
|
||||||
|
&sizes,
|
||||||
|
&view.edges,
|
||||||
|
&mut work,
|
||||||
|
)?;
|
||||||
|
for group in &boundary_groups {
|
||||||
|
let diagnostic = diagnostics
|
||||||
|
.iter_mut()
|
||||||
|
.find(|entry| entry["visible_parent"] == serde_json::json!(group.parent))
|
||||||
|
.unwrap();
|
||||||
|
diagnostic["block_positions"] =
|
||||||
|
serde_json::json!(group.blocks.iter().map(|b| b.position).collect::<Vec<_>>());
|
||||||
|
}
|
||||||
|
Some(report)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let mut plans = Vec::new();
|
let mut plans = Vec::new();
|
||||||
for n in &view.nodes {
|
for n in &view.nodes {
|
||||||
let mut position = offsets[&n.id];
|
let mut position = offsets[&n.id];
|
||||||
@@ -518,10 +584,11 @@ pub fn candidate_with_diagnostics(
|
|||||||
"final mandatory port lead incompatible; no repair attempted",
|
"final mandatory port lead incompatible; no repair attempted",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Ok((
|
let mut report = serde_json::json!({"groups":diagnostics,"work_charged":work,"work_limit":LIMIT,"scope":"Projected-forest quotient heuristic; actual final reprojection and necessary lead guard checked. No full-route or readability acceptance."});
|
||||||
result,
|
if let Some(boundary_report) = boundary_report {
|
||||||
serde_json::json!({"groups":diagnostics,"work_charged":work,"work_limit":LIMIT,"scope":"Projected-forest quotient heuristic; actual final reprojection and necessary lead guard checked. No full-route or readability acceptance."}),
|
report["boundary_order"] = boundary_report;
|
||||||
))
|
}
|
||||||
|
Ok((result, report))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -0,0 +1,672 @@
|
|||||||
|
//! One deterministic boundary-order pass; dependency cycles use frozen scores.
|
||||||
|
use super::{charge, components, error, lift};
|
||||||
|
use oa_core::{Id, ModelError, Position, Size, projection::ProjectedEdge};
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
|
pub(super) struct Block {
|
||||||
|
pub members: Vec<Id>,
|
||||||
|
pub size: Size,
|
||||||
|
pub position: Position,
|
||||||
|
}
|
||||||
|
pub(super) struct Band {
|
||||||
|
pub components: Vec<usize>,
|
||||||
|
pub gap: f32,
|
||||||
|
}
|
||||||
|
pub(super) struct Group {
|
||||||
|
pub parent: Id,
|
||||||
|
pub blocks: Vec<Block>,
|
||||||
|
pub bands: Vec<Band>,
|
||||||
|
pub undirected: BTreeSet<(usize, usize)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Unit {
|
||||||
|
blocks: Vec<usize>,
|
||||||
|
sources: BTreeSet<Id>,
|
||||||
|
hints: Vec<(Id, Id, Id)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn center(
|
||||||
|
id: Id,
|
||||||
|
parents: &BTreeMap<Id, Option<Id>>,
|
||||||
|
offsets: &BTreeMap<Id, Position>,
|
||||||
|
sizes: &BTreeMap<Id, Size>,
|
||||||
|
work: &mut usize,
|
||||||
|
) -> Result<f64, ModelError> {
|
||||||
|
let mut at = Some(id);
|
||||||
|
let mut seen = BTreeSet::new();
|
||||||
|
let mut y = 0.0_f32;
|
||||||
|
while let Some(node) = at {
|
||||||
|
charge(work, 1)?;
|
||||||
|
if !seen.insert(node) {
|
||||||
|
return Err(error("boundary cyclic ancestry"));
|
||||||
|
}
|
||||||
|
let pos = offsets
|
||||||
|
.get(&node)
|
||||||
|
.ok_or_else(|| error("boundary missing offset"))?;
|
||||||
|
if !pos.x.is_finite() || !pos.y.is_finite() {
|
||||||
|
return Err(error("boundary nonfinite offset"));
|
||||||
|
}
|
||||||
|
if seen.len() == 1 {
|
||||||
|
y = pos.y;
|
||||||
|
} else {
|
||||||
|
y += pos.y;
|
||||||
|
}
|
||||||
|
if !y.is_finite() {
|
||||||
|
return Err(error("boundary composed position overflow"));
|
||||||
|
}
|
||||||
|
at = *parents
|
||||||
|
.get(&node)
|
||||||
|
.ok_or_else(|| error("boundary missing parent"))?;
|
||||||
|
}
|
||||||
|
let size = sizes
|
||||||
|
.get(&id)
|
||||||
|
.ok_or_else(|| error("boundary missing source size"))?;
|
||||||
|
y += 40.0;
|
||||||
|
let center = f64::from(y) + f64::from(size.height) / 2.0;
|
||||||
|
if !center.is_finite() {
|
||||||
|
return Err(error("boundary nonfinite center"));
|
||||||
|
}
|
||||||
|
Ok(center)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn translate(value: f32, delta: f32) -> Result<f32, ModelError> {
|
||||||
|
let next = value + delta;
|
||||||
|
if !next.is_finite() || ((f64::from(next) - f64::from(value)) - f64::from(delta)).abs() > 0.001
|
||||||
|
{
|
||||||
|
return Err(error("boundary member translation precision exceeded"));
|
||||||
|
}
|
||||||
|
Ok(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn score(
|
||||||
|
unit: &Unit,
|
||||||
|
parents: &BTreeMap<Id, Option<Id>>,
|
||||||
|
offsets: &BTreeMap<Id, Position>,
|
||||||
|
sizes: &BTreeMap<Id, Size>,
|
||||||
|
work: &mut usize,
|
||||||
|
) -> Result<Option<f64>, ModelError> {
|
||||||
|
if unit.sources.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let mut total = 0.0;
|
||||||
|
for &source in &unit.sources {
|
||||||
|
total += center(source, parents, offsets, sizes, work)?;
|
||||||
|
}
|
||||||
|
let value = total / unit.sources.len() as f64;
|
||||||
|
if !value.is_finite() {
|
||||||
|
return Err(error("boundary nonfinite mean"));
|
||||||
|
}
|
||||||
|
Ok(Some(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ordered_units(scores: &[Option<f64>]) -> Vec<usize> {
|
||||||
|
let mut anchored: Vec<_> = (0..scores.len()).filter(|&i| scores[i].is_some()).collect();
|
||||||
|
anchored.sort_by(|&a, &b| {
|
||||||
|
scores[a]
|
||||||
|
.unwrap()
|
||||||
|
.total_cmp(&scores[b].unwrap())
|
||||||
|
.then(a.cmp(&b))
|
||||||
|
});
|
||||||
|
let mut next = anchored.into_iter();
|
||||||
|
(0..scores.len())
|
||||||
|
.map(|i| {
|
||||||
|
if scores[i].is_some() {
|
||||||
|
next.next().unwrap()
|
||||||
|
} else {
|
||||||
|
i
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn packing(
|
||||||
|
blocks: &[Block],
|
||||||
|
before: &[usize],
|
||||||
|
after: &[usize],
|
||||||
|
gap: f32,
|
||||||
|
) -> Result<Vec<f32>, ModelError> {
|
||||||
|
const EPS: f64 = 0.001;
|
||||||
|
let origin = f64::from(blocks[before[0]].position.y);
|
||||||
|
let mut canonical = origin;
|
||||||
|
for &c in before {
|
||||||
|
if (f64::from(blocks[c].position.y) - canonical).abs() > EPS {
|
||||||
|
return Err(error("boundary non-prefix canonical band"));
|
||||||
|
}
|
||||||
|
canonical += f64::from(blocks[c].size.height) + f64::from(gap);
|
||||||
|
}
|
||||||
|
let mut next = origin;
|
||||||
|
let mut ys = Vec::new();
|
||||||
|
for &c in after {
|
||||||
|
let y = next as f32;
|
||||||
|
if !y.is_finite() || (f64::from(y) - next).abs() > EPS {
|
||||||
|
return Err(error("boundary prefix precision bound exceeded"));
|
||||||
|
}
|
||||||
|
ys.push(y);
|
||||||
|
next += f64::from(blocks[c].size.height) + f64::from(gap);
|
||||||
|
}
|
||||||
|
if (next - canonical).abs() > EPS {
|
||||||
|
return Err(error("boundary envelope changed"));
|
||||||
|
}
|
||||||
|
if before == after {
|
||||||
|
Ok(before.iter().map(|&c| blocks[c].position.y).collect())
|
||||||
|
} else {
|
||||||
|
Ok(ys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn reorder(
|
||||||
|
groups: &mut [Group],
|
||||||
|
parents: &BTreeMap<Id, Option<Id>>,
|
||||||
|
offsets: &mut BTreeMap<Id, Position>,
|
||||||
|
sizes: &BTreeMap<Id, Size>,
|
||||||
|
edges: &[ProjectedEdge],
|
||||||
|
work: &mut usize,
|
||||||
|
) -> Result<serde_json::Value, ModelError> {
|
||||||
|
let mut group_order: Vec<_> = (0..groups.len()).collect();
|
||||||
|
group_order.sort_by_key(|&i| groups[i].parent);
|
||||||
|
let mut offset_owner = BTreeMap::new();
|
||||||
|
let mut parent_seen = BTreeSet::new();
|
||||||
|
let mut units: Vec<Vec<Vec<Unit>>> = vec![Vec::new(); groups.len()];
|
||||||
|
let mut active = BTreeSet::new();
|
||||||
|
for &g in &group_order {
|
||||||
|
let group = &groups[g];
|
||||||
|
if !parent_seen.insert(group.parent) || !parents.contains_key(&group.parent) {
|
||||||
|
return Err(error("boundary duplicate/missing group parent"));
|
||||||
|
}
|
||||||
|
let mut child_block = BTreeMap::new();
|
||||||
|
for (c, block) in group.blocks.iter().enumerate() {
|
||||||
|
if !block.size.width.is_finite()
|
||||||
|
|| !block.size.height.is_finite()
|
||||||
|
|| block.size.width <= 0.0
|
||||||
|
|| block.size.height <= 0.0
|
||||||
|
|| !block.position.x.is_finite()
|
||||||
|
|| !block.position.y.is_finite()
|
||||||
|
{
|
||||||
|
return Err(error("boundary invalid block geometry"));
|
||||||
|
}
|
||||||
|
for &id in &block.members {
|
||||||
|
if parents.get(&id) != Some(&Some(group.parent))
|
||||||
|
|| !offsets.contains_key(&id)
|
||||||
|
|| child_block.insert(id, c).is_some()
|
||||||
|
|| offset_owner.insert(id, g).is_some()
|
||||||
|
{
|
||||||
|
return Err(error("boundary invalid child membership"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut covered = BTreeSet::new();
|
||||||
|
for band in &group.bands {
|
||||||
|
if !band.gap.is_finite() || band.gap < 0.0 || band.components.is_empty() {
|
||||||
|
return Err(error("boundary invalid band"));
|
||||||
|
}
|
||||||
|
let local: BTreeMap<_, _> = band
|
||||||
|
.components
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, &c)| (c, i))
|
||||||
|
.collect();
|
||||||
|
for &c in &band.components {
|
||||||
|
if c >= group.blocks.len() || !covered.insert(c) {
|
||||||
|
return Err(error("boundary duplicate/unknown band block"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut arcs = vec![BTreeSet::new(); band.components.len()];
|
||||||
|
for &(a, b) in &group.undirected {
|
||||||
|
charge(work, 1)?;
|
||||||
|
if let (Some(&a), Some(&b)) = (local.get(&a), local.get(&b)) {
|
||||||
|
arcs[a].insert(b);
|
||||||
|
arcs[b].insert(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let bundles = components(&arcs, work)?;
|
||||||
|
let mut band_units = Vec::new();
|
||||||
|
for bundle in bundles {
|
||||||
|
if bundle.windows(2).any(|v| v[1] != v[0] + 1) {
|
||||||
|
return Err(error("boundary undirected bundle not contiguous"));
|
||||||
|
}
|
||||||
|
let blocks: Vec<_> = bundle.iter().map(|&i| band.components[i]).collect();
|
||||||
|
let mut unit = Unit {
|
||||||
|
blocks,
|
||||||
|
sources: BTreeSet::new(),
|
||||||
|
hints: Vec::new(),
|
||||||
|
};
|
||||||
|
for edge in edges {
|
||||||
|
charge(work, 1)?;
|
||||||
|
if !edge.oriented {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let target = lift(edge.target, Some(group.parent), parents, work)?;
|
||||||
|
let Some(target) = target else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !unit.blocks.contains(
|
||||||
|
child_block
|
||||||
|
.get(&target)
|
||||||
|
.ok_or_else(|| error("boundary missing lifted target"))?,
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if lift(edge.source, Some(group.parent), parents, work)?.is_none() {
|
||||||
|
unit.sources.insert(edge.source);
|
||||||
|
unit.hints.push((edge.id, edge.source, edge.target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unit.hints.sort();
|
||||||
|
band_units.push(unit);
|
||||||
|
}
|
||||||
|
if band_units.iter().filter(|u| !u.sources.is_empty()).count() >= 2 {
|
||||||
|
active.insert(g);
|
||||||
|
}
|
||||||
|
units[g].push(band_units);
|
||||||
|
}
|
||||||
|
if covered.len() != group.blocks.len() {
|
||||||
|
return Err(error("boundary incomplete band inventory"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let active_order: Vec<_> = group_order
|
||||||
|
.into_iter()
|
||||||
|
.filter(|g| active.contains(g))
|
||||||
|
.collect();
|
||||||
|
let active_index: BTreeMap<_, _> = active_order
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, &g)| (g, i))
|
||||||
|
.collect();
|
||||||
|
let mut dependencies = vec![BTreeSet::new(); active_order.len()];
|
||||||
|
let mut dependency_evidence = BTreeSet::new();
|
||||||
|
for (&g, &gi) in &active_index {
|
||||||
|
for unit in units[g].iter().flatten() {
|
||||||
|
for &source in &unit.sources {
|
||||||
|
let mut at = Some(source);
|
||||||
|
let mut seen = BTreeSet::new();
|
||||||
|
while let Some(node) = at {
|
||||||
|
charge(work, 1)?;
|
||||||
|
if !seen.insert(node) {
|
||||||
|
return Err(error("boundary dependency ancestry cycle"));
|
||||||
|
}
|
||||||
|
if let Some(&owner) = offset_owner.get(&node)
|
||||||
|
&& let Some(&oi) = active_index.get(&owner)
|
||||||
|
{
|
||||||
|
dependencies[oi].insert(gi);
|
||||||
|
dependency_evidence.insert((
|
||||||
|
groups[owner].parent,
|
||||||
|
groups[g].parent,
|
||||||
|
source,
|
||||||
|
node,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
at = *parents
|
||||||
|
.get(&node)
|
||||||
|
.ok_or_else(|| error("boundary missing dependency ancestor"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let sccs = components(&dependencies, work)?;
|
||||||
|
let mut scc_owner = vec![0; active_order.len()];
|
||||||
|
for (s, members) in sccs.iter().enumerate() {
|
||||||
|
for &i in members {
|
||||||
|
scc_owner[i] = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut dag = vec![BTreeSet::new(); sccs.len()];
|
||||||
|
for (a, targets) in dependencies.iter().enumerate() {
|
||||||
|
for &b in targets {
|
||||||
|
if scc_owner[a] != scc_owner[b] {
|
||||||
|
dag[scc_owner[a]].insert(scc_owner[b]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut done = BTreeSet::new();
|
||||||
|
let mut records = Vec::new();
|
||||||
|
let mut processed = Vec::new();
|
||||||
|
while done.len() < sccs.len() {
|
||||||
|
charge(work, sccs.len() * sccs.len())?;
|
||||||
|
let s = (0..sccs.len())
|
||||||
|
.find(|s| {
|
||||||
|
!done.contains(s)
|
||||||
|
&& (0..sccs.len()).all(|p| !dag[p].contains(s) || done.contains(&p))
|
||||||
|
})
|
||||||
|
.ok_or_else(|| error("boundary condensation cycle"))?;
|
||||||
|
// Score the entire dependency SCC before modifying any offsets.
|
||||||
|
let mut snapshots = Vec::new();
|
||||||
|
for &i in &sccs[s] {
|
||||||
|
let g = active_order[i];
|
||||||
|
for (b, band_units) in units[g].iter().enumerate() {
|
||||||
|
let scores = band_units
|
||||||
|
.iter()
|
||||||
|
.map(|u| score(u, parents, offsets, sizes, work))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
snapshots.push((g, b, scores));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (g, b, scores) in snapshots {
|
||||||
|
let band_units = &units[g][b];
|
||||||
|
let order = ordered_units(&scores);
|
||||||
|
let before = groups[g].bands[b].components.clone();
|
||||||
|
let gap = groups[g].bands[b].gap;
|
||||||
|
let after: Vec<_> = order
|
||||||
|
.iter()
|
||||||
|
.flat_map(|&u| band_units[u].blocks.iter().copied())
|
||||||
|
.collect();
|
||||||
|
let ys = packing(&groups[g].blocks, &before, &after, gap)?;
|
||||||
|
let mut changed = Vec::new();
|
||||||
|
for (&c, y) in after.iter().zip(ys) {
|
||||||
|
charge(work, groups[g].blocks[c].members.len() + 1)?;
|
||||||
|
let old = groups[g].blocks[c].position.y;
|
||||||
|
let delta = y - old;
|
||||||
|
if !y.is_finite() || !delta.is_finite() {
|
||||||
|
return Err(error("boundary packing overflow"));
|
||||||
|
}
|
||||||
|
if delta != 0.0 {
|
||||||
|
changed.push(c);
|
||||||
|
}
|
||||||
|
for id in &groups[g].blocks[c].members {
|
||||||
|
let p = offsets
|
||||||
|
.get_mut(id)
|
||||||
|
.ok_or_else(|| error("boundary lost member offset"))?;
|
||||||
|
p.y = translate(p.y, delta)?;
|
||||||
|
}
|
||||||
|
groups[g].blocks[c].position.y = y;
|
||||||
|
}
|
||||||
|
groups[g].bands[b].components = after.clone();
|
||||||
|
let changed_units: Vec<_> = band_units
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, u)| u.blocks.iter().any(|c| changed.contains(c)))
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.collect();
|
||||||
|
records.push(serde_json::json!({"parent":groups[g].parent,"band":b,"before_blocks":before,"after_blocks":after,"unit_order":order,"scores":scores,"changed_blocks":changed,"changed_units":changed_units,"envelope_epsilon":0.001,"units":band_units.iter().map(|u|serde_json::json!({"blocks":u.blocks,"source_owners":u.sources,"edge_hints":u.hints})).collect::<Vec<_>>()}));
|
||||||
|
}
|
||||||
|
processed.push(serde_json::json!({"parents":sccs[s].iter().map(|&i|groups[active_order[i]].parent).collect::<Vec<_>>(),"cyclic":sccs[s].len()>1||sccs[s].iter().any(|&i|dependencies[i].contains(&i)),"policy":"all scores snapshotted before one simultaneous group-permutation pass"}));
|
||||||
|
done.insert(s);
|
||||||
|
}
|
||||||
|
Ok(
|
||||||
|
serde_json::json!({"active_parents":active_order.iter().map(|&g|groups[g].parent).collect::<Vec<_>>(),"dependencies":dependency_evidence,"dependency_components":processed,"bands":records,"limitation":"One pass; cyclic dependencies use a common snapshot, not a fixed-point solution. Root unchanged; no quality or repeat-stability acceptance."}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
fn id(n: u128) -> Id {
|
||||||
|
Id::from_u128(n)
|
||||||
|
}
|
||||||
|
fn block(n: u128, y: f32, height: f32) -> Block {
|
||||||
|
Block {
|
||||||
|
members: vec![id(n)],
|
||||||
|
size: Size {
|
||||||
|
width: 10.0,
|
||||||
|
height,
|
||||||
|
},
|
||||||
|
position: Position { x: 0.0, y },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn edge(n: u128, source: u128, target: u128) -> ProjectedEdge {
|
||||||
|
ProjectedEdge {
|
||||||
|
id: id(n),
|
||||||
|
source: id(source),
|
||||||
|
target: id(target),
|
||||||
|
trace: false,
|
||||||
|
oriented: true,
|
||||||
|
source_port: None,
|
||||||
|
target_port: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn anchored_sort_preserves_unanchored_ordinal_slot() {
|
||||||
|
assert_eq!(
|
||||||
|
ordered_units(&[Some(90.0), None, Some(10.0), Some(10.0)]),
|
||||||
|
vec![2, 1, 3, 0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn heterogeneous_blocks_use_new_prefix_not_old_slots() {
|
||||||
|
let blocks = vec![
|
||||||
|
Block {
|
||||||
|
members: vec![],
|
||||||
|
size: Size {
|
||||||
|
width: 10.0,
|
||||||
|
height: 100.0,
|
||||||
|
},
|
||||||
|
position: Position { x: 0.0, y: 0.0 },
|
||||||
|
},
|
||||||
|
Block {
|
||||||
|
members: vec![],
|
||||||
|
size: Size {
|
||||||
|
width: 10.0,
|
||||||
|
height: 20.0,
|
||||||
|
},
|
||||||
|
position: Position { x: 0.0, y: 132.0 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
packing(&blocks, &[0, 1], &[1, 0], 32.0).unwrap(),
|
||||||
|
vec![0.0, 52.0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn center_includes_grandparent_offset_change() {
|
||||||
|
let (a, b, c) = (Id::from_u128(1), Id::from_u128(2), Id::from_u128(3));
|
||||||
|
let parents = BTreeMap::from([(a, None), (b, Some(a)), (c, Some(b))]);
|
||||||
|
let mut offsets = BTreeMap::from([
|
||||||
|
(a, Position { x: 0.0, y: 100.0 }),
|
||||||
|
(b, Position { x: 0.0, y: 20.0 }),
|
||||||
|
(c, Position { x: 0.0, y: 5.0 }),
|
||||||
|
]);
|
||||||
|
let sizes = BTreeMap::from([(
|
||||||
|
c,
|
||||||
|
Size {
|
||||||
|
width: 10.0,
|
||||||
|
height: 10.0,
|
||||||
|
},
|
||||||
|
)]);
|
||||||
|
assert_eq!(
|
||||||
|
center(c, &parents, &offsets, &sizes, &mut 0).unwrap(),
|
||||||
|
170.0
|
||||||
|
);
|
||||||
|
offsets.get_mut(&a).unwrap().y += 300.0;
|
||||||
|
assert_eq!(
|
||||||
|
center(c, &parents, &offsets, &sizes, &mut 0).unwrap(),
|
||||||
|
470.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn ancestor_dependency_updates_downstream_scores_before_its_permutation() {
|
||||||
|
// Downstream parent1 sorts before upstream20, so an absent ancestor
|
||||||
|
// dependency would leave downstream order unchanged and fail this test.
|
||||||
|
let mut groups = vec![
|
||||||
|
Group {
|
||||||
|
parent: id(1),
|
||||||
|
blocks: vec![block(2, 0.0, 100.0), block(3, 132.0, 100.0)],
|
||||||
|
bands: vec![Band {
|
||||||
|
components: vec![0, 1],
|
||||||
|
gap: 32.0,
|
||||||
|
}],
|
||||||
|
undirected: BTreeSet::new(),
|
||||||
|
},
|
||||||
|
Group {
|
||||||
|
parent: id(20),
|
||||||
|
blocks: vec![block(21, 0.0, 100.0), block(22, 132.0, 100.0)],
|
||||||
|
bands: vec![Band {
|
||||||
|
components: vec![0, 1],
|
||||||
|
gap: 32.0,
|
||||||
|
}],
|
||||||
|
undirected: BTreeSet::new(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let parents = BTreeMap::from([
|
||||||
|
(id(1), None),
|
||||||
|
(id(2), Some(id(1))),
|
||||||
|
(id(3), Some(id(1))),
|
||||||
|
(id(20), None),
|
||||||
|
(id(21), Some(id(20))),
|
||||||
|
(id(22), Some(id(20))),
|
||||||
|
(id(23), Some(id(21))),
|
||||||
|
(id(30), None),
|
||||||
|
(id(31), None),
|
||||||
|
(id(32), None),
|
||||||
|
]);
|
||||||
|
let mut offsets: BTreeMap<_, _> = [
|
||||||
|
(1, 0.0),
|
||||||
|
(2, 0.0),
|
||||||
|
(3, 132.0),
|
||||||
|
(20, 0.0),
|
||||||
|
(21, 0.0),
|
||||||
|
(22, 132.0),
|
||||||
|
(23, 10.0),
|
||||||
|
(30, 300.0),
|
||||||
|
(31, 0.0),
|
||||||
|
(32, 50.0),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(n, y)| (id(n), Position { x: 0.0, y }))
|
||||||
|
.collect();
|
||||||
|
let sizes = parents
|
||||||
|
.keys()
|
||||||
|
.map(|&n| {
|
||||||
|
(
|
||||||
|
n,
|
||||||
|
Size {
|
||||||
|
width: 10.0,
|
||||||
|
height: 10.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let edges = vec![
|
||||||
|
edge(101, 30, 21),
|
||||||
|
edge(102, 31, 22),
|
||||||
|
edge(103, 23, 2),
|
||||||
|
edge(104, 32, 3),
|
||||||
|
];
|
||||||
|
let report = reorder(&mut groups, &parents, &mut offsets, &sizes, &edges, &mut 0).unwrap();
|
||||||
|
assert_eq!(groups[1].bands[0].components, vec![1, 0]);
|
||||||
|
assert_eq!(groups[0].bands[0].components, vec![1, 0]);
|
||||||
|
assert_eq!(offsets[&id(21)].y, 132.0);
|
||||||
|
assert_eq!(offsets[&id(23)].y, 10.0); // descendant is not directly moved
|
||||||
|
assert_eq!(
|
||||||
|
center(id(23), &parents, &offsets, &sizes, &mut 0).unwrap(),
|
||||||
|
187.0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
report["dependency_components"][0]["parents"][0],
|
||||||
|
serde_json::json!(id(20))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn undirected_bundle_stays_whole_around_unanchored_unit() {
|
||||||
|
let mut groups = vec![Group {
|
||||||
|
parent: id(100),
|
||||||
|
blocks: vec![
|
||||||
|
block(101, 0.0, 20.0),
|
||||||
|
block(102, 30.0, 30.0),
|
||||||
|
block(103, 70.0, 40.0),
|
||||||
|
block(104, 120.0, 10.0),
|
||||||
|
],
|
||||||
|
bands: vec![Band {
|
||||||
|
components: vec![0, 1, 2, 3],
|
||||||
|
gap: 10.0,
|
||||||
|
}],
|
||||||
|
undirected: BTreeSet::from([(0, 1)]),
|
||||||
|
}];
|
||||||
|
let parents = BTreeMap::from([
|
||||||
|
(id(100), None),
|
||||||
|
(id(101), Some(id(100))),
|
||||||
|
(id(102), Some(id(100))),
|
||||||
|
(id(103), Some(id(100))),
|
||||||
|
(id(104), Some(id(100))),
|
||||||
|
(id(110), None),
|
||||||
|
(id(111), None),
|
||||||
|
]);
|
||||||
|
let mut offsets: BTreeMap<_, _> = [
|
||||||
|
(100, 0.0),
|
||||||
|
(101, 0.0),
|
||||||
|
(102, 30.0),
|
||||||
|
(103, 70.0),
|
||||||
|
(104, 120.0),
|
||||||
|
(110, 300.0),
|
||||||
|
(111, 0.0),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(n, y)| (id(n), Position { x: 0.0, y }))
|
||||||
|
.collect();
|
||||||
|
let sizes = parents
|
||||||
|
.keys()
|
||||||
|
.map(|&n| {
|
||||||
|
(
|
||||||
|
n,
|
||||||
|
Size {
|
||||||
|
width: 10.0,
|
||||||
|
height: 10.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let report = reorder(
|
||||||
|
&mut groups,
|
||||||
|
&parents,
|
||||||
|
&mut offsets,
|
||||||
|
&sizes,
|
||||||
|
&[
|
||||||
|
edge(201, 110, 101),
|
||||||
|
edge(202, 110, 102),
|
||||||
|
edge(203, 111, 104),
|
||||||
|
],
|
||||||
|
&mut 0,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(groups[0].bands[0].components, vec![3, 2, 0, 1]);
|
||||||
|
assert_eq!(
|
||||||
|
[
|
||||||
|
offsets[&id(104)].y,
|
||||||
|
offsets[&id(103)].y,
|
||||||
|
offsets[&id(101)].y,
|
||||||
|
offsets[&id(102)].y
|
||||||
|
],
|
||||||
|
[0.0, 20.0, 70.0, 100.0]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
report["bands"][0]["units"][0]["source_owners"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn center_uses_final_plan_f32_composition_and_translation_has_error_bound() {
|
||||||
|
let parents = BTreeMap::from([(id(1), None), (id(2), Some(id(1))), (id(3), Some(id(2)))]);
|
||||||
|
let offsets = BTreeMap::from([
|
||||||
|
(
|
||||||
|
id(1),
|
||||||
|
Position {
|
||||||
|
x: 0.0,
|
||||||
|
y: -100000.0,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id(2),
|
||||||
|
Position {
|
||||||
|
x: 0.0,
|
||||||
|
y: 100000.0,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(id(3), Position { x: 0.0, y: 0.1 }),
|
||||||
|
]);
|
||||||
|
let sizes = BTreeMap::from([(
|
||||||
|
id(3),
|
||||||
|
Size {
|
||||||
|
width: 10.0,
|
||||||
|
height: 10.0,
|
||||||
|
},
|
||||||
|
)]);
|
||||||
|
assert_eq!(
|
||||||
|
center(id(3), &parents, &offsets, &sizes, &mut 0).unwrap(),
|
||||||
|
45.1015625
|
||||||
|
);
|
||||||
|
assert_eq!(translate(10.125, 0.375).unwrap(), 10.5);
|
||||||
|
assert!(translate(32768.0, 0.002).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user