273 lines
8.2 KiB
Rust
273 lines
8.2 KiB
Rust
use oa_core::{language, projection::*, routing::*, *};
|
|
|
|
fn pos(x: f32, y: f32) -> Position {
|
|
Position { x, y }
|
|
}
|
|
|
|
fn id(project: &Project, alias: &str) -> Id {
|
|
project
|
|
.identities()
|
|
.into_iter()
|
|
.find(|(_, a)| *a == alias)
|
|
.unwrap()
|
|
.0
|
|
}
|
|
|
|
fn authored(edge: Id) -> EdgeRoute {
|
|
EdgeRoute {
|
|
edge,
|
|
source_attachment: Some(EdgeAttachment {
|
|
side: PortAnchorSide::Bottom,
|
|
offset: 0.2,
|
|
}),
|
|
target_attachment: Some(EdgeAttachment {
|
|
side: PortAnchorSide::Top,
|
|
offset: 0.8,
|
|
}),
|
|
waypoints: vec![pos(520., 420.)],
|
|
label: Some(EdgeLabel {
|
|
fraction: 0.35,
|
|
offset: pos(17., -23.),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn routed(project: &Project, diagram: Id) -> RoutedView {
|
|
let d = &project.diagrams[&diagram];
|
|
route_diagram(project, d, &project_diagram(project, d))
|
|
}
|
|
|
|
fn fixture() -> (Project, Id, Id, Id, Id, Id, Id) {
|
|
let mut p = language::parse(
|
|
r#"arcadia 7;
|
|
project lifecycle "Attachment lifecycle" {
|
|
oa_activity group "Group" { oa_activity first "First" {} }
|
|
oa_activity second "Second" {}
|
|
oa_activity replacement "Replacement" {}
|
|
interaction signal "Signal" { source @first; target @second; }
|
|
diagram view "View" {}
|
|
diagram other "Other" {}
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
let (d, other, group, first, second, replacement, edge) = (
|
|
id(&p, "view"),
|
|
id(&p, "other"),
|
|
id(&p, "group"),
|
|
id(&p, "first"),
|
|
id(&p, "second"),
|
|
id(&p, "replacement"),
|
|
id(&p, "signal"),
|
|
);
|
|
for diagram in [d, other] {
|
|
p.place(diagram, group, pos(20., 20.));
|
|
p.place(diagram, first, pos(100., 130.));
|
|
p.place(diagram, second, pos(750., 150.));
|
|
p.place(diagram, replacement, pos(750., 570.));
|
|
p.diagrams
|
|
.get_mut(&diagram)
|
|
.unwrap()
|
|
.routes
|
|
.push(authored(edge));
|
|
}
|
|
(p, d, other, group, first, replacement, edge)
|
|
}
|
|
|
|
#[test]
|
|
fn attachment_fold_promotion_suspends_the_complete_route_and_expand_restores_it() {
|
|
let (mut p, d, other, group, _, _, edge) = fixture();
|
|
assert_eq!(semantic_port_endpoint(&p, edge, EdgeEnd::Source), None);
|
|
assert_eq!(semantic_port_endpoint(&p, edge, EdgeEnd::Target), None);
|
|
let before = p.clone();
|
|
let expanded = routed(&p, d);
|
|
let other_geometry = routed(&p, other);
|
|
let parent = p.diagrams[&d]
|
|
.nodes
|
|
.values()
|
|
.find(|n| n.element == group)
|
|
.unwrap()
|
|
.id;
|
|
p.diagrams
|
|
.get_mut(&d)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&parent)
|
|
.unwrap()
|
|
.collapsed = true;
|
|
let folded = routed(&p, d);
|
|
assert!(folded.edges[&edge].custom_suspended);
|
|
assert_eq!(folded.edges[&edge].pins, []);
|
|
assert!(
|
|
folded.edges[&edge]
|
|
.warnings
|
|
.contains(&RouteWarning::PromotedEndpoints)
|
|
);
|
|
assert_eq!(p.diagrams[&d].routes, before.diagrams[&d].routes);
|
|
let mut automatic = p.clone();
|
|
automatic.diagrams.get_mut(&d).unwrap().routes.clear();
|
|
assert_eq!(
|
|
folded,
|
|
routed(&automatic, d),
|
|
"attachments, pins and label must all be dormant on promotion"
|
|
);
|
|
assert_eq!(p.diagrams[&other], before.diagrams[&other]);
|
|
assert_eq!(routed(&p, other), other_geometry);
|
|
p.diagrams
|
|
.get_mut(&d)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&parent)
|
|
.unwrap()
|
|
.collapsed = false;
|
|
assert_eq!(p, before);
|
|
assert_eq!(routed(&p, d), expanded);
|
|
}
|
|
|
|
#[test]
|
|
fn attachment_reconnect_retains_slot_preferences_and_other_view_records_with_exact_history() {
|
|
let (p, d, other, _, _, replacement, edge) = fixture();
|
|
let before = routed(&p, d);
|
|
let mut relation = p.relations[&edge].clone();
|
|
let RelationBody::Interaction { target, .. } = &mut relation.body else {
|
|
unreachable!()
|
|
};
|
|
*target = replacement;
|
|
let mut expected = p.clone();
|
|
expected.relations.insert(edge, relation.clone());
|
|
let mut session = Session::new(p.clone());
|
|
session
|
|
.execute(Command::EditRelation(edge, relation))
|
|
.unwrap();
|
|
assert_eq!(
|
|
session.project(),
|
|
&expected,
|
|
"reconnect changes only the semantic endpoint; all per-view records remain exact"
|
|
);
|
|
assert_eq!(session.project().diagrams[&other], p.diagrams[&other]);
|
|
for diagram in [d, other] {
|
|
let current = session.project();
|
|
let view = project_diagram(current, ¤t.diagrams[&diagram]);
|
|
let node = view
|
|
.nodes
|
|
.iter()
|
|
.find(|n| n.element == replacement)
|
|
.unwrap();
|
|
let preference = current.diagrams[&diagram].routes[0]
|
|
.target_attachment
|
|
.unwrap();
|
|
let expected_tip =
|
|
attachment_point(current, ¤t.diagrams[&diagram], node, &preference);
|
|
let result = routed(current, diagram);
|
|
assert_eq!(result.edges[&edge].points.last(), Some(&expected_tip));
|
|
assert_eq!(
|
|
result.edges[&edge].pins,
|
|
p.diagrams[&diagram].routes[0].waypoints
|
|
);
|
|
}
|
|
assert_ne!(
|
|
routed(session.project(), d).edges[&edge].points.last(),
|
|
before.edges[&edge].points.last()
|
|
);
|
|
session.undo();
|
|
assert_eq!(session.project(), &p);
|
|
assert_eq!(routed(session.project(), d), before);
|
|
session.redo();
|
|
assert_eq!(session.project(), &expected);
|
|
}
|
|
|
|
#[test]
|
|
fn attachment_direct_port_endpoints_remain_authoritative_when_their_glyphs_are_filtered() {
|
|
let mut p = Project::empty();
|
|
let system = p
|
|
.add_element(Kind::System, Layer::Sa, "System", None)
|
|
.unwrap();
|
|
let actor = p
|
|
.add_element(Kind::SystemActor, Layer::Sa, "Actor", None)
|
|
.unwrap();
|
|
let function = p
|
|
.add_element(Kind::Function, Layer::Sa, "Send", None)
|
|
.unwrap();
|
|
let mut ports = Vec::new();
|
|
for (owner, name, contract) in [
|
|
(
|
|
system,
|
|
"Output",
|
|
PortContract::Component {
|
|
kind: ComponentPortKind::Flow,
|
|
orientation: ComponentPortOrientation::Out,
|
|
},
|
|
),
|
|
(
|
|
actor,
|
|
"Input",
|
|
PortContract::Component {
|
|
kind: ComponentPortKind::Flow,
|
|
orientation: ComponentPortOrientation::In,
|
|
},
|
|
),
|
|
(
|
|
function,
|
|
"Function output",
|
|
PortContract::FunctionOutput {
|
|
outgoing_items: vec![],
|
|
},
|
|
),
|
|
] {
|
|
let port = Id::new_v4();
|
|
p.elements
|
|
.insert(port, make_port(&p, port, owner, name, contract).unwrap());
|
|
ports.push(port);
|
|
}
|
|
p.relate(
|
|
RelationKind::Allocation,
|
|
"Function allocation",
|
|
system,
|
|
function,
|
|
);
|
|
let boundary = p.relate(
|
|
RelationKind::BoundaryExchange,
|
|
"Boundary",
|
|
ports[0],
|
|
ports[1],
|
|
);
|
|
let allocation = p.relate(
|
|
RelationKind::PortAllocation,
|
|
"Port allocation",
|
|
ports[0],
|
|
ports[2],
|
|
);
|
|
let d = p.ensure_diagram(Layer::Sa);
|
|
p.place(d, system, pos(0., 0.));
|
|
p.place(d, actor, pos(700., 0.));
|
|
p.place(d, function, pos(0., 450.));
|
|
let original = p.clone();
|
|
let baseline = routed(&p, d);
|
|
for (edge, target) in [(boundary, ports[1]), (allocation, ports[2])] {
|
|
assert!(baseline.edges.contains_key(&edge));
|
|
assert_eq!(
|
|
semantic_port_endpoint(&p, edge, EdgeEnd::Source),
|
|
Some(ports[0])
|
|
);
|
|
assert_eq!(
|
|
semantic_port_endpoint(&p, edge, EdgeEnd::Target),
|
|
Some(target)
|
|
);
|
|
let mut route = authored(edge);
|
|
route.waypoints.clear();
|
|
route.label = None;
|
|
p.diagrams.get_mut(&d).unwrap().routes.push(route);
|
|
}
|
|
assert_eq!(routed(&p, d), baseline);
|
|
let mut hidden = project_diagram(&p, &p.diagrams[&d]);
|
|
hidden.ports.clear();
|
|
for edge in &mut hidden.edges {
|
|
edge.source_port = None;
|
|
edge.target_port = None;
|
|
}
|
|
assert_eq!(
|
|
route_diagram(&p, &p.diagrams[&d], &hidden),
|
|
route_diagram(&original, &original.diagrams[&d], &hidden),
|
|
"filtered direct Port endpoints must not activate their stored preferences"
|
|
);
|
|
}
|