428 lines
15 KiB
Rust
428 lines
15 KiB
Rust
use oa_core::{diagram_edit::translate_placements, source_project::*, *};
|
|
use std::collections::BTreeSet;
|
|
|
|
fn point(x: f32, y: f32) -> Position {
|
|
Position { x, y }
|
|
}
|
|
fn route(edge: Id, x: f32, y: f32) -> 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![point(x, y)],
|
|
label: Some(EdgeLabel {
|
|
fraction: 0.4,
|
|
offset: point(3., -9.),
|
|
}),
|
|
}
|
|
}
|
|
struct Fixture {
|
|
project: Project,
|
|
diagram: Id,
|
|
a: Id,
|
|
b: Id,
|
|
child: Id,
|
|
foreign: Id,
|
|
}
|
|
fn fixture() -> Fixture {
|
|
let mut p = Project::empty();
|
|
let a = p.add_element(Kind::Activity, Layer::Oa, "A", None).unwrap();
|
|
let b = p.add_element(Kind::Activity, Layer::Oa, "B", None).unwrap();
|
|
let c = p.add_element(Kind::Activity, Layer::Oa, "C", None).unwrap();
|
|
let child = p
|
|
.add_element(Kind::Activity, Layer::Oa, "Child", Some(a))
|
|
.unwrap();
|
|
let internal = p.relate(RelationKind::Interaction, "Internal", a, b);
|
|
let external = p.relate(RelationKind::Interaction, "External", b, c);
|
|
let d = p.ensure_diagram(Layer::Oa);
|
|
let na = p.place(d, a, point(10., 20.));
|
|
let nb = p.place(d, b, point(400., 20.));
|
|
let nc = p.place(d, child, point(30., 80.));
|
|
p.place(d, c, point(800., 20.));
|
|
p.diagrams.get_mut(&d).unwrap().routes =
|
|
vec![route(internal, 320., 150.), route(external, 720., 170.)];
|
|
p.diagrams
|
|
.get_mut(&d)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&na)
|
|
.unwrap()
|
|
.collapsed = true;
|
|
let other = Id::new_v4();
|
|
let mut other_view = p.diagrams[&d].clone();
|
|
other_view.id = other;
|
|
other_view.alias = "other_view".into();
|
|
other_view.nodes.clear();
|
|
other_view.routes.clear();
|
|
p.diagrams.insert(other, other_view);
|
|
let foreign = p.place(other, a, point(-40., -50.));
|
|
validate_candidate(&p).unwrap();
|
|
Fixture {
|
|
project: p,
|
|
diagram: d,
|
|
a: na,
|
|
b: nb,
|
|
child: nc,
|
|
foreign,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn simultaneous_siblings_translate_internal_pins_once_and_preserve_complete_project() {
|
|
let f = fixture();
|
|
let before = f.project.clone();
|
|
let command = translate_placements(
|
|
&before,
|
|
f.diagram,
|
|
&BTreeSet::from([f.a, f.b]),
|
|
point(40., 60.),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
before, f.project,
|
|
"command construction must not mutate input"
|
|
);
|
|
// Independently enumerated expected edits, without the projection helpers.
|
|
let mut expected = before.clone();
|
|
let view = expected.diagrams.get_mut(&f.diagram).unwrap();
|
|
view.nodes.get_mut(&f.a).unwrap().position = point(50., 80.);
|
|
view.nodes.get_mut(&f.b).unwrap().position = point(440., 80.);
|
|
view.nodes.get_mut(&f.child).unwrap().position = point(70., 140.);
|
|
view.routes[0].waypoints = vec![point(360., 210.)];
|
|
let mut session = Session::new(before.clone());
|
|
session.execute(command.clone()).unwrap();
|
|
assert_eq!(session.project(), &expected);
|
|
session.undo();
|
|
assert_eq!(session.project(), &before);
|
|
assert!(
|
|
!session.can_undo(),
|
|
"one movement must be one history entry"
|
|
);
|
|
session.redo();
|
|
assert_eq!(session.project(), &expected);
|
|
assert!(!session.can_redo());
|
|
|
|
// Independent one-node moves cannot see the shared endpoint displacement.
|
|
let mut sequential = Session::new(before.clone());
|
|
for (node, position) in [(f.a, point(50., 80.)), (f.b, point(440., 80.))] {
|
|
sequential
|
|
.execute(Command::MoveNode {
|
|
diagram: f.diagram,
|
|
node,
|
|
position,
|
|
with_descendants: true,
|
|
})
|
|
.unwrap();
|
|
}
|
|
assert_eq!(
|
|
sequential.project().diagrams[&f.diagram].routes[0],
|
|
before.diagrams[&f.diagram].routes[0]
|
|
);
|
|
assert_ne!(sequential.project(), &expected);
|
|
|
|
let json = serde_json::to_string(&expected).unwrap();
|
|
assert_eq!(serde_json::from_str::<Project>(&json).unwrap(), expected);
|
|
let mut bundle = format_bundle(&before);
|
|
bundle
|
|
.files
|
|
.get_mut(&bundle.entry.clone())
|
|
.unwrap()
|
|
.insert_str(0, "// preserve movement rationale\n");
|
|
let original = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
|
|
let prepared = prepare_command(&before, &command).unwrap();
|
|
let updated = update_bundle_with_identity_map(
|
|
&original,
|
|
&prepared.project,
|
|
&prepared.identity_map,
|
|
ProjectLimits::default(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(updated.decoded.project, expected);
|
|
assert!(updated.bundle.files[&bundle.entry].starts_with("// preserve movement rationale\n"));
|
|
let reread = compile_bundle(&updated.bundle, ProjectLimits::default()).unwrap();
|
|
assert_eq!(reread.decoded.project, expected);
|
|
assert_eq!(reread.bundle, updated.bundle);
|
|
}
|
|
|
|
#[test]
|
|
fn selecting_folded_ancestor_and_hidden_descendant_moves_each_once() {
|
|
let f = fixture();
|
|
assert!(
|
|
!projection::project_view(&f.project, f.diagram)
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.id == f.child)
|
|
);
|
|
let ancestor = translate_placements(
|
|
&f.project,
|
|
f.diagram,
|
|
&BTreeSet::from([f.a]),
|
|
point(-15., 25.),
|
|
)
|
|
.unwrap();
|
|
let overlap = translate_placements(
|
|
&f.project,
|
|
f.diagram,
|
|
&BTreeSet::from([f.a, f.child]),
|
|
point(-15., 25.),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(overlap, ancestor);
|
|
let after = prepare_command(&f.project, &overlap).unwrap().project;
|
|
assert_eq!(
|
|
after.diagrams[&f.diagram].nodes[&f.child].position,
|
|
point(15., 105.)
|
|
);
|
|
assert_eq!(
|
|
after.diagrams[&f.diagram].routes,
|
|
f.project.diagrams[&f.diagram].routes
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_movements_refuse_atomically_and_zero_has_no_history() {
|
|
let f = fixture();
|
|
let selected = BTreeSet::from([f.a, f.b]);
|
|
for (diagram, ids, delta) in [
|
|
(Id::new_v4(), selected.clone(), point(1., 1.)),
|
|
(f.diagram, BTreeSet::new(), point(1., 1.)),
|
|
(
|
|
f.diagram,
|
|
BTreeSet::from([f.a, Id::new_v4()]),
|
|
point(1., 1.),
|
|
),
|
|
(f.diagram, BTreeSet::from([f.a, f.foreign]), point(0., 0.)),
|
|
(f.diagram, selected.clone(), point(f32::NAN, 1.)),
|
|
(f.diagram, selected.clone(), point(1., f32::INFINITY)),
|
|
(f.diagram, selected.clone(), point(MAX_COORDINATE, 0.)),
|
|
(f.diagram, selected.clone(), point(f32::MAX, f32::MAX)),
|
|
] {
|
|
let before = f.project.clone();
|
|
assert!(translate_placements(&f.project, diagram, &ids, delta).is_err());
|
|
assert_eq!(f.project, before);
|
|
}
|
|
let mut bad_route = f.project.clone();
|
|
bad_route.diagrams.get_mut(&f.diagram).unwrap().routes[0].waypoints =
|
|
vec![point(MAX_COORDINATE, 0.)];
|
|
validate_candidate(&bad_route).unwrap();
|
|
let snapshot = bad_route.clone();
|
|
assert!(translate_placements(&bad_route, f.diagram, &selected, point(1., 0.)).is_err());
|
|
assert_eq!(bad_route, snapshot);
|
|
let zero = translate_placements(&f.project, f.diagram, &selected, point(0., -0.)).unwrap();
|
|
assert_eq!(zero, Command::Batch(vec![]));
|
|
let mut session = Session::new(f.project.clone());
|
|
session.execute(zero).unwrap();
|
|
assert_eq!(session.project(), &f.project);
|
|
assert!(!session.can_undo());
|
|
assert_eq!(session.revision(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn typed_component_port_endpoints_follow_their_owner_placements() {
|
|
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, "Actor", None)
|
|
.unwrap();
|
|
let output = Id::new_v4();
|
|
let input = Id::new_v4();
|
|
for (id, owner, name, orientation) in [
|
|
(output, a, "Output", ComponentPortOrientation::Out),
|
|
(input, b, "Input", ComponentPortOrientation::In),
|
|
] {
|
|
let port = make_port(
|
|
&p,
|
|
id,
|
|
owner,
|
|
name,
|
|
PortContract::Component {
|
|
kind: ComponentPortKind::Flow,
|
|
orientation,
|
|
},
|
|
)
|
|
.unwrap();
|
|
p.elements.insert(id, port);
|
|
}
|
|
let edge = p.relate(RelationKind::BoundaryExchange, "Boundary", output, input);
|
|
let d = p.ensure_diagram(Layer::Sa);
|
|
let na = p.place(d, a, point(0., 0.));
|
|
let nb = p.place(d, b, point(400., 0.));
|
|
for (node, port, side, offset) in [
|
|
(na, output, PortAnchorSide::Bottom, 0.3),
|
|
(nb, input, PortAnchorSide::Top, 0.7),
|
|
] {
|
|
p.diagrams
|
|
.get_mut(&d)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&node)
|
|
.unwrap()
|
|
.port_anchors
|
|
.push(PortAnchor { port, side, offset });
|
|
}
|
|
p.diagrams
|
|
.get_mut(&d)
|
|
.unwrap()
|
|
.routes
|
|
.push(route(edge, 300., 100.));
|
|
validate_candidate(&p).unwrap();
|
|
let command = translate_placements(&p, d, &BTreeSet::from([na, nb]), point(10., -20.)).unwrap();
|
|
let after = prepare_command(&p, &command).unwrap().project;
|
|
let mut expected = p.clone();
|
|
let view = expected.diagrams.get_mut(&d).unwrap();
|
|
view.nodes.get_mut(&na).unwrap().position = point(10., -20.);
|
|
view.nodes.get_mut(&nb).unwrap().position = point(410., -20.);
|
|
view.routes[0].waypoints = vec![point(310., 80.)];
|
|
assert_eq!(after, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn allocation_movement_follows_visual_performers_and_same_performer_work_groups() {
|
|
let mut p = Project::empty();
|
|
let performer_a = p
|
|
.add_element(Kind::Entity, Layer::Oa, "Performer A", None)
|
|
.unwrap();
|
|
let performer_b = p
|
|
.add_element(Kind::Entity, Layer::Oa, "Performer B", None)
|
|
.unwrap();
|
|
let mixed = p
|
|
.add_element(Kind::Activity, Layer::Oa, "Mixed work", None)
|
|
.unwrap();
|
|
let branch = p
|
|
.add_element(Kind::Activity, Layer::Oa, "A work", Some(mixed))
|
|
.unwrap();
|
|
let leaf_a = p
|
|
.add_element(Kind::Activity, Layer::Oa, "A leaf", Some(branch))
|
|
.unwrap();
|
|
let leaf_b = p
|
|
.add_element(Kind::Activity, Layer::Oa, "B leaf", Some(mixed))
|
|
.unwrap();
|
|
p.relate(RelationKind::Allocation, "A performs", performer_a, leaf_a);
|
|
p.relate(RelationKind::Allocation, "B performs", performer_b, leaf_b);
|
|
let d = p.ensure_diagram(Layer::Oa);
|
|
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Allocation;
|
|
let na = p.place(d, performer_a, point(0., 0.));
|
|
p.place(d, performer_b, point(600., 0.));
|
|
let nm = p.place(d, mixed, point(1000., 0.));
|
|
let branch_node = p.place(d, branch, point(20., 80.));
|
|
let leaf_node = p.place(d, leaf_a, point(40., 160.));
|
|
p.place(d, leaf_b, point(620., 80.));
|
|
validate_candidate(&p).unwrap();
|
|
|
|
// Mixed work retains semantic ownership of both branches, but its leaves
|
|
// have different performers. A work is visually nested under Performer A,
|
|
// with its own same-performer leaf nested inside it.
|
|
let command =
|
|
translate_placements(&p, d, &BTreeSet::from([na, branch_node]), point(15., -25.)).unwrap();
|
|
let after = prepare_command(&p, &command).unwrap().project;
|
|
let mut expected = p.clone();
|
|
let view = expected.diagrams.get_mut(&d).unwrap();
|
|
view.nodes.get_mut(&na).unwrap().position = point(15., -25.);
|
|
view.nodes.get_mut(&branch_node).unwrap().position = point(35., 55.);
|
|
view.nodes.get_mut(&leaf_node).unwrap().position = point(55., 135.);
|
|
assert_eq!(after, expected);
|
|
|
|
// Moving the semantic parent does not move children displayed elsewhere.
|
|
let command = translate_placements(&p, d, &BTreeSet::from([nm]), point(15., -25.)).unwrap();
|
|
let after = prepare_command(&p, &command).unwrap().project;
|
|
let mut expected = p.clone();
|
|
expected
|
|
.diagrams
|
|
.get_mut(&d)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&nm)
|
|
.unwrap()
|
|
.position = point(1015., -25.);
|
|
assert_eq!(after, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn traceability_movement_is_flat_even_for_a_semantic_parent() {
|
|
let mut f = fixture();
|
|
f.project.diagrams.get_mut(&f.diagram).unwrap().projection = ProjectionKind::Traceability;
|
|
validate_candidate(&f.project).unwrap();
|
|
let command = translate_placements(
|
|
&f.project,
|
|
f.diagram,
|
|
&BTreeSet::from([f.a]),
|
|
point(40., 60.),
|
|
)
|
|
.unwrap();
|
|
let after = prepare_command(&f.project, &command).unwrap().project;
|
|
let mut expected = f.project.clone();
|
|
expected
|
|
.diagrams
|
|
.get_mut(&f.diagram)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&f.a)
|
|
.unwrap()
|
|
.position = point(50., 80.);
|
|
assert_eq!(
|
|
after, expected,
|
|
"flat view movement must preserve semantic descendants, routes, other views and all semantics"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn state_entry_pins_follow_the_boundary_only_when_it_also_moves() {
|
|
let mut p = Project::empty();
|
|
let machine = p
|
|
.add_element(Kind::StateMachine, Layer::Oa, "Machine", None)
|
|
.unwrap();
|
|
let region = p
|
|
.add_element(Kind::Region, Layer::Oa, "Region", Some(machine))
|
|
.unwrap();
|
|
let composite = p
|
|
.add_element(Kind::State, Layer::Oa, "Composite", Some(region))
|
|
.unwrap();
|
|
let inner = p
|
|
.add_element(Kind::Region, Layer::Oa, "Inner", Some(composite))
|
|
.unwrap();
|
|
let entry = p
|
|
.add_element(Kind::EntryPoint, Layer::Oa, "Entry", Some(inner))
|
|
.unwrap();
|
|
let leaf = p
|
|
.add_element(Kind::State, Layer::Oa, "Leaf", Some(inner))
|
|
.unwrap();
|
|
let edge = p.relate(RelationKind::StateTransition, "Enter", entry, leaf);
|
|
if let RelationBody::StateTransition {
|
|
transition_kind, ..
|
|
} = &mut p.relations.get_mut(&edge).unwrap().body
|
|
{
|
|
*transition_kind = TransitionKind::Local;
|
|
}
|
|
let d = p.ensure_diagram(Layer::Oa);
|
|
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::StateMachine { machine };
|
|
let boundary = p.place(d, composite, point(0., 0.));
|
|
let ne = p.place(d, entry, point(20., 80.));
|
|
let nl = p.place(d, leaf, point(100., 80.));
|
|
p.diagrams
|
|
.get_mut(&d)
|
|
.unwrap()
|
|
.routes
|
|
.push(route(edge, 90., 100.));
|
|
validate_candidate(&p).unwrap();
|
|
let endpoints =
|
|
translate_placements(&p, d, &BTreeSet::from([ne, nl]), point(10., 20.)).unwrap();
|
|
let after = prepare_command(&p, &endpoints).unwrap().project;
|
|
assert_eq!(after.diagrams[&d].routes, p.diagrams[&d].routes);
|
|
let with_boundary =
|
|
translate_placements(&p, d, &BTreeSet::from([boundary, ne, nl]), point(10., 20.)).unwrap();
|
|
let after = prepare_command(&p, &with_boundary).unwrap().project;
|
|
assert_eq!(
|
|
after.diagrams[&d].routes[0].waypoints,
|
|
vec![point(100., 120.)]
|
|
);
|
|
assert_eq!(after.diagrams[&d].nodes[&ne].position, point(30., 100.));
|
|
}
|