Add persisted per-view port anchors and connection-aware border placement

This commit is contained in:
2026-09-06 01:20:28 +03:00
parent 93fd32c684
commit 35133de650
13 changed files with 1435 additions and 149 deletions
+816
View File
@@ -0,0 +1,816 @@
use oa_core::{language, persistence, projection::*, routing::*, source_project::*, *};
use serde_json::json;
use std::collections::BTreeMap;
fn pos(x: f32, y: f32) -> Position {
Position { x, y }
}
fn fixture() -> (Project, Id, Id, Id, Id, Id, Id) {
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);
let na = p.place(d, a, pos(100., 100.));
let nb = p.place(d, b, pos(900., 100.));
(p, d, na, nb, source_port, target_port, edge)
}
fn manual(p: &mut Project, d: Id, n: Id, port: Id, side: PortAnchorSide, offset: f32) {
let prepared = prepare_command(
p,
&Command::SetPortAnchor {
diagram: d,
node: n,
anchor: PortAnchor { port, side, offset },
},
)
.unwrap();
assert_eq!(prepared.selected, Some(port));
*p = prepared.project;
}
fn port(view: &ProjectedView, id: Id) -> &ProjectedPort {
view.ports.iter().find(|p| p.element == id).unwrap()
}
#[test]
fn every_side_uses_exact_final_border_fraction_and_outward_routing_normal() {
for side in [
PortAnchorSide::Left,
PortAnchorSide::Right,
PortAnchorSide::Top,
PortAnchorSide::Bottom,
] {
let (mut p, d, na, _, a, _, edge) = fixture();
let before = project_diagram(&p, &p.diagrams[&d]);
manual(&mut p, d, na, a, side, 0.3);
let view = project_diagram(&p, &p.diagrams[&d]);
assert_eq!(
view.nodes, before.nodes,
"A port drag must not resize or move its owner"
);
let node = view.nodes.iter().find(|n| n.id == na).unwrap();
let anchor = port(&view, a);
assert_eq!(anchor.side, side);
assert_eq!(anchor.position, port_anchor_position(node, side, 0.3));
assert!((port_anchor_offset(node, side, anchor.position) - 0.3).abs() < 0.00001);
let route = route_diagram(&p, &p.diagrams[&d], &view);
let points = &route.edges[&edge].points;
assert_eq!(points[0], anchor.position);
let normal = side.normal();
let delta = pos(points[1].x - points[0].x, points[1].y - points[0].y);
assert!(
delta.x * normal.x + delta.y * normal.y >= PORT_STUB - 0.001,
"{side:?}: {points:?}"
);
assert!(
points
.windows(2)
.all(|w| w[0].x == w[1].x || w[0].y == w[1].y)
);
}
}
#[test]
fn neutral_connected_ports_face_peers_and_unconnected_ports_use_four_sides() {
for (peer, expected) in [
(pos(-700., 100.), PortAnchorSide::Left),
(pos(900., 100.), PortAnchorSide::Right),
(pos(100., -700.), PortAnchorSide::Top),
(pos(100., 900.), PortAnchorSide::Bottom),
] {
let mut p = Project::empty();
let a = p
.add_element(Kind::System, Layer::Sa, "System", None)
.unwrap();
let b = p
.add_element(Kind::SystemActor, Layer::Sa, "Partner", None)
.unwrap();
let mut ports = Vec::new();
for owner in [a, b] {
let id = Id::new_v4();
let port = make_port(
&p,
id,
owner,
"Boundary",
PortContract::Component {
kind: ComponentPortKind::Standard,
orientation: ComponentPortOrientation::Unset,
},
)
.unwrap();
p.elements.insert(id, port);
ports.push(id);
}
p.relate(
RelationKind::BoundaryExchange,
"Connection",
ports[0],
ports[1],
);
let d = p.ensure_diagram(Layer::Sa);
p.place(d, a, pos(100., 100.));
p.place(d, b, peer);
let view = project_diagram(&p, &p.diagrams[&d]);
assert_eq!(port(&view, ports[0]).side, expected);
let before = view.ports.clone();
for element in p.elements.values_mut() {
element.alias = format!("renamed_{}", element.id.simple());
}
assert_eq!(project_diagram(&p, &p.diagrams[&d]).ports, before);
p.relations.clear();
for i in 0..3 {
let id = Id::new_v4();
let port = make_port(
&p,
id,
a,
&format!("Spare {i}"),
PortContract::Component {
kind: ComponentPortKind::Standard,
orientation: ComponentPortOrientation::Unset,
},
)
.unwrap();
p.elements.insert(id, port);
}
let view = project_diagram(&p, &p.diagrams[&d]);
let sides: std::collections::BTreeSet<_> = view
.ports
.iter()
.filter(|port| p.elements[&port.element].parent == Some(a))
.map(|port| port.side)
.collect();
assert_eq!(sides.len(), 4);
}
}
#[test]
fn port_upsert_reset_and_undo_preserve_unrelated_node_fields_and_list_order() {
let (mut p, d, n, _, a, _, _) = fixture();
let owner = p.diagrams[&d].nodes[&n].element;
let extra = Id::new_v4();
let spare = make_port(
&p,
extra,
owner,
"Spare",
PortContract::FunctionOutput {
outgoing_items: vec![],
},
)
.unwrap();
p.elements.insert(extra, spare);
manual(&mut p, d, n, a, PortAnchorSide::Top, 0.2);
manual(&mut p, d, n, extra, PortAnchorSide::Bottom, 0.7);
let before = p.clone();
let mut session = Session::new(p);
session
.execute(Command::SetPortAnchor {
diagram: d,
node: n,
anchor: PortAnchor {
port: a,
side: PortAnchorSide::Right,
offset: 0.45,
},
})
.unwrap();
let changed = session.project().clone();
let anchors = &changed.diagrams[&d].nodes[&n].port_anchors;
assert_eq!(
anchors.iter().map(|a| a.port).collect::<Vec<_>>(),
[a, extra]
);
assert_eq!(anchors[1], before.diagrams[&d].nodes[&n].port_anchors[1]);
assert_eq!(
changed.diagrams[&d].nodes[&n].position,
before.diagrams[&d].nodes[&n].position
);
session.undo();
assert_eq!(session.project(), &before);
session.redo();
assert_eq!(session.project(), &changed);
session
.execute(Command::RemovePortAnchor {
diagram: d,
node: n,
port: a,
})
.unwrap();
assert_eq!(
session.project().diagrams[&d].nodes[&n].port_anchors.len(),
1
);
let revision = session.revision();
session
.execute(Command::RemovePortAnchor {
diagram: d,
node: n,
port: a,
})
.unwrap();
assert_eq!(session.revision(), revision);
}
#[test]
fn invalid_anchors_and_payloads_fail_atomically_including_duplicate_owner_placements() {
let (p, d, n, _, port, other_port, _) = fixture();
for (port, offset) in [
(port, f32::NAN),
(port, f32::INFINITY),
(port, -0.1),
(port, 1.1),
(other_port, 0.5),
(Id::new_v4(), 0.5),
(p.diagrams[&d].nodes[&n].element, 0.5),
] {
let mut session = Session::new(p.clone());
assert!(
session
.execute(Command::SetPortAnchor {
diagram: d,
node: n,
anchor: PortAnchor {
port,
side: PortAnchorSide::Left,
offset
}
})
.is_err()
);
assert_eq!(session.project(), &p);
assert_eq!(session.revision(), 0);
}
let anchor = json!({"port": port, "side": "Top", "offset": 0.5});
for bad in [
json!({"port":port,"side":"Top"}),
json!({"port":port,"side":"Diagonal","offset":0.5}),
json!({"port":port,"side":"Top","offset":0.5,"owner":n}),
] {
assert!(serde_json::from_value::<PortAnchor>(bad).is_err());
}
assert!(
serde_json::from_value::<Command>(
json!({"SetPortAnchor":{"diagram":d,"node":n,"anchor":anchor,"unknown":true}})
)
.is_err()
);
let mut duplicate = p.clone();
let value = PortAnchor {
port,
side: PortAnchorSide::Left,
offset: 0.5,
};
duplicate
.diagrams
.get_mut(&d)
.unwrap()
.nodes
.get_mut(&n)
.unwrap()
.port_anchors = vec![value.clone(), value];
assert!(prepare_command(&p, &Command::Replace(duplicate)).is_err());
let mut duplicate = p.diagrams[&d].nodes[&n].clone();
duplicate.id = Id::new_v4();
duplicate.alias = "duplicate_view".into();
assert!(
prepare_command(
&p,
&Command::CreateNode {
diagram: d,
node: duplicate
}
)
.is_err()
);
}
#[test]
fn reidentification_deletion_reuse_and_owner_repair_preserve_correct_identity() {
let (mut p, d, n, target, _, _, _) = fixture();
let owner = p.diagrams[&d].nodes[&n].element;
let id = Id::new_v4();
let value = make_port(
&p,
id,
owner,
"Spare output",
PortContract::FunctionOutput {
outgoing_items: vec![],
},
)
.unwrap();
p.elements.insert(id, value.clone());
manual(&mut p, d, n, id, PortAnchorSide::Top, 0.25);
let new_id = Id::new_v4();
let renamed = prepare_command(&p, &Command::Reidentify(id, new_id))
.unwrap()
.project;
assert_eq!(renamed.diagrams[&d].nodes[&n].port_anchors[0].port, new_id);
let reused = prepare_command(
&p,
&Command::Batch(vec![Command::Delete(id), Command::CreateElement(value)]),
)
.unwrap()
.project;
assert!(reused.diagrams[&d].nodes[&n].port_anchors.is_empty());
let parent = Some(p.diagrams[&d].nodes[&target].element);
assert!(
prepare_command(
&p,
&Command::Reparent {
element: id,
parent
}
)
.is_err()
);
let mut edited = p.elements[&id].clone();
edited.parent = parent;
assert!(prepare_command(&p, &Command::EditElement(id, edited.clone())).is_err());
let repaired = prepare_command(
&p,
&Command::Batch(vec![
Command::RemovePortAnchor {
diagram: d,
node: n,
port: id,
},
Command::EditElement(id, edited),
]),
)
.unwrap()
.project;
assert!(repaired.diagrams[&d].nodes[&n].port_anchors.is_empty());
assert_eq!(repaired.elements[&id].parent, parent);
}
#[test]
fn separate_views_resize_layout_and_hidden_owners_keep_manual_metadata() {
let (mut p, d, n, _, a, _, _) = fixture();
manual(&mut p, d, n, a, PortAnchorSide::Top, 0.35);
let mut second = p.diagrams[&d].clone();
second.id = Id::new_v4();
second.alias = "review_view".into();
second.nodes.clear();
let other_d = second.id;
p.diagrams.insert(second.id, second);
let owner = p.diagrams[&d].nodes[&n].element;
let other_n = p.place(other_d, owner, pos(50., 50.));
manual(&mut p, other_d, other_n, a, PortAnchorSide::Bottom, 0.8);
let original = p.diagrams[&d].nodes[&n].port_anchors.clone();
let mut edited = p.diagrams[&d].nodes[&n].clone();
edited.size = Size {
width: 600.,
height: 450.,
};
edited.position = pos(200., 250.);
p = prepare_command(
&p,
&Command::EditNode {
diagram: d,
original: n,
node: edited,
},
)
.unwrap()
.project;
let view = project_diagram(&p, &p.diagrams[&d]);
assert_eq!(port(&view, a).position, pos(410., 250.));
p = prepare_command(&p, &Command::Layout(d)).unwrap().project;
assert_eq!(p.diagrams[&d].nodes[&n].port_anchors, original);
assert_eq!(
p.diagrams[&other_d].nodes[&other_n].port_anchors[0].side,
PortAnchorSide::Bottom
);
let mut diagram = p.diagrams[&d].clone();
diagram.projection = ProjectionKind::Capabilities;
p = prepare_command(&p, &Command::EditDiagram(d, diagram))
.unwrap()
.project;
assert!(project_diagram(&p, &p.diagrams[&d]).ports.is_empty());
assert_eq!(p.diagrams[&d].nodes[&n].port_anchors, original);
}
#[test]
fn nested_owner_bounds_group_movement_and_authored_routes_share_the_same_geometry() {
let (mut p, d, na, nb, a, b, edge) = fixture();
let parent = p
.add_element(Kind::Function, Layer::Sa, "Group", None)
.unwrap();
for node in [na, nb] {
let element = p.diagrams[&d].nodes[&node].element;
p.elements.get_mut(&element).unwrap().parent = Some(parent);
}
let group = p.place(d, parent, pos(50., 0.));
let obstacle = p
.add_element(Kind::Function, Layer::Sa, "Sibling obstacle", Some(parent))
.unwrap();
let obstacle_node = p.place(d, obstacle, pos(440., 160.));
let group_port = Id::new_v4();
let element = make_port(
&p,
group_port,
parent,
"Group input",
PortContract::FunctionInput {
incoming_items: vec![],
},
)
.unwrap();
p.elements.insert(group_port, element);
manual(&mut p, d, group, group_port, PortAnchorSide::Top, 0.25);
manual(&mut p, d, na, a, PortAnchorSide::Bottom, 0.5);
manual(&mut p, d, nb, b, PortAnchorSide::Bottom, 0.5);
p = prepare_command(
&p,
&Command::SetEdgeRoute {
diagram: d,
route: EdgeRoute {
edge,
waypoints: vec![pos(500., 420.)],
label: None,
},
},
)
.unwrap()
.project;
let view = project_diagram(&p, &p.diagrams[&d]);
let parent_body = view.nodes.iter().find(|n| n.id == group).unwrap();
assert!(parent_body.size.width > p.diagrams[&d].nodes[&group].size.width);
assert_eq!(
port(&view, group_port).position,
port_anchor_position(parent_body, PortAnchorSide::Top, 0.25)
);
let routes = route_diagram(&p, &p.diagrams[&d], &view);
assert!(
routes.edges[&edge].warnings.is_empty(),
"{:?}",
routes.edges[&edge]
);
let obstacle = view.nodes.iter().find(|n| n.id == obstacle_node).unwrap();
for pair in routes.edges[&edge].points.windows(2) {
let [a, b] = [pair[0], pair[1]];
let crosses = if a.x == b.x {
a.x > obstacle.position.x
&& a.x < obstacle.position.x + obstacle.size.width
&& a.y.min(b.y) < obstacle.position.y + obstacle.size.height
&& a.y.max(b.y) > obstacle.position.y
} else {
a.y > obstacle.position.y
&& a.y < obstacle.position.y + obstacle.size.height
&& a.x.min(b.x) < obstacle.position.x + obstacle.size.width
&& a.x.max(b.x) > obstacle.position.x
};
assert!(!crosses);
}
let mut session = Session::new(p.clone());
session
.execute(Command::MoveNode {
diagram: d,
node: group,
position: pos(190., 80.),
with_descendants: true,
})
.unwrap();
let moved = session.project();
let after = project_diagram(moved, &moved.diagrams[&d]);
for before in &view.ports {
assert_eq!(
port(&after, before.element).position,
pos(before.position.x + 140., before.position.y + 80.)
);
}
for (id, node) in &p.diagrams[&d].nodes {
assert_eq!(moved.diagrams[&d].nodes[id].port_anchors, node.port_anchors);
}
assert_eq!(moved.diagrams[&d].routes[0].waypoints, [pos(640., 500.)]);
assert!(
route_diagram(moved, &moved.diagrams[&d], &after).edges[&edge]
.warnings
.is_empty()
);
session.undo();
assert_eq!(session.project(), &p);
}
#[test]
fn narrow_peer_gap_uses_clear_side_instead_of_a_blocked_facing_stub() {
let (mut p, d, a, b, output, input, edge) = fixture();
p.diagrams
.get_mut(&d)
.unwrap()
.nodes
.get_mut(&b)
.unwrap()
.position = pos(100., 260.);
let view = project_diagram(&p, &p.diagrams[&d]);
let source = view.nodes.iter().find(|n| n.id == a).unwrap();
let target = view.nodes.iter().find(|n| n.id == b).unwrap();
assert!(target.position.y - source.position.y - source.size.height < PORT_STUB + CLEARANCE);
assert_eq!(port(&view, output).side, PortAnchorSide::Right);
assert_eq!(port(&view, input).side, PortAnchorSide::Left);
let routed = route_diagram(&p, &p.diagrams[&d], &view);
assert!(
routed.edges[&edge].warnings.is_empty(),
"{:?}",
routed.edges[&edge]
);
}
#[test]
fn many_neutral_ports_balance_capacity_without_a_giant_bottom_footer() {
let mut p = Project::empty();
let owner = p
.add_element(Kind::System, Layer::Sa, "Many interfaces", None)
.unwrap();
for i in 0..100 {
let id = Id::from_u128(i + 1);
let port = make_port(
&p,
id,
owner,
&format!("Interface {i}"),
PortContract::Component {
kind: ComponentPortKind::Standard,
orientation: ComponentPortOrientation::Unset,
},
)
.unwrap();
p.elements.insert(id, port);
}
let d = p.ensure_diagram(Layer::Sa);
let node = p.place(d, owner, pos(0., 0.));
let view = project_diagram(&p, &p.diagrams[&d]);
assert_eq!(view.ports.len(), 100);
assert!(view.nodes[0].size.width < 1000. && view.nodes[0].size.height < 1000.);
for side in [
PortAnchorSide::Left,
PortAnchorSide::Right,
PortAnchorSide::Top,
PortAnchorSide::Bottom,
] {
let mut offsets: Vec<_> = view
.ports
.iter()
.filter(|p| p.side == side)
.map(|p| port_anchor_offset(&view.nodes[0], side, p.position))
.collect();
assert_eq!(offsets.len(), 25);
offsets.sort_by(f32::total_cmp);
let length = if matches!(side, PortAnchorSide::Left | PortAnchorSide::Right) {
view.nodes[0].size.height
} else {
view.nodes[0].size.width
};
assert!(offsets.windows(2).all(|w| (w[1] - w[0]) * length >= 27.99));
}
let id = view
.ports
.iter()
.find(|p| p.side == PortAnchorSide::Left)
.unwrap()
.element;
manual(&mut p, d, node, id, PortAnchorSide::Top, 0.5);
let after = project_diagram(&p, &p.diagrams[&d]);
assert_eq!(after.nodes, view.nodes);
let placed = port(&after, id);
for other in after.ports.iter().filter(|p| p.element != id) {
assert_eq!(other.side, port(&view, other.element).side);
let distance = (other.position.x - placed.position.x).abs()
+ (other.position.y - placed.position.y).abs();
assert!(
distance >= 14.,
"An automatic port overlaps the authored anchor"
);
}
}
const SOURCE: &str = r#"arcadia 3;
project ports "Ports" {
sa_function owner "Owner" {}
sa_port first "First" { parent @owner; contract { FunctionOutput { outgoing_items []; }; }; }
sa_port second "Second" { parent @owner; contract { FunctionOutput { outgoing_items []; }; }; }
diagram view "View" { layer Sa; }
node owner_view { diagram @view; element @owner; port_anchors [
// first rationale
{ port @first; side Top; offset 0.25; },
// second rationale
{ port @second; side Bottom; offset 0.75; }
]; }
}
"#;
fn find(p: &Project, alias: &str) -> Id {
p.identities()
.into_iter()
.find(|(_, a)| *a == alias)
.unwrap()
.0
}
#[test]
fn source_json_roundtrip_defaults_and_admission_include_anchor_records() {
let p = language::parse(SOURCE).unwrap();
let formatted = language::format(&p);
assert!(formatted.contains("port @first;") && formatted.contains("side Top;"));
assert_eq!(language::parse(&formatted).unwrap(), p);
assert_eq!(
persistence::decode(&serde_json::to_vec(&p).unwrap()).unwrap(),
p
);
assert_eq!(
compile_bundle(&format_bundle(&p), ProjectLimits::default())
.unwrap()
.decoded
.project,
p
);
// project + function + two ports + diagram + node + two anchors = 8.
let limits = language::ParseLimits {
max_records: 7,
..language::ParseLimits::UNLIMITED
};
assert!(limits.check_model(&p).is_err());
assert!(persistence::decode_limited(&serde_json::to_vec(&p).unwrap(), limits).is_err());
assert!(language::parse_limited(SOURCE, limits).is_err());
assert!(
compile_bundle(
&format_bundle(&p),
ProjectLimits {
model: limits,
..ProjectLimits::default()
}
)
.is_err()
);
let mut value = serde_json::to_value(&p).unwrap();
let d = find(&p, "view");
let n = find(&p, "owner_view");
value["diagrams"][d.to_string()]["nodes"][n.to_string()]
.as_object_mut()
.unwrap()
.remove("port_anchors");
let read = persistence::decode(&serde_json::to_vec(&value).unwrap()).unwrap();
assert!(read.diagrams[&d].nodes[&n].port_anchors.is_empty());
}
#[test]
fn source_updater_keeps_comment_attachment_through_identity_swap_and_anchor_reordering() {
let current = compile_bundle(
&SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([("project.arcadia".into(), SOURCE.replace('\n', "\r\n"))]),
},
ProjectLimits::default(),
)
.unwrap();
let p = &current.decoded.project;
let d = find(p, "view");
let n = find(p, "owner_view");
let first = find(p, "first");
let second = find(p, "second");
let temp = Id::new_v4();
let mut commands = vec![
Command::Reidentify(first, temp),
Command::Reidentify(second, first),
Command::Reidentify(temp, second),
Command::SetAlias(second, "revised_first".into()),
];
let candidate = prepare_command(p, &Command::Batch(commands.clone()))
.unwrap()
.project;
let mut node = candidate.diagrams[&d].nodes[&n].clone();
node.port_anchors.reverse();
node.port_anchors[1].offset = 0.5;
commands.push(Command::EditNode {
diagram: d,
original: n,
node,
});
let prepared = prepare_command(p, &Command::Batch(commands)).unwrap();
let result = update_bundle_with_identity_map(
&current,
&prepared.project,
&prepared.identity_map,
ProjectLimits::default(),
)
.unwrap();
let text = &result.bundle.files["project.arcadia"];
assert!(
text.contains("// first rationale\r\n { port @revised_first; side Top; offset 0.5; }"),
"{text}"
);
assert!(
text.contains("// second rationale\r\n { port @second; side Bottom; offset 0.75; }"),
"{text}"
);
assert_eq!(result.decoded.project, prepared.project);
assert_eq!(
update_bundle(&result, &prepared.project, ProjectLimits::default())
.unwrap()
.bundle,
result.bundle
);
}
#[test]
fn resetting_last_source_anchors_preserves_comments_and_can_add_a_new_manual_record() {
let current = compile_bundle(
&SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([("project.arcadia".into(), SOURCE.into())]),
},
ProjectLimits::default(),
)
.unwrap();
let p = &current.decoded.project;
let diagram = find(p, "view");
let node = find(p, "owner_view");
let first = find(p, "first");
let second = find(p, "second");
let prepared = prepare_command(
p,
&Command::Batch(vec![
Command::RemovePortAnchor {
diagram,
node,
port: first,
},
Command::RemovePortAnchor {
diagram,
node,
port: second,
},
]),
)
.unwrap();
let cleared = update_bundle_with_identity_map(
&current,
&prepared.project,
&prepared.identity_map,
ProjectLimits::default(),
)
.unwrap();
assert_eq!(cleared.decoded.project, prepared.project);
let text = &cleared.bundle.files["project.arcadia"];
assert!(text.contains("// first rationale") && text.contains("// second rationale"));
assert!(!text.contains("port_anchors"));
let prepared = prepare_command(
&cleared.decoded.project,
&Command::SetPortAnchor {
diagram,
node,
anchor: PortAnchor {
port: second,
side: PortAnchorSide::Right,
offset: 0.5,
},
},
)
.unwrap();
let added = update_bundle_with_identity_map(
&cleared,
&prepared.project,
&prepared.identity_map,
ProjectLimits::default(),
)
.unwrap();
assert_eq!(added.decoded.project, prepared.project);
assert_eq!(
added.bundle.files["project.arcadia"]
.matches("// second rationale")
.count(),
1
);
}
#[test]
fn malformed_source_anchor_records_have_located_errors() {
for bad in [
"{ port @first; side Top; }",
"{ port @first; side Top; offset -0.1; }",
"{ port @first; side Corner; offset 0.5; }",
"{ port @owner; side Top; offset 0.5; }",
"{ port @first; side Top; offset 0.5; unexpected true; }",
"{ port @first; side Top; offset 0.1; }, { port @first; side Right; offset 0.2; }",
] {
let start = SOURCE.find("port_anchors [").unwrap() + "port_anchors [".len();
let end = SOURCE[start..].find("]; }").unwrap() + start;
let source = format!("{}\n{}\n{}", &SOURCE[..start], bad, &SOURCE[end..]);
let errors = language::parse(&source).expect_err(bad);
assert!(errors.iter().any(|e| e.line >= 7), "{bad}: {errors:?}");
}
}
+13 -10
View File
@@ -811,16 +811,19 @@ fn projection_anchors_are_contract_aware_and_unoriented_connections_keep_their_i
let edge = view.edges.iter().find(|e| e.id == boundary).unwrap();
assert!(!edge.oriented);
assert_eq!(edge.source_port, Some(p.relations[&boundary].source()));
assert!(
view.ports
.iter()
.filter(|port| [
p.relations[&boundary].source(),
p.relations[&boundary].target()
]
.contains(&port.element))
.all(|port| port.side == PortAnchorSide::Bottom)
);
for (element, side) in [
(p.relations[&boundary].source(), PortAnchorSide::Right),
(p.relations[&boundary].target(), PortAnchorSide::Left),
] {
assert_eq!(
view.ports
.iter()
.find(|port| port.element == element)
.unwrap()
.side,
side
);
}
let (output, input) = endpoints(&p, fe);
assert_eq!(
view.ports
+5 -2
View File
@@ -357,9 +357,12 @@ fn workshop_automatic_labels_are_readable_without_mutating_authored_routes() {
size
}
),
"{} overlaps {}",
"{} overlaps {}: label {:?}, node {:?}, path {:?}",
label.text,
p.named(node.element)
p.named(node.element),
label.bounds,
node,
routed.edges[&edge.id].points
);
}
}