Prepare complete-scene diagram readability fixtures and probe
This commit is contained in:
@@ -0,0 +1,548 @@
|
||||
//! Experimental geometry evidence, not a native selection or usability oracle.
|
||||
//! See harness/experiments/diagram-readability-20260929/README.md.
|
||||
use oa_core::{Diagram, Id, Position, Project, persistence, projection, routing, validation};
|
||||
use serde_json::{Value, json};
|
||||
use std::{
|
||||
error::Error,
|
||||
fs::{self, OpenOptions},
|
||||
io::Write,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
const MIXED: &[u8] = include_bytes!(
|
||||
"../../../harness/experiments/diagram-readability-20260929/mixed-dispatch-original.json"
|
||||
);
|
||||
const WARNINGS: &[u8] = include_bytes!(
|
||||
"../../../harness/experiments/diagram-readability-20260929/mixed-dispatch-expected-diagnostics.json"
|
||||
);
|
||||
const RELEASE: &[u8] = include_bytes!("../../../examples/release1/model.json");
|
||||
const RELEASE_WARNINGS: &[u8] =
|
||||
include_bytes!("../../../examples/release1/expected-diagnostics.json");
|
||||
const EIGHT_VIEW: &[u8] = include_bytes!(
|
||||
"../../../harness/experiments/diagram-readability-20260929/eight-card-view.json"
|
||||
);
|
||||
const MAIN: &str = "00000000-0000-0000-0000-0000000186a1";
|
||||
const EIGHT: &str = "00000000-0038-4000-8000-000000000001";
|
||||
|
||||
fn canonical_diagnostics(value: Value) -> Result<Vec<String>> {
|
||||
let mut rows = value
|
||||
.as_array()
|
||||
.ok_or("Expected a diagnostics array")?
|
||||
.iter()
|
||||
.map(serde_json::to_string)
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
rows.sort();
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
fn admit(bytes: &[u8], baseline: &[u8]) -> Result<Project> {
|
||||
let project = persistence::decode(bytes)?;
|
||||
let issues = validation::validate(&project);
|
||||
if issues
|
||||
.iter()
|
||||
.any(|i| i.severity == validation::Severity::Error)
|
||||
{
|
||||
return Err(format!("Invalid project: {}", serde_json::to_string(&issues)?).into());
|
||||
}
|
||||
if canonical_diagnostics(serde_json::to_value(&issues)?)?
|
||||
!= canonical_diagnostics(serde_json::from_slice(baseline)?)?
|
||||
{
|
||||
return Err(format!(
|
||||
"Unexpected diagnostics; review without automatically accepting: {}",
|
||||
serde_json::to_string(&issues)?
|
||||
)
|
||||
.into());
|
||||
}
|
||||
Ok(project)
|
||||
}
|
||||
|
||||
fn representative() -> Result<Project> {
|
||||
let mut project = admit(RELEASE, RELEASE_WARNINGS)?;
|
||||
let view: Diagram = serde_json::from_slice(EIGHT_VIEW)?;
|
||||
if project.diagrams.insert(view.id, view).is_some() {
|
||||
return Err("Representative view ID collision".into());
|
||||
}
|
||||
admit(&serde_json::to_vec(&project)?, RELEASE_WARNINGS)
|
||||
}
|
||||
|
||||
fn rect(point: Position, origin: Position, size: oa_core::Size) -> bool {
|
||||
point.x >= origin.x
|
||||
&& point.x <= origin.x + size.width
|
||||
&& point.y >= origin.y
|
||||
&& point.y <= origin.y + size.height
|
||||
}
|
||||
|
||||
fn distance(point: Position, a: Position, b: Position) -> f32 {
|
||||
let dx = b.x - a.x;
|
||||
let dy = b.y - a.y;
|
||||
let length = dx * dx + dy * dy;
|
||||
let t = if length == 0. {
|
||||
0.
|
||||
} else {
|
||||
(((point.x - a.x) * dx + (point.y - a.y) * dy) / length).clamp(0., 1.)
|
||||
};
|
||||
(point.x - a.x - t * dx).hypot(point.y - a.y - t * dy)
|
||||
}
|
||||
|
||||
// Intentionally returns every distinct candidate, with no selected-edge bias,
|
||||
// UUID winner, click dispatch, viewport clipping, or gesture simulation.
|
||||
fn candidates(
|
||||
project: &Project,
|
||||
view: &projection::ProjectedView,
|
||||
routed: &routing::RoutedView,
|
||||
point: Position,
|
||||
zoom: f32,
|
||||
) -> Value {
|
||||
let edges: Vec<_> = routed
|
||||
.edges
|
||||
.values()
|
||||
.filter_map(|edge| {
|
||||
let segments: Vec<_> = edge
|
||||
.points
|
||||
.windows(2)
|
||||
.enumerate()
|
||||
.filter_map(|(i, pair)| {
|
||||
let d = distance(point, pair[0], pair[1]);
|
||||
(d <= 7. / zoom).then(|| json!({"index":i,"distance":d}))
|
||||
})
|
||||
.collect();
|
||||
(!segments.is_empty()).then(|| json!({"edge":edge.id,"segments":segments}))
|
||||
})
|
||||
.collect();
|
||||
let nodes: Vec<_> = view
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|n| rect(point, n.position, n.size))
|
||||
.map(|n| n.id)
|
||||
.collect();
|
||||
let labels: Vec<_> = routed
|
||||
.edges
|
||||
.values()
|
||||
.filter(|e| {
|
||||
!e.label.text.is_empty() && rect(point, e.label.bounds.position, e.label.bounds.size)
|
||||
})
|
||||
.map(|e| e.id)
|
||||
.collect();
|
||||
let association_labels: Vec<_> = routed
|
||||
.edges
|
||||
.values()
|
||||
.flat_map(|e| {
|
||||
project
|
||||
.relations
|
||||
.get(&e.id)
|
||||
.into_iter()
|
||||
.flat_map(move |relation| {
|
||||
routing::association_end_geometry(relation, e)
|
||||
.into_iter()
|
||||
.filter(move |end| {
|
||||
rect(point, end.label.bounds.position, end.label.bounds.size)
|
||||
})
|
||||
.map(move |end| json!({"edge":e.id,"source_end":end.source_end}))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let radius = (10. / zoom).max(10.);
|
||||
let ports: Vec<_> = view
|
||||
.ports
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
(p.position.x - point.x).abs() <= radius && (p.position.y - point.y).abs() <= radius
|
||||
})
|
||||
.map(|p| json!({"element":p.element,"owner":p.owner}))
|
||||
.collect();
|
||||
json!({"world_point":point,"zoom":zoom,"edge_candidates":edges,
|
||||
"node_occluders":nodes,"label_occluders":labels,"association_label_occluders":association_labels,"port_occluders":ports})
|
||||
}
|
||||
|
||||
fn unobscured(value: &Value) -> bool {
|
||||
[
|
||||
"node_occluders",
|
||||
"label_occluders",
|
||||
"association_label_occluders",
|
||||
"port_occluders",
|
||||
]
|
||||
.iter()
|
||||
.all(|key| value[key].as_array().is_some_and(Vec::is_empty))
|
||||
}
|
||||
|
||||
fn snapshot(project: &Project, diagram: Id, point: Position, zoom: f32) -> Result<Value> {
|
||||
if !zoom.is_finite() || zoom <= 0. || !point.x.is_finite() || !point.y.is_finite() {
|
||||
return Err("Point must be finite; zoom must be finite and positive".into());
|
||||
}
|
||||
let original = project.clone();
|
||||
let diagram = project.diagrams.get(&diagram).ok_or("Unknown diagram")?;
|
||||
let view = projection::project_view(project, diagram.id);
|
||||
let routed = routing::route_diagram(project, diagram, &view);
|
||||
let mut inventory = Vec::new();
|
||||
for edge in &view.edges {
|
||||
let (kind, source, target) = if let Some(r) = project.relations.get(&edge.id) {
|
||||
(format!("{:?}", r.kind()), r.source(), r.target())
|
||||
} else if let Some(t) = project.traces.get(&edge.id) {
|
||||
(format!("Trace:{:?}", t.kind), t.oa, t.sa)
|
||||
} else {
|
||||
return Err("Projected edge has no semantic identity".into());
|
||||
};
|
||||
inventory.push(
|
||||
json!({"id":edge.id,"kind":kind,"source":source,"target":target,
|
||||
"source_port":edge.source_port,"target_port":edge.target_port,
|
||||
"projected_source":edge.source,"projected_target":edge.target,"trace":edge.trace}),
|
||||
);
|
||||
}
|
||||
inventory.sort_by_key(|row| row["id"].to_string());
|
||||
let expected: Option<Value> = if diagram.id.to_string() == MAIN {
|
||||
Some(serde_json::from_slice(include_bytes!(
|
||||
"../../../harness/experiments/diagram-readability-20260929/mixed-answer-key.json"
|
||||
))?)
|
||||
} else if diagram.id.to_string() == EIGHT {
|
||||
Some(serde_json::from_slice(include_bytes!(
|
||||
"../../../harness/experiments/diagram-readability-20260929/eight-answer-key.json"
|
||||
))?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if expected
|
||||
.as_ref()
|
||||
.is_some_and(|key| *key != json!(inventory))
|
||||
{
|
||||
return Err(format!(
|
||||
"Projected semantic inventory differs from independently declared answer key: {}",
|
||||
json!(inventory)
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let mut edges = Vec::new();
|
||||
let mut alternate = Vec::new();
|
||||
for edge in routed.edges.values() {
|
||||
let projected = view
|
||||
.edges
|
||||
.iter()
|
||||
.find(|p| p.id == edge.id)
|
||||
.ok_or("Routed edge missing projected identity")?;
|
||||
let semantic = if let Some(relation) = project.relations.get(&edge.id) {
|
||||
json!({"relation":relation,"source_element":project.elements.get(&relation.source()),
|
||||
"target_element":project.elements.get(&relation.target())})
|
||||
} else {
|
||||
json!({"trace":project.traces.get(&edge.id)})
|
||||
};
|
||||
edges.push(
|
||||
json!({"id":edge.id,"projected":projected,"semantic":semantic,
|
||||
"points":edge.points,"label":{"text":edge.label.text,"fraction":edge.label.fraction,
|
||||
"anchor":edge.label.anchor,"center":edge.label.center,
|
||||
"position":edge.label.bounds.position,"size":edge.label.bounds.size},
|
||||
"effective_pins":edge.pins,"pin_fractions":edge.pin_fractions,
|
||||
"authored_route":diagram.routes.iter().find(|r| r.edge == edge.id),
|
||||
"custom_suspended":edge.custom_suspended,
|
||||
"route_warnings":edge.warnings.iter().map(|w| format!("{w:?}")).collect::<Vec<_>>()}),
|
||||
);
|
||||
// A positive sample proves a geometric alternate under this query policy.
|
||||
// Failure to find one does NOT prove global inaccessibility.
|
||||
let mut line_witness = None;
|
||||
for pair in edge.points.windows(2) {
|
||||
for t in [0.25, 0.5, 0.75] {
|
||||
let p = Position {
|
||||
x: pair[0].x + (pair[1].x - pair[0].x) * t,
|
||||
y: pair[0].y + (pair[1].y - pair[0].y) * t,
|
||||
};
|
||||
let hits = candidates(project, &view, &routed, p, zoom);
|
||||
if unobscured(&hits)
|
||||
&& hits["edge_candidates"]
|
||||
.as_array()
|
||||
.is_some_and(|a| a.len() == 1)
|
||||
{
|
||||
line_witness = Some(p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if line_witness.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let label_hits = candidates(project, &view, &routed, edge.label.center, zoom);
|
||||
let label_witness = !edge.label.text.is_empty()
|
||||
&& label_hits["node_occluders"]
|
||||
.as_array()
|
||||
.is_some_and(Vec::is_empty)
|
||||
&& label_hits["port_occluders"]
|
||||
.as_array()
|
||||
.is_some_and(Vec::is_empty)
|
||||
&& label_hits["association_label_occluders"]
|
||||
.as_array()
|
||||
.is_some_and(Vec::is_empty)
|
||||
&& label_hits["label_occluders"]
|
||||
.as_array()
|
||||
.is_some_and(|a| a.len() == 1);
|
||||
alternate.push(json!({"edge":edge.id,"unoccluded_unique_segment_sample":line_witness,
|
||||
"unoccluded_unique_label_center":label_witness.then_some(edge.label.center),
|
||||
"negative_result":"No sampled witness is not proof of global inaccessibility",
|
||||
"native_access":"Not evaluated; Inspector, viewport, selected handles and gesture state excluded"}));
|
||||
}
|
||||
if *project != original {
|
||||
return Err("Projection/routing changed the Project".into());
|
||||
}
|
||||
let query = candidates(project, &view, &routed, point, zoom);
|
||||
let historical_point = Position { x: 1982., y: 375. };
|
||||
let historical_query = candidates(project, &view, &routed, historical_point, 1.);
|
||||
let historical_ids = [
|
||||
"00000000-0000-0000-0000-00000001adb8",
|
||||
"00000000-0000-0000-0000-00000001adb9",
|
||||
];
|
||||
let historical_reproduced = diagram.id.to_string() == MAIN
|
||||
&& unobscured(&historical_query)
|
||||
&& historical_query["edge_candidates"]
|
||||
.as_array()
|
||||
.is_some_and(|rows| {
|
||||
rows.len() == 2
|
||||
&& historical_ids.iter().all(|id| {
|
||||
rows.iter().any(|r| {
|
||||
r["edge"] == *id
|
||||
&& r["segments"]
|
||||
.as_array()
|
||||
.is_some_and(|s| s.iter().any(|s| s["distance"] == 0.))
|
||||
})
|
||||
})
|
||||
})
|
||||
&& historical_ids.iter().all(|id| {
|
||||
routed
|
||||
.edges
|
||||
.get(&id.parse::<Id>().unwrap())
|
||||
.is_some_and(|e| {
|
||||
e.points.windows(2).any(|p| {
|
||||
p[0].x == 1982.
|
||||
&& p[1].x == 1982.
|
||||
&& p[0].y.min(p[1].y) <= 336.
|
||||
&& p[0].y.max(p[1].y) >= 414.
|
||||
})
|
||||
})
|
||||
});
|
||||
Ok(
|
||||
json!({"experiment":"issue38 geometry probe; not feature acceptance",
|
||||
"experiment_declared_base":"715036e132677dd2bc2fb37ed316d93dc9987aec",
|
||||
"build_provenance":"Require external manifest/source hash verification; this field does not identify the executable build",
|
||||
"diagram":diagram.id,"project":project,
|
||||
"project_conserved":true,"projected_view":view,"routed_edges":edges,
|
||||
"bounds":{"position":routed.bounds.position,"size":routed.bounds.size},
|
||||
"query":query,"alternate_access_samples":alternate,
|
||||
"projected_semantic_inventory":inventory,"declared_answer_key":expected,
|
||||
"historical_witness_reproduced":if diagram.id.to_string()==MAIN {Some(historical_reproduced)} else {None},
|
||||
"historical_witness_policy":"Exact IDs, zero distance at (1982,375), no occluders, common x1982 span336..414 beyond shared port lead; zoom1. False is a result, not failure.",
|
||||
"diagnostics":validation::validate(project)}),
|
||||
)
|
||||
}
|
||||
|
||||
fn write_new(path: &Path, bytes: &[u8]) -> Result<()> {
|
||||
let mut file = OpenOptions::new().write(true).create_new(true).open(path)?;
|
||||
file.write_all(bytes)?;
|
||||
file.sync_all()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
if args.len() == 2 && args[0] == "generate" {
|
||||
// Refuse an existing directory so no previous experiment output is replaced.
|
||||
let out = Path::new(&args[1]);
|
||||
let mixed = admit(MIXED, WARNINGS)?;
|
||||
let eight = representative()?;
|
||||
let scenes = [
|
||||
(
|
||||
"mixed-dispatch",
|
||||
mixed,
|
||||
MAIN,
|
||||
Position { x: 1982., y: 375. },
|
||||
),
|
||||
(
|
||||
"eight-card-representative",
|
||||
eight,
|
||||
EIGHT,
|
||||
Position { x: 600., y: 450. },
|
||||
),
|
||||
];
|
||||
let mut outputs = Vec::new();
|
||||
for (name, project, id, point) in scenes {
|
||||
outputs.push((format!("{name}.json"), serde_json::to_vec_pretty(&project)?));
|
||||
outputs.push((
|
||||
format!("{name}-geometry.json"),
|
||||
serde_json::to_vec_pretty(&snapshot(&project, id.parse()?, point, 1.)?)?,
|
||||
));
|
||||
}
|
||||
fs::create_dir(out)?;
|
||||
write_new(&out.join("mixed-dispatch-original.json"), MIXED)?;
|
||||
write_new(&out.join("release1-original.json"), RELEASE)?;
|
||||
for (name, bytes) in outputs {
|
||||
write_new(&out.join(name), &bytes)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if args.len() != 8 || args[0] != "probe" {
|
||||
return Err("Usage: generate NEW_DIRECTORY | probe INPUT_JSON_OR_SINGLE_SOURCE EXPECTED_DIAGNOSTICS DIAGRAM_UUID WORLD_X WORLD_Y ZOOM NEW_OUTPUT_JSON".into());
|
||||
}
|
||||
let input = fs::read(&args[1])?;
|
||||
let project = admit(&input, &fs::read(&args[2])?)?;
|
||||
let mut result = snapshot(
|
||||
&project,
|
||||
args[3].parse()?,
|
||||
Position {
|
||||
x: args[4].parse()?,
|
||||
y: args[5].parse()?,
|
||||
},
|
||||
args[6].parse()?,
|
||||
)?;
|
||||
result["original_input_utf8"] = json!(std::str::from_utf8(&input)?);
|
||||
if fs::read(&args[1])? != input {
|
||||
return Err("Input changed during experiment".into());
|
||||
}
|
||||
write_new(Path::new(&args[7]), &serde_json::to_vec_pretty(&result)?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn representative_preserves_every_original_record_and_view() {
|
||||
let baseline = admit(RELEASE, RELEASE_WARNINGS).unwrap();
|
||||
let mut generated = representative().unwrap();
|
||||
let added = generated.diagrams.remove(&EIGHT.parse().unwrap()).unwrap();
|
||||
assert_eq!(added.nodes.len(), 8);
|
||||
assert_eq!(generated, baseline);
|
||||
let project = representative().unwrap();
|
||||
let result = snapshot(
|
||||
&project,
|
||||
EIGHT.parse().unwrap(),
|
||||
Position { x: 600., y: 450. },
|
||||
1.,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result["projected_semantic_inventory"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.len(),
|
||||
12
|
||||
);
|
||||
assert_eq!(
|
||||
result["projected_semantic_inventory"],
|
||||
result["declared_answer_key"]
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn complete_scene_preserves_semantics_sentinel_and_authored_route() {
|
||||
let project = admit(MIXED, WARNINGS).unwrap();
|
||||
let before = project.clone();
|
||||
let id = MAIN.parse().unwrap();
|
||||
let result = snapshot(&project, id, Position { x: 1982., y: 375. }, 1.).unwrap();
|
||||
assert_eq!(project, before);
|
||||
assert_eq!(result["project"], serde_json::to_value(&before).unwrap());
|
||||
assert_eq!(project.diagrams.len(), 2);
|
||||
assert_eq!(
|
||||
result["projected_view"]["nodes"].as_array().unwrap().len(),
|
||||
10
|
||||
);
|
||||
assert_eq!(result["routed_edges"].as_array().unwrap().len(), 14);
|
||||
for edge in result["routed_edges"].as_array().unwrap() {
|
||||
let edge_id: Id = serde_json::from_value(edge["id"].clone()).unwrap();
|
||||
assert_eq!(
|
||||
edge["semantic"]["relation"],
|
||||
serde_json::to_value(&before.relations[&edge_id]).unwrap()
|
||||
);
|
||||
}
|
||||
let route = &before.diagrams[&id].routes[0];
|
||||
assert_eq!(
|
||||
route.edge.to_string(),
|
||||
"00000000-0000-0000-0000-00000001adbd"
|
||||
);
|
||||
assert_eq!(route.waypoints, vec![Position { x: 300., y: -100. }]);
|
||||
assert_eq!(route.label.as_ref().unwrap().fraction, 0.4);
|
||||
assert_eq!(
|
||||
snapshot(&project, id, Position { x: 1982., y: 375. }, 1.).unwrap(),
|
||||
result
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn unexpected_diagnostics_and_invalid_source_fail_closed() {
|
||||
assert!(admit(MIXED, b"[]").is_err());
|
||||
assert!(admit(b"{}", b"[]").is_err());
|
||||
let project = admit(MIXED, WARNINGS).unwrap();
|
||||
assert!(
|
||||
snapshot(
|
||||
&project,
|
||||
MAIN.parse().unwrap(),
|
||||
Position { x: 0., y: 0. },
|
||||
0.
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn square_port_corner_is_occluded_even_outside_ten_unit_circle() {
|
||||
let project = admit(MIXED, WARNINGS).unwrap();
|
||||
let diagram = &project.diagrams[&MAIN.parse().unwrap()];
|
||||
let view = projection::project_view(&project, diagram.id);
|
||||
let routed = routing::route_diagram(&project, diagram, &view);
|
||||
let port = &view.ports[0];
|
||||
let point = Position {
|
||||
x: port.position.x + 9.,
|
||||
y: port.position.y + 9.,
|
||||
};
|
||||
assert!(9_f32.hypot(9.) > 10.);
|
||||
for zoom in [1., 2.] {
|
||||
let hits = candidates(&project, &view, &routed, point, zoom);
|
||||
assert!(hits["port_occluders"].as_array().unwrap().iter()
|
||||
.any(|p| p["element"]==json!(port.element) && p["owner"]==json!(port.owner)));
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn analytical_query_controls_distinguish_edges_segments_and_labels() {
|
||||
// Synthetic geometry controls using genuine identities, NOT witness evidence.
|
||||
let project = admit(MIXED, WARNINGS).unwrap();
|
||||
let diagram = &project.diagrams[&MAIN.parse().unwrap()];
|
||||
let mut view = projection::project_view(&project, diagram.id);
|
||||
let mut routed = routing::route_diagram(&project, diagram, &view);
|
||||
view.nodes.clear();
|
||||
view.ports.clear();
|
||||
let first: Id = "00000000-0000-0000-0000-00000001adb8".parse().unwrap();
|
||||
let second: Id = "00000000-0000-0000-0000-00000001adb9".parse().unwrap();
|
||||
routed.edges.retain(|id, _| *id == first || *id == second);
|
||||
for (id, edge) in &mut routed.edges {
|
||||
edge.points = if *id == first {
|
||||
vec![
|
||||
Position { x: -20., y: 0. },
|
||||
Position { x: 0., y: 0. },
|
||||
Position { x: 0., y: 20. },
|
||||
]
|
||||
} else {
|
||||
vec![Position { x: -20., y: 50. }, Position { x: 20., y: 50. }]
|
||||
};
|
||||
edge.label.text.clear();
|
||||
}
|
||||
let point = Position { x: 0., y: 0. };
|
||||
let hits = candidates(&project, &view, &routed, point, 1.);
|
||||
assert_eq!(hits["edge_candidates"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(
|
||||
hits["edge_candidates"][0]["segments"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
assert!(unobscured(&hits));
|
||||
let edge = routed.edges.get_mut(&second).unwrap();
|
||||
edge.points = vec![Position { x: -20., y: 0. }, Position { x: 20., y: 0. }];
|
||||
edge.label.bounds = routing::RoutingBounds {
|
||||
position: Position { x: -5., y: -5. },
|
||||
size: oa_core::Size {
|
||||
width: 10.,
|
||||
height: 10.,
|
||||
},
|
||||
};
|
||||
edge.label.text = "Control label".into();
|
||||
let hits = candidates(&project, &view, &routed, point, 1.);
|
||||
assert_eq!(hits["edge_candidates"].as_array().unwrap().len(), 2);
|
||||
assert_eq!(hits["label_occluders"], json!([second]));
|
||||
assert!(!unobscured(&hits));
|
||||
routed.edges.get_mut(&second).unwrap().label.text.clear();
|
||||
let hits = candidates(&project, &view, &routed, point, 1.);
|
||||
assert!(hits["label_occluders"].as_array().unwrap().is_empty());
|
||||
assert!(unobscured(&hits));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user