239 lines
7.0 KiB
Rust
239 lines
7.0 KiB
Rust
use oa_core::{projection, routing, *};
|
|
|
|
struct Fixture {
|
|
p: Project,
|
|
d: Id,
|
|
edge: Id,
|
|
source: Id,
|
|
target: Id,
|
|
blocker: Id,
|
|
}
|
|
fn fixture(ports: bool, gap: f32) -> Fixture {
|
|
let mut p = Project::empty();
|
|
let kind = if ports {
|
|
Kind::Function
|
|
} else {
|
|
Kind::DataType
|
|
};
|
|
let a = p.add_element(kind, Layer::Sa, "Source", None).unwrap();
|
|
let b = p.add_element(kind, Layer::Sa, "Target", None).unwrap();
|
|
let c = p.add_element(kind, Layer::Sa, "Blocker", None).unwrap();
|
|
for element in p.elements.values_mut() {
|
|
if let Some(SharedBody::DataType { definition, .. }) = element.shared_mut() {
|
|
*definition = DataTypeDefinition::Record { supertype: None };
|
|
}
|
|
}
|
|
let edge = p.relate(
|
|
if ports {
|
|
RelationKind::FunctionalExchange
|
|
} else {
|
|
RelationKind::DataAssociation
|
|
},
|
|
"Flow",
|
|
a,
|
|
b,
|
|
);
|
|
let d = p.ensure_diagram(Layer::Sa);
|
|
let source = p.place(d, a, Position { x: 0., y: 0. });
|
|
let target = p.place(
|
|
d,
|
|
b,
|
|
Position {
|
|
x: 500.,
|
|
y: 180. + gap,
|
|
},
|
|
);
|
|
let blocker = p.place(d, c, Position { x: 500., y: 20. });
|
|
for n in p.diagrams.get_mut(&d).unwrap().nodes.values_mut() {
|
|
n.size = Size {
|
|
width: 224.,
|
|
height: 160.,
|
|
};
|
|
}
|
|
if ports {
|
|
for (owner, end, side) in [
|
|
(source, EdgeEnd::Source, PortAnchorSide::Right),
|
|
(target, EdgeEnd::Target, PortAnchorSide::Top),
|
|
] {
|
|
let port = routing::semantic_port_endpoint(&p, edge, end).unwrap();
|
|
p.diagrams
|
|
.get_mut(&d)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&owner)
|
|
.unwrap()
|
|
.port_anchors
|
|
.push(PortAnchor {
|
|
port,
|
|
side,
|
|
offset: 0.5,
|
|
});
|
|
}
|
|
}
|
|
validate_candidate(&p).unwrap();
|
|
Fixture {
|
|
p,
|
|
d,
|
|
edge,
|
|
source,
|
|
target,
|
|
blocker,
|
|
}
|
|
}
|
|
fn report(f: &Fixture) -> Vec<routing::LeadIncompatibility> {
|
|
routing::lead_incompatibilities(
|
|
&f.p,
|
|
&f.p.diagrams[&f.d],
|
|
&projection::project_view(&f.p, f.d),
|
|
)
|
|
}
|
|
fn attachment_route(edge: Id) -> EdgeRoute {
|
|
EdgeRoute {
|
|
edge,
|
|
source_attachment: Some(EdgeAttachment {
|
|
side: PortAnchorSide::Top,
|
|
offset: 0.25,
|
|
}),
|
|
target_attachment: Some(EdgeAttachment {
|
|
side: PortAnchorSide::Bottom,
|
|
offset: 0.75,
|
|
}),
|
|
waypoints: vec![Position { x: 300., y: 400. }],
|
|
label: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn exact_twenty_gap_is_blocked_thirty_plus_clears_without_mutation() {
|
|
let f = fixture(true, 20.);
|
|
let original = f.p.clone();
|
|
let r = report(&f);
|
|
assert_eq!(r.len(), 1);
|
|
assert_eq!(
|
|
(
|
|
r[0].edge,
|
|
r[0].allowed_pair_count,
|
|
r[0].lead_clear_pair_count
|
|
),
|
|
(f.edge, 1, 0)
|
|
);
|
|
assert_eq!(r[0].source[0].owner, f.source);
|
|
let end = &r[0].target[0];
|
|
assert_eq!(end.owner, f.target);
|
|
assert_eq!(end.tip, Position { x: 612., y: 200. });
|
|
assert_eq!(end.stub, Position { x: 612., y: 182. });
|
|
assert_eq!(end.blockers.len(), 1);
|
|
assert_eq!(end.blockers[0].id, f.blocker);
|
|
assert_eq!(
|
|
end.blockers[0].bounds,
|
|
routing::RoutingBounds {
|
|
position: Position { x: 488., y: 8. },
|
|
size: Size {
|
|
width: 248.,
|
|
height: 184.
|
|
},
|
|
}
|
|
);
|
|
assert_eq!(end.stub_containers, end.blockers);
|
|
assert_eq!(f.p, original);
|
|
assert!(report(&fixture(true, 30.)).is_empty());
|
|
assert!(report(&fixture(true, 31.)).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn semantic_manual_ports_override_dormant_route_attachments_and_pins() {
|
|
let mut f = fixture(true, 20.);
|
|
let expected = report(&f);
|
|
f.p.diagrams
|
|
.get_mut(&f.d)
|
|
.unwrap()
|
|
.routes
|
|
.push(attachment_route(f.edge));
|
|
assert_eq!(report(&f), expected);
|
|
let view = projection::project_view(&f.p, f.d);
|
|
let routed = routing::route_diagram(&f.p, &f.p.diagrams[&f.d], &view);
|
|
assert_eq!(
|
|
routed.edges[&f.edge].points.last(),
|
|
Some(&expected[0].target[0].tip)
|
|
);
|
|
assert!(
|
|
routed.edges[&f.edge]
|
|
.warnings
|
|
.contains(&routing::RouteWarning::NoClearPath)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unconstrained_nonport_alternative_is_not_hard_blocked() {
|
|
let mut f = fixture(false, 20.);
|
|
assert!(report(&f).is_empty());
|
|
let mut route = attachment_route(f.edge);
|
|
route.source_attachment = Some(EdgeAttachment {
|
|
side: PortAnchorSide::Right,
|
|
offset: 0.5,
|
|
});
|
|
route.target_attachment = Some(EdgeAttachment {
|
|
side: PortAnchorSide::Top,
|
|
offset: 0.5,
|
|
});
|
|
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(route);
|
|
let r = report(&f);
|
|
assert_eq!(r.len(), 1);
|
|
assert_eq!(r[0].allowed_pair_count, 1);
|
|
assert_eq!(r[0].target[0].blockers[0].id, f.blocker);
|
|
}
|
|
|
|
#[test]
|
|
fn nested_endpoint_body_is_traversable_and_promoted_attachments_are_suspended() {
|
|
let mut f = fixture(false, 20.);
|
|
// DataType containment is not required for this routing-only view witness:
|
|
// construct the supplied projected forest explicitly, retaining real owners.
|
|
let mut view = projection::project_view(&f.p, f.d);
|
|
let parent = Id::new_v4();
|
|
let source = view.nodes.iter_mut().find(|n| n.id == f.source).unwrap();
|
|
source.visual_parent = Some(parent);
|
|
source.position = Position { x: 40., y: 100. };
|
|
let element = source.element;
|
|
view.nodes.push(projection::ProjectedNode {
|
|
id: parent,
|
|
element,
|
|
position: Position { x: 0., y: 0. },
|
|
size: Size {
|
|
width: 300.,
|
|
height: 300.,
|
|
},
|
|
visual_parent: None,
|
|
depth: 0,
|
|
collapsed: false,
|
|
allocation_summary: false,
|
|
});
|
|
let mut route = attachment_route(f.edge);
|
|
route.source_attachment = Some(EdgeAttachment {
|
|
side: PortAnchorSide::Right,
|
|
offset: 0.5,
|
|
});
|
|
route.target_attachment = Some(EdgeAttachment {
|
|
side: PortAnchorSide::Top,
|
|
offset: 0.5,
|
|
});
|
|
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(route);
|
|
let r = routing::lead_incompatibilities(&f.p, &f.p.diagrams[&f.d], &view);
|
|
assert_eq!(r.len(), 1);
|
|
assert!(
|
|
r[0].source[0].blockers.is_empty(),
|
|
"Ancestor body must not block source lead"
|
|
);
|
|
// Promotion changes actual projected owner: custom attachments no longer apply.
|
|
view.edges
|
|
.iter_mut()
|
|
.find(|e| e.id == f.edge)
|
|
.unwrap()
|
|
.source = parent;
|
|
let with_dormant = routing::lead_incompatibilities(&f.p, &f.p.diagrams[&f.d], &view);
|
|
f.p.diagrams.get_mut(&f.d).unwrap().routes.clear();
|
|
assert_eq!(
|
|
with_dormant,
|
|
routing::lead_incompatibilities(&f.p, &f.p.diagrams[&f.d], &view)
|
|
);
|
|
}
|