Preserve canvas context across property edits and contextual drawing
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
//! Populated presentation survives mixed-layer projection switches.
|
||||
//! Complements projection_switching.rs without external fixture dependencies.
|
||||
use oa_core::{
|
||||
projection::*, routing::route_diagram, source_project::*, view_options::ViewIndex, *,
|
||||
};
|
||||
|
||||
struct Populated {
|
||||
project: Project,
|
||||
diagram: Id,
|
||||
groups: [Id; 2],
|
||||
children: [[Id; 2]; 2],
|
||||
edges: [Id; 2],
|
||||
ports: [Id; 2],
|
||||
trace: Id,
|
||||
}
|
||||
|
||||
fn xy(x: f32, y: f32) -> Position {
|
||||
Position { x, y }
|
||||
}
|
||||
|
||||
fn apply(project: &mut Project, command: Command) {
|
||||
*project = prepare_command(project, &command).unwrap().project;
|
||||
}
|
||||
|
||||
fn fixture(layer: Layer, collapsed: bool) -> Populated {
|
||||
let mut p = Project::empty();
|
||||
let oa_group = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Operational response", None)
|
||||
.unwrap();
|
||||
let oa_send = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Request repair", Some(oa_group))
|
||||
.unwrap();
|
||||
let oa_receive = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Perform repair", Some(oa_group))
|
||||
.unwrap();
|
||||
let sa_group = p
|
||||
.add_element(Kind::Function, Layer::Sa, "System response", None)
|
||||
.unwrap();
|
||||
let sa_send = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Dispatch repair", Some(sa_group))
|
||||
.unwrap();
|
||||
let sa_receive = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Execute repair", Some(sa_group))
|
||||
.unwrap();
|
||||
let oa_edge = p.relate(
|
||||
RelationKind::Interaction,
|
||||
"Repair request",
|
||||
oa_send,
|
||||
oa_receive,
|
||||
);
|
||||
let sa_edge = p.relate(
|
||||
RelationKind::FunctionalExchange,
|
||||
"Repair dispatch",
|
||||
sa_send,
|
||||
sa_receive,
|
||||
);
|
||||
let RelationBody::FunctionalExchange {
|
||||
source_port,
|
||||
target_port,
|
||||
..
|
||||
} = p.relations[&sa_edge].body
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let trace = Id::new_v4();
|
||||
let refinement = Trace {
|
||||
id: trace,
|
||||
alias: p.fresh_alias("dispatch_refinement", "trace", trace),
|
||||
name: "Dispatch refines repair request".into(),
|
||||
description: "Keep this cross-layer trace and its authored route.".into(),
|
||||
oa: oa_send,
|
||||
sa: sa_send,
|
||||
kind: TraceKind::Refinement,
|
||||
baseline: Some(TraceBaseline {
|
||||
oa: p.properties(oa_send).unwrap(),
|
||||
sa: p.properties(sa_send).unwrap(),
|
||||
semantic: None,
|
||||
}),
|
||||
};
|
||||
apply(&mut p, Command::CreateTrace(refinement));
|
||||
let d = p.ensure_diagram(layer);
|
||||
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Traceability;
|
||||
let oa_group_node = p.place(d, oa_group, xy(0., 0.));
|
||||
let oa_send_node = p.place(d, oa_send, xy(40., 120.));
|
||||
let oa_receive_node = p.place(d, oa_receive, xy(360., 120.));
|
||||
let sa_group_node = p.place(d, sa_group, xy(0., 500.));
|
||||
let sa_send_node = p.place(d, sa_send, xy(40., 620.));
|
||||
let sa_receive_node = p.place(d, sa_receive, xy(360., 620.));
|
||||
for (node, port, side, offset) in [
|
||||
(sa_send_node, source_port, PortAnchorSide::Bottom, 0.25),
|
||||
(sa_receive_node, target_port, PortAnchorSide::Top, 0.75),
|
||||
] {
|
||||
apply(
|
||||
&mut p,
|
||||
Command::SetPortAnchor {
|
||||
diagram: d,
|
||||
node,
|
||||
anchor: PortAnchor { port, side, offset },
|
||||
},
|
||||
);
|
||||
}
|
||||
for (edge, y, port_backed) in [
|
||||
(oa_edge, 260., false),
|
||||
(sa_edge, 860., true),
|
||||
(trace, 420., false),
|
||||
] {
|
||||
apply(
|
||||
&mut p,
|
||||
Command::SetEdgeRoute {
|
||||
diagram: d,
|
||||
route: EdgeRoute {
|
||||
edge,
|
||||
source_attachment: (!port_backed).then_some(EdgeAttachment {
|
||||
side: PortAnchorSide::Right,
|
||||
offset: 0.3,
|
||||
}),
|
||||
target_attachment: (!port_backed).then_some(EdgeAttachment {
|
||||
side: PortAnchorSide::Left,
|
||||
offset: 0.7,
|
||||
}),
|
||||
waypoints: vec![xy(680., y), xy(740., y)],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.4,
|
||||
offset: xy(13., -27.),
|
||||
}),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
for node in [oa_group_node, sa_group_node] {
|
||||
let mut placement = p.diagrams[&d].nodes[&node].clone();
|
||||
placement.collapsed = collapsed;
|
||||
placement.size = Size {
|
||||
width: 310.,
|
||||
height: 170.,
|
||||
};
|
||||
apply(
|
||||
&mut p,
|
||||
Command::EditNode {
|
||||
diagram: d,
|
||||
original: node,
|
||||
node: placement,
|
||||
},
|
||||
);
|
||||
}
|
||||
validate_candidate(&p).unwrap();
|
||||
Populated {
|
||||
project: p,
|
||||
diagram: d,
|
||||
groups: [oa_group_node, sa_group_node],
|
||||
children: [
|
||||
[oa_send_node, oa_receive_node],
|
||||
[sa_send_node, sa_receive_node],
|
||||
],
|
||||
edges: [oa_edge, sa_edge],
|
||||
ports: [source_port, target_port],
|
||||
trace,
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_round_trips(p: &Project) {
|
||||
assert_eq!(language::parse(&language::format(p)).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
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_layer_only(p: &Project, d: Id, view: &ProjectedView) {
|
||||
let layer = p.diagrams[&d].layer;
|
||||
for node in &view.nodes {
|
||||
assert_eq!(p.elements[&node.element].layer(), layer);
|
||||
if let Some(parent) = node.visual_parent {
|
||||
let parent = view.nodes.iter().find(|n| n.id == parent).unwrap();
|
||||
assert_eq!(p.elements[&parent.element].layer(), layer);
|
||||
}
|
||||
}
|
||||
for port in &view.ports {
|
||||
assert_eq!(p.elements[&port.element].layer(), layer);
|
||||
let owner = view.nodes.iter().find(|n| n.id == port.owner).unwrap();
|
||||
assert_eq!(p.elements[&owner.element].layer(), layer);
|
||||
}
|
||||
for edge in &view.edges {
|
||||
assert!(
|
||||
!edge.trace,
|
||||
"cross-layer trace leaked into ordinary projection"
|
||||
);
|
||||
assert_eq!(p.layer_of(edge.id), Some(layer));
|
||||
for endpoint in [edge.source, edge.target] {
|
||||
let owner = view.nodes.iter().find(|n| n.id == endpoint).unwrap();
|
||||
assert_eq!(p.elements[&owner.element].layer(), layer);
|
||||
}
|
||||
for port in [edge.source_port, edge.target_port].into_iter().flatten() {
|
||||
assert_eq!(p.elements[&port].layer(), layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn populated_mixed_projection_switch_conserves_authorship_and_restores_geometry() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for collapsed in [false, true] {
|
||||
let f = fixture(layer, collapsed);
|
||||
let baseline = &f.project;
|
||||
let d = f.diagram;
|
||||
let before_view = project_view(baseline, d);
|
||||
let before_routes = route_diagram(baseline, &baseline.diagrams[&d], &before_view);
|
||||
// Guard against a vacuous fixture: Traceability deliberately flattens parents.
|
||||
assert_eq!(before_view.nodes.len(), 6);
|
||||
assert!(before_view.nodes.iter().all(|n| n.visual_parent.is_none()));
|
||||
for id in [f.edges[0], f.edges[1], f.trace] {
|
||||
assert!(before_view.edges.iter().any(|e| e.id == id));
|
||||
let route = baseline.diagrams[&d]
|
||||
.routes
|
||||
.iter()
|
||||
.find(|r| r.edge == id)
|
||||
.unwrap();
|
||||
assert_eq!(route.waypoints.len(), 2);
|
||||
assert!(route.label.is_some());
|
||||
}
|
||||
for port in f.ports {
|
||||
assert!(before_view.ports.iter().any(|p| p.element == port));
|
||||
}
|
||||
assert_round_trips(baseline);
|
||||
for projection in [
|
||||
ProjectionKind::Overview,
|
||||
ProjectionKind::Decomposition,
|
||||
ProjectionKind::Allocation,
|
||||
ProjectionKind::FunctionalDataflow,
|
||||
ProjectionKind::Capabilities,
|
||||
] {
|
||||
let mut expected = baseline.clone();
|
||||
expected.diagrams.get_mut(&d).unwrap().projection = projection;
|
||||
let mut session = Session::new(baseline.clone());
|
||||
session
|
||||
.execute(Command::EditDiagram(d, expected.diagrams[&d].clone()))
|
||||
.unwrap();
|
||||
assert_eq!(session.project(), &expected);
|
||||
assert_round_trips(session.project());
|
||||
let view = project_view(session.project(), d);
|
||||
assert_eq!(
|
||||
view,
|
||||
project_view_with_index(&expected, d, &ViewIndex::new(&expected))
|
||||
);
|
||||
assert_layer_only(session.project(), d, &view);
|
||||
if projection == ProjectionKind::Overview {
|
||||
let own = if layer == Layer::Oa { 0 } else { 1 };
|
||||
assert!(view.nodes.iter().any(|n| n.id == f.groups[own]));
|
||||
for child in f.children[own] {
|
||||
if collapsed {
|
||||
assert!(!view.nodes.iter().any(|n| n.id == child));
|
||||
} else {
|
||||
assert_eq!(
|
||||
view.nodes
|
||||
.iter()
|
||||
.find(|n| n.id == child)
|
||||
.unwrap()
|
||||
.visual_parent,
|
||||
Some(f.groups[own])
|
||||
);
|
||||
}
|
||||
}
|
||||
if !collapsed {
|
||||
assert!(view.edges.iter().any(|e| e.id == f.edges[own]));
|
||||
if layer == Layer::Sa {
|
||||
assert_eq!(view.ports.len(), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
session.undo();
|
||||
assert_eq!(session.project(), baseline);
|
||||
assert!(
|
||||
!session.can_undo(),
|
||||
"projection change must be one model history step"
|
||||
);
|
||||
assert_round_trips(session.project());
|
||||
session.redo();
|
||||
assert_eq!(session.project(), &expected);
|
||||
session
|
||||
.execute(Command::EditDiagram(d, baseline.diagrams[&d].clone()))
|
||||
.unwrap();
|
||||
assert_eq!(session.project(), baseline);
|
||||
let restored = project_view(session.project(), d);
|
||||
assert_eq!(restored, before_view);
|
||||
assert_eq!(
|
||||
route_diagram(
|
||||
session.project(),
|
||||
&session.project().diagrams[&d],
|
||||
&restored
|
||||
),
|
||||
before_routes
|
||||
);
|
||||
assert_round_trips(session.project());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user