Files
ARCADIA-Studio/harness/experiments/resize-port-policy-20260912/experiment.patch
T

873 lines
31 KiB
Diff

diff --git a/crates/oa-core/src/projection.rs b/crates/oa-core/src/projection.rs
index befb027..2ee820e 100644
--- a/crates/oa-core/src/projection.rs
+++ b/crates/oa-core/src/projection.rs
@@ -4,6 +4,15 @@ use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
mod port_layout;
pub use port_layout::{port_anchor_offset, port_anchor_position};
+
+/// Isolated experiment only: no persisted setting or production default change.
+#[doc(hidden)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ExperimentalPortPolicy {
+ Current,
+ SameSidesAdaptSpacing,
+ CapacityAware,
+}
#[path = "projection/state.rs"]
mod state;
pub(crate) use state::node_size as state_node_size;
@@ -249,7 +258,17 @@ pub fn project_diagram_with_index(
d: &Diagram,
index: &crate::view_options::ViewIndex,
) -> ProjectedView {
- project_diagram_impl(p, d, None, Some(index))
+ project_diagram_impl(p, d, None, Some(index), ExperimentalPortPolicy::Current)
+}
+/// Compare port policies over the actual projector; not a document/API setting.
+#[doc(hidden)]
+pub fn project_diagram_with_experimental_port_policy(
+ p: &Project,
+ d: &Diagram,
+ policy: ExperimentalPortPolicy,
+) -> ProjectedView {
+ let index = crate::view_options::ViewIndex::new(p);
+ project_diagram_impl(p, d, None, Some(&index), policy)
}
/// Unfiltered eligibility from the same projection/anchor/fold decisions used
/// for drawing. Compute once for a view-options editor, then combine with the
@@ -269,7 +288,13 @@ pub fn edge_availability(
)
})
.collect();
- project_diagram_impl(p, d, Some(&mut availability), None);
+ project_diagram_impl(
+ p,
+ d,
+ Some(&mut availability),
+ None,
+ ExperimentalPortPolicy::Current,
+ );
availability
}
fn project_diagram_impl(
@@ -277,6 +302,7 @@ fn project_diagram_impl(
d: &Diagram,
mut availability: Option<&mut BTreeMap<Id, crate::view_options::EdgeAvailability>>,
index: Option<&crate::view_options::ViewIndex>,
+ port_policy: ExperimentalPortPolicy,
) -> ProjectedView {
let parents = parents(p, d);
let mut nodes = Vec::new();
@@ -516,7 +542,7 @@ fn project_diagram_impl(
let ports = if d.projection.state_machine().is_some() {
Vec::new()
} else {
- port_layout::layout(p, d, &mut nodes, &edges)
+ port_layout::layout(p, d, &mut nodes, &edges, port_policy)
};
// Port geometry uses every eligible edge. Presentation filters must never
// move visible ports when another line is hidden or a category is selected.
diff --git a/crates/oa-core/src/projection/port_capacity_experiment.rs b/crates/oa-core/src/projection/port_capacity_experiment.rs
new file mode 100644
index 0000000..25bdab0
--- /dev/null
+++ b/crates/oa-core/src/projection/port_capacity_experiment.rs
@@ -0,0 +1,221 @@
+//! Experimental four-side capacity assignment; no model fields or router feedback.
+use super::*;
+
+#[derive(Clone)]
+pub(super) struct Demand {
+ pub preferred: PortAnchorSide,
+ pub semantic: Option<PortAnchorSide>,
+ pub clear: [bool; 4],
+}
+
+fn index(side: PortAnchorSide) -> usize {
+ SIDES
+ .iter()
+ .position(|candidate| *candidate == side)
+ .unwrap()
+}
+
+fn capacity(length: f32) -> usize {
+ if length < 2. * CORNER {
+ 0
+ } else {
+ ((length - 2. * CORNER) / GAP).floor() as usize + 1
+ }
+}
+
+fn counts(assigned: &[Option<PortAnchorSide>]) -> [usize; 4] {
+ let mut result = [0; 4];
+ for side in assigned.iter().flatten() {
+ result[index(*side)] += 1;
+ }
+ result
+}
+
+fn ranked(demand: &Demand, counts: [usize; 4]) -> Vec<PortAnchorSide> {
+ let mut result = Vec::new();
+ for side in [Some(demand.preferred), demand.semantic]
+ .into_iter()
+ .flatten()
+ {
+ if demand.clear[index(side)] && !result.contains(&side) {
+ result.push(side);
+ }
+ }
+ let mut remaining = SIDES;
+ remaining.sort_by_key(|side| (counts[index(*side)], index(*side)));
+ for side in remaining {
+ if demand.clear[index(side)] && !result.contains(&side) {
+ result.push(side);
+ }
+ }
+ result
+}
+
+// An alternating reassignment visits each of four side bins at most once.
+// It is attempted only after every directly free clear side has been considered.
+fn try_assign(
+ port: usize,
+ demands: &[Demand],
+ capacities: [usize; 4],
+ assigned: &mut [Option<PortAnchorSide>],
+ visited: &mut [bool; 4],
+) -> bool {
+ let counts = counts(assigned);
+ let sides = ranked(&demands[port], counts);
+ for side in &sides {
+ let slot = index(*side);
+ if !visited[slot] && counts[slot] < capacities[slot] {
+ assigned[port] = Some(*side);
+ return true;
+ }
+ }
+ for side in sides {
+ let slot = index(side);
+ if visited[slot] || capacities[slot] == 0 {
+ continue;
+ }
+ visited[slot] = true;
+ let occupants: Vec<_> = assigned
+ .iter()
+ .enumerate()
+ .filter_map(|(port, current)| (*current == Some(side)).then_some(port))
+ .collect();
+ for other in occupants {
+ if try_assign(other, demands, capacities, assigned, visited) {
+ assigned[port] = Some(side);
+ return true;
+ }
+ }
+ }
+ false
+}
+
+fn assign_with_capacities(demands: &[Demand], capacities: [usize; 4]) -> Vec<PortAnchorSide> {
+ let mut assigned = vec![None; demands.len()];
+ // Demands are in the core's stable semantic port-ID order. Manual anchors
+ // are deliberately absent: editing one must not resize owners or flip peers.
+ for port in 0..demands.len() {
+ try_assign(port, demands, capacities, &mut assigned, &mut [false; 4]);
+ }
+ for (port, demand) in demands.iter().enumerate() {
+ if assigned[port].is_some() {
+ continue;
+ }
+ let counts = counts(&assigned);
+ // Prefer a crowded clear exit to an available blocked one. The existing
+ // position allocator adapts spacing; no port or owner geometry is invented.
+ let side = ranked(demand, counts)
+ .into_iter()
+ .next()
+ .unwrap_or_else(|| {
+ *SIDES
+ .iter()
+ .min_by_key(|side| counts[index(**side)])
+ .unwrap()
+ });
+ assigned[port] = Some(side);
+ }
+ assigned.into_iter().map(Option::unwrap).collect()
+}
+
+pub(super) fn assign(node: &ProjectedNode, demands: &[Demand]) -> Vec<PortAnchorSide> {
+ assign_with_capacities(
+ demands,
+ [
+ capacity(node.size.height),
+ capacity(node.size.height),
+ capacity(node.size.width),
+ capacity(node.size.width),
+ ],
+ )
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn demand(preferred: PortAnchorSide, sides: &[PortAnchorSide]) -> Demand {
+ Demand {
+ preferred,
+ semantic: None,
+ clear: SIDES.map(|side| sides.contains(&side)),
+ }
+ }
+
+ #[test]
+ fn reassignment_recovers_scarce_clear_capacity() {
+ use PortAnchorSide::{Left, Right, Top};
+ // Assignment-level falsifier only. Its clear masks are not yet produced
+ // by a model fixture with actual peer geometry.
+ let mut demands = vec![demand(Left, &[Left, Top]); 8];
+ demands.extend(vec![demand(Left, &[Left, Right]); 9]);
+ demands.extend(vec![demand(Right, &[Right, Top]); 3]);
+ let capacities = [8, 8, 6, 0];
+ let result = assign_with_capacities(&demands, capacities);
+ let totals = counts(&result.iter().copied().map(Some).collect::<Vec<_>>());
+ for (actual, capacity) in totals.into_iter().zip(capacities) {
+ assert!(actual <= capacity);
+ }
+ for (side, demand) in result.iter().zip(&demands) {
+ assert!(demand.clear[index(*side)]);
+ }
+ assert_eq!(assign_with_capacities(&demands, capacities), result);
+ }
+
+ #[test]
+ fn clear_overflow_does_not_choose_a_blocked_empty_side() {
+ let demands = vec![demand(PortAnchorSide::Right, &[PortAnchorSide::Right]); 5];
+ let result = assign_with_capacities(&demands, [10, 1, 10, 10]);
+ assert!(result.iter().all(|side| *side == PortAnchorSide::Right));
+ }
+
+ #[test]
+ fn small_clear_masks_match_an_exhaustive_assignment_oracle() {
+ let capacities = [1, 1, 1, 0];
+ // All343 triples of nonempty masks on three sides. This validates the
+ // alternating assignment itself, not geometric reachability of masks.
+ for a in 1..8_u8 {
+ for b in 1..8_u8 {
+ for c in 1..8_u8 {
+ let demands: Vec<_> = [a, b, c]
+ .into_iter()
+ .map(|mask| {
+ let sides: Vec<_> = SIDES
+ .iter()
+ .copied()
+ .filter(|side| mask & (1 << index(*side)) != 0)
+ .collect();
+ demand(sides[0], &sides)
+ })
+ .collect();
+ let feasible = (0..3).any(|x| {
+ (0..3).any(|y| {
+ (0..3).any(|z| {
+ x != y
+ && x != z
+ && y != z
+ && demands[0].clear[x]
+ && demands[1].clear[y]
+ && demands[2].clear[z]
+ })
+ })
+ });
+ let result = assign_with_capacities(&demands, capacities);
+ for (side, demand) in result.iter().zip(&demands) {
+ assert!(demand.clear[index(*side)]);
+ }
+ if feasible {
+ let totals = counts(&result.into_iter().map(Some).collect::<Vec<_>>());
+ assert!(
+ totals
+ .into_iter()
+ .zip(capacities)
+ .all(|(n, capacity)| n <= capacity),
+ "missed feasible masks {a}/{b}/{c}"
+ );
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/crates/oa-core/src/projection/port_layout.rs b/crates/oa-core/src/projection/port_layout.rs
index c8ec61d..ba0d868 100644
--- a/crates/oa-core/src/projection/port_layout.rs
+++ b/crates/oa-core/src/projection/port_layout.rs
@@ -1,6 +1,8 @@
//! Presentation-only port anchors. Automatic choices depend on owner geometry,
//! never on routed elbows, so routing cannot feed back into layout.
use super::*;
+#[path = "port_capacity_experiment.rs"]
+mod capacity_experiment;
const GAP: f32 = 28.;
const CORNER: f32 = 18.;
@@ -50,6 +52,7 @@ struct Choice {
automatic_side: PortAnchorSide,
manual: Option<f32>,
peer: Option<Position>,
+ capacity: Option<capacity_experiment::Demand>,
}
fn facing(node: &ProjectedNode, peer: Position) -> Option<PortAnchorSide> {
@@ -158,6 +161,7 @@ pub(super) fn layout(
d: &Diagram,
nodes: &mut [ProjectedNode],
edges: &[ProjectedEdge],
+ policy: ExperimentalPortPolicy,
) -> Vec<ProjectedPort> {
let by_id: BTreeMap<_, _> = nodes.iter().map(|n| (n.id, n)).collect();
let mut peers: BTreeMap<(Id, Id), Peers> = BTreeMap::new();
@@ -228,34 +232,62 @@ pub(super) fn layout(
automatic_side: side,
manual: anchor.map(|a| a.offset),
peer,
+ capacity: (policy == ExperimentalPortPolicy::CapacityAware).then(|| {
+ capacity_experiment::Demand {
+ preferred: side,
+ semantic: port_anchor_side(port),
+ clear: SIDES.map(clear),
+ }
+ }),
});
}
}
- // One capacity pass followed by semantic enclosure. Side choices stay frozen
- // throughout this pass; expanded parents cannot cause layout oscillation.
- for node in nodes.iter_mut() {
- for side in SIDES {
- let count = choices
- .get(&node.id)
- .into_iter()
- .flatten()
- .filter(|c| c.automatic_side == side)
- .count();
- if count == 0 {
+ if policy == ExperimentalPortPolicy::CapacityAware {
+ for node in nodes.iter() {
+ let Some(choices) = choices.get_mut(&node.id) else {
continue;
+ };
+ let demands: Vec<_> = choices
+ .iter()
+ .map(|choice| choice.capacity.clone().unwrap())
+ .collect();
+ let sides = capacity_experiment::assign(node, &demands);
+ for (choice, side) in choices.iter_mut().zip(sides) {
+ choice.automatic_side = side;
+ if choice.manual.is_none() {
+ choice.side = side;
+ }
}
- let minimum = 2. * CORNER + GAP * count.saturating_sub(1) as f32;
- match side {
- PortAnchorSide::Left | PortAnchorSide::Right => {
- node.size.height = node.size.height.max(minimum)
+ }
+ }
+ // One capacity pass followed by semantic enclosure. Side choices stay frozen
+ // throughout this pass; expanded parents cannot cause layout oscillation.
+ // Experimental alternatives finalize nodes before port-side assignment.
+ if policy == ExperimentalPortPolicy::Current {
+ for node in nodes.iter_mut() {
+ for side in SIDES {
+ let count = choices
+ .get(&node.id)
+ .into_iter()
+ .flatten()
+ .filter(|c| c.automatic_side == side)
+ .count();
+ if count == 0 {
+ continue;
}
- PortAnchorSide::Top | PortAnchorSide::Bottom => {
- node.size.width = node.size.width.max(minimum)
+ let minimum = 2. * CORNER + GAP * count.saturating_sub(1) as f32;
+ match side {
+ PortAnchorSide::Left | PortAnchorSide::Right => {
+ node.size.height = node.size.height.max(minimum)
+ }
+ PortAnchorSide::Top | PortAnchorSide::Bottom => {
+ node.size.width = node.size.width.max(minimum)
+ }
}
}
}
+ super::enclose(p, nodes);
}
- super::enclose(p, nodes);
let mut result = Vec::new();
for node in nodes.iter() {
let Some(choices) = choices.get(&node.id) else {
diff --git a/crates/oa-core/tests/experimental_port_policy.rs b/crates/oa-core/tests/experimental_port_policy.rs
new file mode 100644
index 0000000..06e3dc3
--- /dev/null
+++ b/crates/oa-core/tests/experimental_port_policy.rs
@@ -0,0 +1,458 @@
+//! Isolated policy comparisons. Current remains the ordinary projector default.
+use oa_core::{projection::*, routing::*, *};
+
+const ALTERNATIVES: [ExperimentalPortPolicy; 2] = [
+ ExperimentalPortPolicy::SameSidesAdaptSpacing,
+ ExperimentalPortPolicy::CapacityAware,
+];
+const POLICIES: [ExperimentalPortPolicy; 3] = [
+ ExperimentalPortPolicy::Current,
+ ExperimentalPortPolicy::SameSidesAdaptSpacing,
+ ExperimentalPortPolicy::CapacityAware,
+];
+
+fn pos(x: f32, y: f32) -> Position {
+ Position { x, y }
+}
+fn size(width: f32, height: f32) -> Size {
+ Size { width, height }
+}
+fn standard(p: &mut Project, owner: Id, name: &str) -> Id {
+ let id = Id::new_v4();
+ let port = make_port(
+ p,
+ id,
+ owner,
+ name,
+ PortContract::Component {
+ kind: ComponentPortKind::Standard,
+ orientation: ComponentPortOrientation::Unset,
+ },
+ )
+ .unwrap();
+ p.elements.insert(id, port);
+ id
+}
+fn place(p: &mut Project, d: Id, e: Id, position: Position, dimensions: Size) -> Id {
+ let n = p.place(d, e, position);
+ p.diagrams
+ .get_mut(&d)
+ .unwrap()
+ .nodes
+ .get_mut(&n)
+ .unwrap()
+ .size = dimensions;
+ n
+}
+struct Fixture {
+ p: Project,
+ d: Id,
+ owner: Id,
+ ports: Vec<Id>,
+}
+fn eight_ports() -> Fixture {
+ let mut p = Project::empty();
+ let a = p
+ .add_element(Kind::System, Layer::Sa, "Owner", None)
+ .unwrap();
+ let b = p
+ .add_element(Kind::SystemActor, Layer::Sa, "Above", None)
+ .unwrap();
+ let c = p
+ .add_element(Kind::SystemActor, Layer::Sa, "Below", None)
+ .unwrap();
+ let pb = standard(&mut p, b, "Above interface");
+ let pc = standard(&mut p, c, "Below interface");
+ let mut ports = Vec::new();
+ for i in 0..8 {
+ let pa = standard(&mut p, a, &format!("Interface {i}"));
+ p.relate(
+ RelationKind::BoundaryExchange,
+ &format!("Above {i}"),
+ pa,
+ pb,
+ );
+ p.relate(
+ RelationKind::BoundaryExchange,
+ &format!("Below {i}"),
+ pa,
+ pc,
+ );
+ ports.push(pa);
+ }
+ let d = p.ensure_diagram(Layer::Sa);
+ let owner = place(&mut p, d, a, pos(0., 0.), size(224., 160.));
+ place(&mut p, d, b, pos(38., -996.), size(184., 132.));
+ place(&mut p, d, c, pos(38., 1004.), size(184., 132.));
+ validate_candidate(&p).unwrap();
+ Fixture { p, d, owner, ports }
+}
+fn hundred_ports() -> Fixture {
+ let mut p = Project::empty();
+ let a = p
+ .add_element(Kind::System, Layer::Sa, "Owner", None)
+ .unwrap();
+ let b = p
+ .add_element(Kind::SystemActor, Layer::Sa, "Peer", None)
+ .unwrap();
+ let pb = standard(&mut p, b, "Peer interface");
+ let mut ports = Vec::new();
+ for i in 0..100 {
+ let pa = standard(&mut p, a, &format!("Interface {i}"));
+ p.relate(
+ RelationKind::BoundaryExchange,
+ &format!("Exchange {i}"),
+ pa,
+ pb,
+ );
+ ports.push(pa);
+ }
+ let d = p.ensure_diagram(Layer::Sa);
+ // Semantic port minima grow this to728x804 before any side choice.
+ let owner = place(&mut p, d, a, pos(0., 0.), size(224., 160.));
+ place(&mut p, d, b, pos(1908., 336.), size(184., 132.));
+ validate_candidate(&p).unwrap();
+ Fixture { p, d, owner, ports }
+}
+fn project(f: &Fixture, policy: ExperimentalPortPolicy) -> ProjectedView {
+ project_diagram_with_experimental_port_policy(&f.p, &f.p.diagrams[&f.d], policy)
+}
+fn node(view: &ProjectedView, id: Id) -> &ProjectedNode {
+ view.nodes.iter().find(|node| node.id == id).unwrap()
+}
+fn port(view: &ProjectedView, id: Id) -> &ProjectedPort {
+ view.ports.iter().find(|port| port.element == id).unwrap()
+}
+fn minimum_gap(view: &ProjectedView, owner: Id) -> f32 {
+ let mut result = f32::INFINITY;
+ for side in [
+ PortAnchorSide::Left,
+ PortAnchorSide::Right,
+ PortAnchorSide::Top,
+ PortAnchorSide::Bottom,
+ ] {
+ let horizontal = matches!(side, PortAnchorSide::Top | PortAnchorSide::Bottom);
+ let mut positions: Vec<_> = view
+ .ports
+ .iter()
+ .filter(|p| p.owner == owner && p.side == side)
+ .map(|p| {
+ if horizontal {
+ p.position.x
+ } else {
+ p.position.y
+ }
+ })
+ .collect();
+ positions.sort_by(f32::total_cmp);
+ for pair in positions.windows(2) {
+ result = result.min(pair[1] - pair[0]);
+ }
+ }
+ result
+}
+
+#[test]
+fn experimental_current_is_the_unchanged_default_projector() {
+ for p in [Project::demo(), eight_ports().p, hundred_ports().p] {
+ for diagram in p.diagrams.values() {
+ let expected = project_diagram(&p, diagram);
+ let actual = project_diagram_with_experimental_port_policy(
+ &p,
+ diagram,
+ ExperimentalPortPolicy::Current,
+ );
+ assert_eq!(actual, expected);
+ }
+ }
+}
+
+#[test]
+fn alternatives_realize_the_three_previously_forbidden_rectangles() {
+ for policy in POLICIES {
+ for (position, dimensions) in [
+ (pos(0., -10.), size(224., 242.)),
+ (pos(10., 0.), size(214., 232.)),
+ (pos(0., 0.), size(224., 222.)),
+ ] {
+ let mut f = eight_ports();
+ let original = f.p.diagrams[&f.d].nodes[&f.owner].clone();
+ let mut edited = original.clone();
+ edited.position = position;
+ edited.size = dimensions;
+ let before = f.p.clone();
+ f.p = prepare_command(
+ &f.p,
+ &Command::EditNode {
+ diagram: f.d,
+ original: f.owner,
+ node: edited,
+ },
+ )
+ .unwrap()
+ .project;
+ let view = project(&f, policy);
+ let actual = node(&view, f.owner);
+ if policy == ExperimentalPortPolicy::Current {
+ assert_ne!((actual.position, actual.size), (position, dimensions));
+ } else {
+ assert_eq!((actual.position, actual.size), (position, dimensions));
+ }
+ let mut restored = f.p.clone();
+ restored
+ .diagrams
+ .get_mut(&f.d)
+ .unwrap()
+ .nodes
+ .insert(f.owner, original);
+ assert_eq!(restored, before, "Only the selected record may change");
+ }
+ }
+}
+
+#[test]
+fn all_eight_outward_handles_have_exact_fixed_edges_without_authored_inactive_changes() {
+ for policy in ALTERNATIVES {
+ for (left, top, right, bottom) in [
+ (false, true, false, false),
+ (false, true, true, false),
+ (false, false, true, false),
+ (false, false, true, true),
+ (false, false, false, true),
+ (true, false, false, true),
+ (true, false, false, false),
+ (true, true, false, false),
+ ] {
+ let mut f = eight_ports();
+ let baseline = project(&f, policy);
+ let old = node(&baseline, f.owner);
+ assert_eq!(old.size, size(224., 160.));
+ let mut edited = f.p.diagrams[&f.d].nodes[&f.owner].clone();
+ let x = old.position.x - if left { 20. } else { 0. };
+ let y = old.position.y - if top { 20. } else { 0. };
+ let r = old.position.x + old.size.width + if right { 20. } else { 0. };
+ let b = old.position.y + old.size.height + if bottom { 20. } else { 0. };
+ edited.position = pos(x, y);
+ edited.size = size(r - x, b - y);
+ f.p = prepare_command(
+ &f.p,
+ &Command::EditNode {
+ diagram: f.d,
+ original: f.owner,
+ node: edited.clone(),
+ },
+ )
+ .unwrap()
+ .project;
+ let view = project(&f, policy);
+ let actual = node(&view, f.owner);
+ assert_eq!(
+ (actual.position, actual.size),
+ (edited.position, edited.size)
+ );
+ if !left && !right {
+ assert_eq!(edited.position.x, old.position.x);
+ assert_eq!(edited.size.width, old.size.width);
+ }
+ if !top && !bottom {
+ assert_eq!(edited.position.y, old.position.y);
+ assert_eq!(edited.size.height, old.size.height);
+ }
+ }
+ }
+}
+
+#[test]
+fn capacity_uses_available_sides_where_spacing_only_overlaps_fourteen_unit_glyphs() {
+ let f = hundred_ports();
+ let before = f.p.clone();
+ let current = project(&f, ExperimentalPortPolicy::Current);
+ let spacing = project(&f, ExperimentalPortPolicy::SameSidesAdaptSpacing);
+ let capacity = project(&f, ExperimentalPortPolicy::CapacityAware);
+ assert_eq!(node(&current, f.owner).size, size(728., 2808.));
+ assert_eq!(node(&spacing, f.owner).size, size(728., 804.));
+ assert_eq!(capacity.nodes, spacing.nodes);
+ assert!(minimum_gap(&current, f.owner) >= 27.99);
+ assert!(minimum_gap(&spacing, f.owner) < 14.);
+ assert!(minimum_gap(&capacity, f.owner) >= 27.99);
+ assert_eq!(
+ capacity.ports.iter().filter(|p| p.owner == f.owner).count(),
+ 100
+ );
+ assert_eq!(
+ f.p, before,
+ "Projection must not author anchors or new dimensions"
+ );
+ println!(
+ "100 ports: current={:?} spacing={:?} capacity={:?}; gaps={}/{}/{}",
+ node(&current, f.owner).size,
+ node(&spacing, f.owner).size,
+ node(&capacity, f.owner).size,
+ minimum_gap(&current, f.owner),
+ minimum_gap(&spacing, f.owner),
+ minimum_gap(&capacity, f.owner)
+ );
+}
+
+#[test]
+fn manual_anchor_edits_preserve_owner_geometry_and_unrelated_automatic_sides() {
+ let mut f = hundred_ports();
+ let before = project(&f, ExperimentalPortPolicy::CapacityAware);
+ let id = before
+ .ports
+ .iter()
+ .find(|p| p.owner == f.owner && p.side == PortAnchorSide::Right)
+ .unwrap()
+ .element;
+ f.p = prepare_command(
+ &f.p,
+ &Command::SetPortAnchor {
+ diagram: f.d,
+ node: f.owner,
+ anchor: PortAnchor {
+ port: id,
+ side: PortAnchorSide::Top,
+ offset: 0.5,
+ },
+ },
+ )
+ .unwrap()
+ .project;
+ let after = project(&f, ExperimentalPortPolicy::CapacityAware);
+ assert_eq!(after.nodes, before.nodes);
+ assert_eq!(
+ port(&after, id).position,
+ port_anchor_position(node(&after, f.owner), PortAnchorSide::Top, 0.5)
+ );
+ for other in after.ports.iter().filter(|p| p.element != id) {
+ assert_eq!(other.side, port(&before, other.element).side);
+ }
+ assert_eq!(language::parse(&language::format(&f.p)).unwrap(), f.p);
+}
+
+#[test]
+fn exact_crowded_manual_fractions_survive_every_alternative_and_reopen() {
+ for policy in ALTERNATIVES {
+ let mut f = eight_ports();
+ let fractions = [0., 0.25, 0.25, 0.5, 0.75, 1., 0.01, 0.99];
+ f.p.diagrams
+ .get_mut(&f.d)
+ .unwrap()
+ .nodes
+ .get_mut(&f.owner)
+ .unwrap()
+ .port_anchors = f
+ .ports
+ .iter()
+ .zip(fractions)
+ .map(|(port, offset)| PortAnchor {
+ port: *port,
+ side: PortAnchorSide::Right,
+ offset,
+ })
+ .collect();
+ validate_candidate(&f.p).unwrap();
+ let original_anchors = f.p.diagrams[&f.d].nodes[&f.owner].port_anchors.clone();
+ let view = project(&f, policy);
+ assert_eq!(node(&view, f.owner).size, size(224., 160.));
+ for anchor in &original_anchors {
+ let actual = port(&view, anchor.port);
+ assert_eq!(actual.side, anchor.side);
+ assert_eq!(
+ actual.position,
+ port_anchor_position(node(&view, f.owner), anchor.side, anchor.offset)
+ );
+ }
+ let reopened = language::parse(&language::format(&f.p)).unwrap();
+ assert_eq!(reopened, f.p);
+ assert_eq!(
+ project_diagram_with_experimental_port_policy(
+ &reopened,
+ &reopened.diagrams[&f.d],
+ policy
+ ),
+ view
+ );
+ assert_eq!(
+ f.p.diagrams[&f.d].nodes[&f.owner].port_anchors,
+ original_anchors
+ );
+ }
+}
+
+#[test]
+fn narrow_peer_gap_keeps_clear_sides_and_actual_routed_port_endpoints() {
+ let mut p = Project::empty();
+ let a = p
+ .add_element(Kind::Function, Layer::Sa, "Send", None)
+ .unwrap();
+ let b = p
+ .add_element(Kind::Function, Layer::Sa, "Receive", None)
+ .unwrap();
+ let edge = p.relate(RelationKind::FunctionalExchange, "Information", a, b);
+ let RelationBody::FunctionalExchange {
+ source_port,
+ target_port,
+ ..
+ } = p.relations[&edge].body
+ else {
+ unreachable!()
+ };
+ let d = p.ensure_diagram(Layer::Sa);
+ p.place(d, a, pos(100., 100.));
+ p.place(d, b, pos(100., 260.));
+ validate_candidate(&p).unwrap();
+ for policy in POLICIES {
+ let view = project_diagram_with_experimental_port_policy(&p, &p.diagrams[&d], policy);
+ assert_eq!(port(&view, source_port).side, PortAnchorSide::Right);
+ assert_eq!(port(&view, target_port).side, PortAnchorSide::Left);
+ let routed = route_diagram(&p, &p.diagrams[&d], &view);
+ let route = &routed.edges[&edge];
+ assert!(
+ route.warnings.is_empty(),
+ "{policy:?}: {:?}",
+ route.warnings
+ );
+ assert_eq!(
+ route.points.first(),
+ Some(&port(&view, source_port).position)
+ );
+ assert_eq!(
+ route.points.last(),
+ Some(&port(&view, target_port).position)
+ );
+ assert!(
+ route
+ .points
+ .windows(2)
+ .all(|pair| pair[0].x == pair[1].x || pair[0].y == pair[1].y)
+ );
+ }
+}
+
+#[test]
+fn candidate_geometry_is_deterministic_and_independent_of_peer_motion_and_names() {
+ let mut f = eight_ports();
+ let before = project(&f, ExperimentalPortPolicy::CapacityAware);
+ assert_eq!(project(&f, ExperimentalPortPolicy::CapacityAware), before);
+ for element in f.p.elements.values_mut() {
+ element.alias = format!("renamed_{}", element.id.simple());
+ }
+ assert_eq!(project(&f, ExperimentalPortPolicy::CapacityAware), before);
+ for node in
+ f.p.diagrams
+ .get_mut(&f.d)
+ .unwrap()
+ .nodes
+ .values_mut()
+ .filter(|n| n.id != f.owner)
+ {
+ node.position.x += 500.;
+ }
+ let moved = project(&f, ExperimentalPortPolicy::CapacityAware);
+ assert_eq!(
+ node(&moved, f.owner),
+ node(&before, f.owner),
+ "Peer motion must not change owner geometry"
+ );
+}