diff --git a/crates/oa-core/examples/auto_layout_probe.rs b/crates/oa-core/examples/auto_layout_probe.rs
new file mode 100644
index 00000000..d2bf516b
--- /dev/null
+++ b/crates/oa-core/examples/auto_layout_probe.rs
@@ -0,0 +1,671 @@
+//! Experimental observer of Command::Layout; no quality thresholds or candidate algorithm.
+use oa_core::{
+ projection::{self, ProjectedView},
+ routing, *,
+};
+use serde_json::{Value, json};
+use std::collections::{BTreeMap, BTreeSet};
+#[path = "auto_layout_probe/layered.rs"]
+mod layered;
+
+type P = (f64, f64);
+type Rect = (f64, f64, f64, f64);
+const EPS: f64 = 0.0001;
+fn p(v: Position) -> P {
+ (v.x as f64, v.y as f64)
+}
+fn sub(a: P, b: P) -> P {
+ (a.0 - b.0, a.1 - b.1)
+}
+fn dot(a: P, b: P) -> f64 {
+ a.0 * b.0 + a.1 * b.1
+}
+fn cross(a: P, b: P) -> f64 {
+ a.0 * b.1 - a.1 * b.0
+}
+fn length(a: P) -> f64 {
+ a.0.hypot(a.1)
+}
+fn rect(at: Position, size: Size) -> Rect {
+ (
+ at.x as f64,
+ at.y as f64,
+ (at.x + size.width) as f64,
+ (at.y + size.height) as f64,
+ )
+}
+fn overlap(a: Rect, b: Rect) -> f64 {
+ let w = a.2.min(b.2) - a.0.max(b.0);
+ let h = a.3.min(b.3) - a.1.max(b.1);
+ if w > EPS && h > EPS { w * h } else { 0. }
+}
+fn contains(a: Rect, b: Rect) -> bool {
+ a.0 <= b.0 + EPS && a.1 <= b.1 + EPS && a.2 + EPS >= b.2 && a.3 + EPS >= b.3
+}
+fn proper_cross(a: P, b: P, c: P, d: P) -> Option
{
+ let u = sub(b, a);
+ let v = sub(d, c);
+ let denominator = cross(u, v);
+ if denominator.abs() < 1e-9 {
+ return None;
+ }
+ let t = cross(sub(c, a), v) / denominator;
+ let s = cross(sub(c, a), u) / denominator;
+ // Strict interiors: touches, T junctions and intersections at bends are not proper crossings.
+ (t > 1e-9 && t < 1. - 1e-9 && s > 1e-9 && s < 1. - 1e-9)
+ .then_some((a.0 + t * u.0, a.1 + t * u.1))
+}
+fn shared_length(a: P, b: P, c: P, d: P) -> f64 {
+ let u = sub(b, a);
+ let len = length(u);
+ if len < EPS || cross(u, sub(c, a)).abs() / len > EPS || cross(u, sub(d, a)).abs() / len > EPS {
+ return 0.;
+ }
+ let t = dot(sub(c, a), u) / len;
+ let s = dot(sub(d, a), u) / len;
+ (t.max(s).min(len) - t.min(s).max(0.)).max(0.)
+}
+fn ancestor(view: &ProjectedView, owner: Id, mut node: Id) -> bool {
+ for _ in 0..view.nodes.len() {
+ let Some(parent) = view
+ .nodes
+ .iter()
+ .find(|n| n.id == node)
+ .and_then(|n| n.visual_parent)
+ else {
+ return false;
+ };
+ if parent == owner {
+ return true;
+ }
+ node = parent;
+ }
+ false
+}
+fn terminal_points(
+ edge: &projection::ProjectedEdge,
+ points: &[Position],
+ segment: usize,
+) -> Vec<(Id, P)> {
+ let mut owners = Vec::new();
+ if segment == 0 {
+ owners.push((edge.source, p(points[0])));
+ }
+ if segment + 2 == points.len() {
+ owners.push((edge.target, p(*points.last().unwrap())));
+ }
+ owners
+}
+fn inside_length(a: P, b: P, r: Rect) -> f64 {
+ let delta = sub(b, a);
+ let mut lo = 0_f64;
+ let mut hi = 1_f64;
+ for (origin, direction, min, max) in [
+ (a.0, delta.0, r.0 + EPS, r.2 - EPS),
+ (a.1, delta.1, r.1 + EPS, r.3 - EPS),
+ ] {
+ if min >= max {
+ return 0.;
+ }
+ if direction.abs() < 1e-9 {
+ if origin <= min || origin >= max {
+ return 0.;
+ }
+ } else {
+ let t = (min - origin) / direction;
+ let u = (max - origin) / direction;
+ lo = lo.max(t.min(u));
+ hi = hi.min(t.max(u));
+ }
+ }
+ (hi - lo).max(0.) * length(delta)
+}
+fn header_height(project: &Project, diagram: Id, node: &projection::ProjectedNode) -> f64 {
+ let h = if project.diagrams[&diagram]
+ .projection
+ .state_machine()
+ .is_some()
+ {
+ projection::state_header_height(project, node.element)
+ } else {
+ 64.
+ };
+ h.min(node.size.height) as f64
+}
+
+fn metrics(project: &Project, diagram: Id) -> Value {
+ let view = projection::project_view(project, diagram);
+ let routed = routing::route_diagram(project, &project.diagrams[&diagram], &view);
+ let edges: Vec<_> = routed.edges.values().collect();
+ let definitions: BTreeMap<_, _> = view.edges.iter().map(|e| (e.id, e)).collect();
+ let mut crossings = Vec::new();
+ let mut shared = 0.;
+ let mut excluded_shared = 0.;
+ let mut excluded_stub_crossings = 0;
+ for (i, a) in edges.iter().enumerate() {
+ for b in edges.iter().skip(i + 1) {
+ let mut unique = BTreeSet::new();
+ for (ai, ab) in a.points.windows(2).enumerate() {
+ for (bi, cd) in b.points.windows(2).enumerate() {
+ let mut common_tips = Vec::new();
+ for (owner, tip) in terminal_points(definitions[&a.id], &a.points, ai) {
+ if terminal_points(definitions[&b.id], &b.points, bi)
+ .iter()
+ .any(|(other, at)| *other == owner && length(sub(*at, tip)) < EPS)
+ {
+ common_tips.push(tip);
+ }
+ }
+ let crossing = proper_cross(p(ab[0]), p(ab[1]), p(cd[0]), p(cd[1]));
+ let overlap = shared_length(p(ab[0]), p(ab[1]), p(cd[0]), p(cd[1]));
+ // Raw totals stay primary. Only physically common tips get
+ // bounded PORT_STUB exemptions, never a whole compacted segment.
+ shared += overlap;
+ excluded_shared +=
+ overlap.min(common_tips.len() as f64 * routing::PORT_STUB as f64);
+ if let Some(at) = crossing {
+ if unique.insert(((at.0 / EPS).round() as i64, (at.1 / EPS).round() as i64))
+ {
+ let near_tip = common_tips.iter().any(|tip| {
+ length(sub(at, *tip)) <= routing::PORT_STUB as f64 + EPS
+ });
+ excluded_stub_crossings += usize::from(near_tip);
+ crossings.push(json!({"edges":[a.id,b.id],"at":[at.0,at.1],"within_common_terminal_stub":near_tip}));
+ }
+ }
+ }
+ }
+ }
+ }
+ let mut node_collisions = Vec::new();
+ let mut label_nodes = Vec::new();
+ let mut label_labels = Vec::new();
+ let mut excluded_ancestor_labels = 0;
+ let mut penetrations = Vec::new();
+ let mut label_strokes = Vec::new();
+ for e in &edges {
+ let def = definitions[&e.id];
+ for n in &view.nodes {
+ if n.id == def.source || n.id == def.target {
+ continue;
+ }
+ let mut obstacle = rect(n.position, n.size);
+ let ancestor_header =
+ ancestor(&view, n.id, def.source) || ancestor(&view, n.id, def.target);
+ if ancestor_header {
+ obstacle.3 = obstacle.1 + header_height(project, diagram, n);
+ }
+ let length: f64 = e
+ .points
+ .windows(2)
+ .map(|w| inside_length(p(w[0]), p(w[1]), obstacle))
+ .sum();
+ if length > EPS {
+ penetrations.push(json!({"edge":e.id,"node":n.id,"length":length,"ancestor_header_only":ancestor_header}));
+ }
+ }
+ }
+ for (i, a) in view.nodes.iter().enumerate() {
+ for b in view.nodes.iter().skip(i + 1) {
+ let area = overlap(rect(a.position, a.size), rect(b.position, b.size));
+ if area > 0. && !ancestor(&view, a.id, b.id) && !ancestor(&view, b.id, a.id) {
+ node_collisions.push(json!({"nodes":[a.id,b.id],"intersection_area":area}));
+ }
+ }
+ }
+ let labels: Vec<_> = edges.iter().filter(|e| !e.label.text.is_empty()).collect();
+ for (i, e) in labels.iter().enumerate() {
+ let lb = rect(e.label.bounds.position, e.label.bounds.size);
+ let def = definitions[&e.id];
+ for n in &view.nodes {
+ let nb = rect(n.position, n.size);
+ let area = overlap(lb, nb);
+ if area == 0. {
+ continue;
+ }
+ let body = (nb.0, nb.1 + header_height(project, diagram, n), nb.2, nb.3);
+ if contains(body, lb)
+ && (ancestor(&view, n.id, def.source) || ancestor(&view, n.id, def.target))
+ {
+ excluded_ancestor_labels += 1;
+ } else {
+ label_nodes.push(json!({"edge":e.id,"node":n.id,"intersection_area":area}));
+ }
+ }
+ for other in labels.iter().skip(i + 1) {
+ let area = overlap(
+ lb,
+ rect(other.label.bounds.position, other.label.bounds.size),
+ );
+ if area > 0. {
+ label_labels.push(json!({"edges":[e.id,other.id],"intersection_area":area}));
+ }
+ }
+ for other in edges.iter().filter(|other| other.id != e.id) {
+ let length: f64 = other
+ .points
+ .windows(2)
+ .map(|w| inside_length(p(w[0]), p(w[1]), lb))
+ .sum();
+ if length > EPS {
+ label_strokes
+ .push(json!({"label_edge":e.id,"stroke_edge":other.id,"length":length}));
+ }
+ }
+ }
+ let mut total_length = 0.;
+ let mut bends = 0;
+ for e in &edges {
+ total_length += e
+ .points
+ .windows(2)
+ .map(|w| length(sub(p(w[1]), p(w[0]))))
+ .sum::();
+ bends += e
+ .points
+ .windows(3)
+ .filter(|w| {
+ let a = sub(p(w[1]), p(w[0]));
+ let b = sub(p(w[2]), p(w[1]));
+ length(a) > EPS && length(b) > EPS && (cross(a, b).abs() > EPS || dot(a, b) < 0.)
+ })
+ .count();
+ }
+ let warnings: Vec<_> = edges
+ .iter()
+ .flat_map(|e| {
+ e.warnings
+ .iter()
+ .map(move |w| json!({"edge":e.id,"warning":format!("{w:?}")}))
+ })
+ .collect();
+ json!({
+ "nodes":view.nodes.len(),"edges":edges.len(),"ports":view.ports.len(),
+ "proper_crossings":crossings.len(),"crossing_details":crossings,
+ "pairwise_collinear_shared_length":shared,
+ "excluded_common_terminal_shared_length":excluded_shared,"excluded_common_terminal_crossings":excluded_stub_crossings,
+ "nonterminal_proper_crossings":crossings.len()-excluded_stub_crossings,
+ "nonterminal_pairwise_shared_length":shared-excluded_shared,
+ "unrelated_edge_node_penetrations":penetrations.len(),"edge_node_details":penetrations,
+ "unrelated_label_stroke_intersections":label_strokes.len(),"label_stroke_details":label_strokes,
+ "unrelated_node_collisions":node_collisions.len(),"node_collision_details":node_collisions,
+ "label_node_collisions":label_nodes.len(),"label_node_details":label_nodes,
+ "excluded_contained_endpoint_ancestor_label_pairs":excluded_ancestor_labels,
+ "label_label_collisions":label_labels.len(),"label_label_details":label_labels,
+ "warning_count":warnings.len(),"warnings":warnings,
+ "drawing_bounds":{"position":routed.bounds.position,"size":routed.bounds.size},
+ "drawing_area":routed.bounds.size.width as f64*routed.bounds.size.height as f64,
+ "route_bends":bends,"route_length":total_length,
+ "projected_nodes":view.nodes,"projected_ports":view.ports,
+ "routed_edges":edges.iter().map(|e|json!({"id":e.id,"points":e.points,"label":{
+ "text":e.label.text,"position":e.label.bounds.position,"size":e.label.bounds.size}})).collect::>()
+ })
+}
+
+struct Builder {
+ project: Project,
+ diagram: Id,
+ next: u128,
+ witness: Option,
+}
+impl Builder {
+ fn new(name: &str) -> Self {
+ let mut project = Project::empty();
+ project.id = Id::from_u128(90_000);
+ project.alias = "layout_probe".into();
+ project.name = name.into();
+ let old = project.ensure_diagram(Layer::Sa);
+ let diagram = Id::from_u128(90_001);
+ reidentify(&mut project, old, diagram).unwrap();
+ project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::FunctionalDataflow;
+ Self {
+ project,
+ diagram,
+ next: 1,
+ witness: None,
+ }
+ }
+ fn stable(&mut self, old: Id) -> Id {
+ let id = Id::from_u128(self.next);
+ self.next += 1;
+ reidentify(&mut self.project, old, id).unwrap();
+ id
+ }
+ fn function(&mut self, name: &str, parent: Option, x: f32, y: f32) -> Id {
+ let old = self
+ .project
+ .add_element(Kind::Function, Layer::Sa, name, parent)
+ .unwrap();
+ let id = self.stable(old);
+ let old = self.project.place(self.diagram, id, Position { x, y });
+ self.stable(old);
+ id
+ }
+ fn exchange(&mut self, name: &str, a: Id, b: Id) -> Id {
+ let old = self
+ .project
+ .relate(RelationKind::FunctionalExchange, name, a, b);
+ let edge = self.stable(old);
+ let RelationBody::FunctionalExchange {
+ source_port,
+ target_port,
+ ..
+ } = self.project.relations[&edge].body
+ else {
+ unreachable!()
+ };
+ for (old, prefix) in [(source_port, "out"), (target_port, "in")] {
+ let id = self.stable(old);
+ self.project.elements.get_mut(&id).unwrap().alias =
+ format!("{prefix}_{}", id.as_u128());
+ }
+ edge
+ }
+ fn node(&self, element: Id) -> Id {
+ self.project.diagrams[&self.diagram]
+ .nodes
+ .values()
+ .find(|n| n.element == element)
+ .unwrap()
+ .id
+ }
+}
+fn fixtures() -> Vec<(&'static str, Builder)> {
+ let mut all = Vec::new();
+ for (name, count) in [("flat_chain_4", 4), ("chain_7_row_wrap", 7)] {
+ let mut f = Builder::new(name);
+ let mut nodes = Vec::new();
+ for i in 0..count {
+ nodes.push(f.function(&format!("Function {i}"), None, 40. + 320. * i as f32, 80.));
+ }
+ for i in 0..count - 1 {
+ f.exchange(&format!("Flow {i}"), nodes[i], nodes[i + 1]);
+ }
+ all.push((name, f));
+ }
+ for (name, links) in [
+ ("diamond", vec![(0, 1), (0, 2), (1, 3), (2, 3)]),
+ ("crossed_bipartite", vec![(0, 3), (1, 2), (0, 2), (1, 3)]),
+ ] {
+ let mut f = Builder::new(name);
+ let mut nodes = Vec::new();
+ let coordinates = if name == "diamond" {
+ [(40., 240.), (400., 80.), (400., 400.), (760., 240.)]
+ } else {
+ [(40., 80.), (40., 400.), (620., 80.), (620., 400.)]
+ };
+ for (i, (x, y)) in coordinates.into_iter().enumerate() {
+ nodes.push(f.function(&format!("Node {i}"), None, x, y));
+ }
+ for (i, (a, b)) in links.into_iter().enumerate() {
+ f.exchange(&format!("Flow {i}"), nodes[a], nodes[b]);
+ }
+ all.push((name, f));
+ }
+ let mut f = Builder::new("nested_cross_boundary");
+ let group = f.function("Parent function", None, 40., 40.);
+ let a = f.function("Child send", Some(group), 100., 130.);
+ let b = f.function("Child receive", Some(group), 100., 330.);
+ let c = f.function("Outside", None, 900., 210.);
+ f.exchange("Internal flow", a, b);
+ f.exchange("Cross boundary", b, c);
+ all.push(("nested_cross_boundary", f));
+ for fixed in [false, true] {
+ let name = if fixed {
+ "fixed_ports"
+ } else {
+ "authored_detour_and_label"
+ };
+ let mut f = Builder::new(name);
+ let a = f.function("Sender", None, 40., 80.);
+ let b = f.function("Receiver", None, 640., 80.);
+ let edge = f.exchange("Preserved information", a, b);
+ if fixed {
+ let RelationBody::FunctionalExchange {
+ source_port,
+ target_port,
+ ..
+ } = f.project.relations[&edge].body
+ else {
+ unreachable!()
+ };
+ for (element, port, side, offset) in [
+ (a, source_port, PortAnchorSide::Left, 0.3),
+ (b, target_port, PortAnchorSide::Right, 0.7),
+ ] {
+ let node = f.node(element);
+ f.project = prepare_command(
+ &f.project,
+ &Command::SetPortAnchor {
+ diagram: f.diagram,
+ node,
+ anchor: PortAnchor { port, side, offset },
+ },
+ )
+ .unwrap()
+ .project;
+ }
+ } else {
+ f.project
+ .diagrams
+ .get_mut(&f.diagram)
+ .unwrap()
+ .routes
+ .push(EdgeRoute {
+ edge,
+ source_attachment: None,
+ target_attachment: None,
+ waypoints: vec![
+ Position { x: 400., y: -320. },
+ Position { x: 1000., y: -320. },
+ Position { x: 1000., y: 240. },
+ ],
+ label: Some(EdgeLabel {
+ fraction: 0.7,
+ offset: Position { x: 100., y: -180. },
+ }),
+ });
+ }
+ all.push((name, f));
+ }
+ let mut f = Builder::new("two_containers_reversed_children");
+ let left = f.function("Left group", None, 40., 40.);
+ let right = f.function("Right group", None, 820., 40.);
+ let la = f.function("Left A", Some(left), 100., 130.);
+ let lb = f.function("Left B", Some(left), 100., 330.);
+ let ra = f.function("Right A", Some(right), 880., 130.);
+ let rb = f.function("Right B", Some(right), 880., 330.);
+ f.exchange("Upper to lower", la, rb);
+ f.exchange("Lower to upper", lb, ra);
+ let mut witness = f.project.clone();
+ for (element, y) in [(ra, 330.), (rb, 130.)] {
+ let node = f.node(element);
+ witness
+ .diagrams
+ .get_mut(&f.diagram)
+ .unwrap()
+ .nodes
+ .get_mut(&node)
+ .unwrap()
+ .position
+ .y = y;
+ }
+ f.witness = Some(witness);
+ all.push(("two_containers_reversed_children", f));
+ let mut f = Builder::new("long_ordinary_labels");
+ let a = f.function("Input", None, 40., 100.);
+ let b = f.function("Transform", None, 400., 100.);
+ let c = f.function("Output", None, 760., 100.);
+ f.exchange(
+ "A very long first information exchange label exceeding the ordinary cutoff",
+ a,
+ b,
+ );
+ f.exchange(
+ "A very long second information exchange label exceeding the ordinary cutoff",
+ b,
+ c,
+ );
+ f.exchange(
+ "A very long bypass information exchange label exceeding the ordinary cutoff",
+ a,
+ c,
+ );
+ all.push(("long_ordinary_labels", f));
+ all
+}
+
+fn reset_effective_routes(project: &Project, diagram: Id) -> Project {
+ let view = projection::project_view(project, diagram);
+ let routed = routing::route_diagram(project, &project.diagrams[&diagram], &view);
+ let mut reset = project.clone();
+ for route in &mut reset.diagrams.get_mut(&diagram).unwrap().routes {
+ if routed
+ .edges
+ .get(&route.edge)
+ .is_some_and(|edge| !edge.custom_suspended)
+ {
+ route.waypoints.clear();
+ route.label = None;
+ }
+ }
+ reset
+}
+
+fn conservation(before: &Project, after: &Project, diagram: Id) -> Value {
+ let mut semantic = after.clone();
+ semantic.diagrams = before.diagrams.clone();
+ let bd = &before.diagrams[&diagram];
+ let ad = &after.diagrams[&diagram];
+ let mut stable_view = ad.clone();
+ stable_view.nodes = bd.nodes.clone();
+ stable_view.routes = bd.routes.clone();
+ stable_view.layout = bd.layout.clone();
+ let mut other_views = after.diagrams.clone();
+ other_views.insert(diagram, bd.clone());
+ let node_metadata = bd.nodes.len() == ad.nodes.len()
+ && bd.nodes.iter().all(|(id, node)| {
+ ad.nodes.get(id).is_some_and(|new| {
+ let mut restored = new.clone();
+ restored.position = node.position;
+ restored.size = node.size;
+ restored == *node
+ })
+ });
+ json!({"semantic_records_exact":semantic==*before,"target_view_nongeometry_exact":stable_view==*bd,
+ "other_views_exact":other_views==before.diagrams,"node_identity_visibility_anchors_exact":node_metadata,
+ "authored_routes_exact":ad.routes==bd.routes})
+}
+
+fn roundtrip(project: &Project) -> Value {
+ let bytes = serde_json::to_vec(project).unwrap();
+ let json_result = persistence::decode(&bytes);
+ let source = language::format(project);
+ let source_result = language::parse(&source);
+ json!({"json_exact":json_result.as_ref().is_ok_and(|p|p==project),
+ "json_error":json_result.err().map(|e|e.to_string()),
+ "single_source_exact":source_result.as_ref().is_ok_and(|p|p==project),
+ "source_error":source_result.err().map(|e|format!("{e:?}"))})
+}
+fn observe(name: &str, f: Builder) -> Value {
+ let witness = f.witness;
+ let before = f.project;
+ let diagram = f.diagram;
+ if let Err(error) = validate_candidate(&before) {
+ return json!({"fixture":name,"fixture_error":error.to_string()});
+ }
+ let mut session = Session::new(before.clone());
+ if let Err(error) = session.execute(Command::Layout(diagram)) {
+ return json!({"fixture":name,"before":metrics(&before,diagram),"layout_error":error.to_string()});
+ }
+ let after = session.project().clone();
+ let changed = after != before;
+ let had_undo = session.can_undo();
+ session.undo();
+ let undo = session.project() == &before;
+ let one_step = !session.can_undo();
+ session.redo();
+ let redo = session.project() == &after;
+ let second = prepare_command(&after, &Command::Layout(diagram));
+ let reset = reset_effective_routes(&after, diagram);
+ let mut repeated = after.clone();
+ let mut stability = Vec::new();
+ for pass in 2..=10 {
+ match prepare_command(&repeated, &Command::Layout(diagram)) {
+ Ok(next) => {
+ stability.push(json!({"pass":pass,"equal_previous":next.project==repeated,"equal_first":next.project==after}));
+ repeated = next.project;
+ }
+ Err(error) => {
+ stability.push(json!({"pass":pass,"error":error.to_string()}));
+ break;
+ }
+ }
+ }
+ let witness=witness.map(|p|json!({"metrics":metrics(&p,diagram),"project":p,
+ "conservation":conservation(&before,&p,diagram),"validation_error":validate_candidate(&p).err().map(|e|e.to_string())}));
+ let candidate = match layered::candidate(&before, diagram) {
+ Err(error) => json!({"supported":false,"error":error.to_string()}),
+ Ok(candidate) => {
+ if let Err(error) = validate_candidate(&candidate) {
+ json!({"supported":true,"validation_error":error.to_string(),"project":candidate})
+ } else {
+ let again = layered::candidate(&candidate, diagram);
+ let duplicate = layered::candidate(&before, diagram);
+ let mut repeated_candidate = candidate.clone();
+ let mut candidate_stability = Vec::new();
+ for pass in 2..=10 {
+ match layered::candidate(&repeated_candidate, diagram) {
+ Ok(next) => {
+ candidate_stability.push(json!({"pass":pass,"equal_previous":next==repeated_candidate,"equal_first":next==candidate}));
+ repeated_candidate = next;
+ }
+ Err(error) => {
+ candidate_stability
+ .push(json!({"pass":pass,"error":error.to_string()}));
+ break;
+ }
+ }
+ }
+ json!({"supported":true,"metrics":metrics(&candidate,diagram),"project":candidate,
+ "conservation":conservation(&before,&candidate,diagram),"roundtrip":roundtrip(&candidate),
+ "ten_candidate_calls":candidate_stability,
+ "repeat_same_input_exact":duplicate.as_ref().is_ok_and(|p|*p==candidate),
+ "idempotent":again.as_ref().is_ok_and(|p|*p==candidate),
+ "repeat_error":duplicate.err().map(|e|e.to_string()),"idempotence_error":again.err().map(|e|e.to_string())})
+ }
+ }
+ };
+ json!({"fixture":name,"diagram":diagram,"before":metrics(&before,diagram),"after":metrics(&after,diagram),
+ "before_project":before,"after_project":after,"conservation":conservation(&before,&after,diagram),
+ "reset_only_control":{"metrics":metrics(&reset,diagram),"project":reset,"conservation":conservation(&before,&reset,diagram),"roundtrip":roundtrip(&reset)},
+ "manual_planar_order_witness":witness,"ten_layout_calls":stability,"experimental_candidate":candidate,
+ "checks":{
+ "changed":changed,"history_entry_matches_change":had_undo==changed,"undo_exact":undo,"one_step_undo":one_step,"redo_exact":redo,
+ "second_layout_exact":second.as_ref().is_ok_and(|s|s.project==after),"second_layout_error":second.err().map(|e|e.to_string()),
+ "before_roundtrip":roundtrip(&before),"after_roundtrip":roundtrip(&after)}})
+}
+fn main() {
+ // Analytic checks validate the observer, not any layout's quality.
+ assert_eq!(
+ proper_cross((0., 0.), (10., 0.), (5., -5.), (5., 5.)),
+ Some((5., 0.))
+ );
+ assert_eq!(
+ proper_cross((0., 0.), (10., 0.), (10., 0.), (10., 5.)),
+ None
+ );
+ assert!((shared_length((0., 0.), (10., 0.), (4., 0.), (12., 0.)) - 6.).abs() < EPS);
+ assert_eq!(shared_length((0., 0.), (10., 0.), (4., 1.), (12., 1.)), 0.);
+ assert_eq!(overlap((0., 0., 10., 10.), (10., 0., 20., 10.)), 0.);
+ assert!((inside_length((0., 5.), (10., 5.), (2., 2., 8., 8.)) - 6.).abs() < 3. * EPS);
+ assert_eq!(inside_length((0., 2.), (10., 2.), (2., 2., 8., 8.)), 0.);
+ println!(
+ "{}",
+ json!({"probe":"auto_layout_probe_v1","reference_source":"715036e132677dd2bc2fb37ed316d93dc9987aec","metric_selfchecks":"passed", "quality_thresholds":null,
+ "exclusions":"Raw strict-interior crossings and pairwise shared length are primary; separately bound common physical endpoint exclusions to PORT_STUB. Ancestor node containment excluded. Labels wholly inside endpoint-ancestor BODY below header exempt; headers retained. Penetration excludes endpoint nodes and endpoint-ancestor bodies. Label/stroke excludes own edge. Core approximate label bounds, no glyph/port-label metrics."})
+ );
+ for (name, f) in fixtures() {
+ println!("{}", observe(name, f));
+ }
+}
diff --git a/crates/oa-core/examples/auto_layout_probe/layered.rs b/crates/oa-core/examples/auto_layout_probe/layered.rs
new file mode 100644
index 00000000..3d4c9b49
--- /dev/null
+++ b/crates/oa-core/examples/auto_layout_probe/layered.rs
@@ -0,0 +1,380 @@
+//! Isolated #32 experiment, not a production layout engine.
+//! Uses existing Layout sizing, three deterministic ordering passes, and one
+//! core-estimated-label spacing pass. Automatic elbows remain derived. Folded/omitted
+//! node topology is explicitly unsupported; filtered edges retain their metadata.
+use oa_core::{Command, Id, ModelError, Position, Project, Size, projection, routing};
+use std::collections::{BTreeMap, BTreeSet};
+
+const WORK_LIMIT: usize = 200_000;
+const NODE_LIMIT: usize = 256;
+const EDGE_LIMIT: usize = 1024;
+
+fn spend(work: &mut usize) -> Result<(), ModelError> {
+ *work += 1;
+ if *work > WORK_LIMIT {
+ return Err(ModelError("Layered probe work limit exceeded.".into()));
+ }
+ Ok(())
+}
+
+/// Return a new Project; caller owns comparison, history and serialization.
+pub fn candidate(project: &Project, diagram: Id) -> Result {
+ let original = project
+ .diagrams
+ .get(&diagram)
+ .ok_or_else(|| ModelError("Missing probe diagram.".into()))?;
+ let view = projection::project_diagram(project, original);
+ if view.nodes.len() > NODE_LIMIT || view.edges.len() > EDGE_LIMIT {
+ return Err(ModelError("Layered probe graph limit exceeded.".into()));
+ }
+ if view.nodes.len() != original.nodes.len() || view.nodes.iter().any(|n| n.collapsed) {
+ return Err(ModelError(
+ "Layered probe does not support folded/omitted node topology.".into(),
+ ));
+ }
+ let routed = routing::route_diagram(project, original, &view);
+ let eligible: BTreeSet<_> = view
+ .edges
+ .iter()
+ .filter(|edge| {
+ projection::edge_endpoint_placements(project, original, edge.id)
+ == Some((edge.source, edge.target))
+ && routed
+ .edges
+ .get(&edge.id)
+ .is_some_and(|route| !route.custom_suspended)
+ })
+ .map(|edge| edge.id)
+ .collect();
+ let mut result = oa_core::prepare_command(project, &Command::Layout(diagram))?.project;
+ // Discard baseline route translation: final routes are reconstructed directly
+ // from original geometry, avoiding two translations and preserving list order.
+ result.diagrams.get_mut(&diagram).unwrap().routes = original.routes.clone();
+ for route in &mut result.diagrams.get_mut(&diagram).unwrap().routes {
+ if eligible.contains(&route.edge) {
+ route.waypoints.clear();
+ route.label = None;
+ }
+ }
+ let baseline = projection::project_view(&result, diagram);
+ let labels = routing::route_diagram(&result, &result.diagrams[&diagram], &baseline);
+ let mut gap_x = 64.0_f32;
+ let mut gap_y = 32.0_f32;
+ for edge in labels.edges.values() {
+ gap_x = gap_x.max(edge.label.bounds.size.width + 24.0);
+ gap_y = gap_y.max(edge.label.bounds.size.height + 24.0);
+ if let Some(relation) = result.relations.get(&edge.id) {
+ for end in routing::association_end_geometry(relation, edge) {
+ gap_x = gap_x.max(end.label.bounds.size.width + 24.0);
+ gap_y = gap_y.max(end.label.bounds.size.height + 24.0);
+ }
+ }
+ }
+ if !gap_x.is_finite() || !gap_y.is_finite() {
+ return Err(ModelError("Nonfinite estimated label bound.".into()));
+ }
+ let parents: BTreeMap<_, _> = baseline
+ .nodes
+ .iter()
+ .map(|n| (n.id, n.visual_parent))
+ .collect();
+ let mut children: BTreeMap