768 lines
27 KiB
Rust
768 lines
27 KiB
Rust
//! Explicit, one-fixture visual experiment. No persisted formatting policy.
|
|
use super::*;
|
|
use iced::advanced::{graphics::text as graphics_text, text as shaped};
|
|
use shaped::Paragraph as _;
|
|
use std::sync::{Mutex, OnceLock};
|
|
|
|
const DIAGRAM: u128 = 0x00000000003840008000000000000001;
|
|
const EDGE: u128 = 0x63009b5f539e5914b630ec6b7fefb4d5;
|
|
pub(super) fn edge_id() -> Id {
|
|
Id::from_u128(EDGE)
|
|
}
|
|
|
|
const NAME: &str = "Technician service conversation";
|
|
const FONT_SIZE: f32 = 13.;
|
|
const ADVANCE: f32 = 16.;
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub(super) enum Profile {
|
|
#[default]
|
|
Baseline,
|
|
E,
|
|
F,
|
|
}
|
|
pub(super) fn profile() -> Profile {
|
|
static PROFILE: OnceLock<Profile> = OnceLock::new();
|
|
*PROFILE.get_or_init(
|
|
|| match std::env::var("ARCADIA_LABEL_EXPERIMENT").as_deref() {
|
|
Ok("E") => Profile::E,
|
|
Ok("F") => Profile::F,
|
|
_ => Profile::Baseline,
|
|
},
|
|
)
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
struct MeasuredLine {
|
|
text: &'static str,
|
|
width: f32,
|
|
baseline: f32,
|
|
family: String,
|
|
}
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub(super) struct Line {
|
|
pub text: &'static str,
|
|
pub position: Position,
|
|
pub baseline_y: f32,
|
|
pub width: f32,
|
|
pub family: String,
|
|
}
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub(super) struct Layout {
|
|
pub canonical_text: String,
|
|
pub lines: Vec<Line>,
|
|
pub geometry: routing::EdgeLabelGeometry,
|
|
/// Authored moves retain their shape even outside the automatic guard.
|
|
pub clear: bool,
|
|
}
|
|
|
|
pub(super) fn font_version(profile: Profile) -> Option<graphics_text::Version> {
|
|
(profile != Profile::Baseline).then(|| {
|
|
graphics_text::font_system()
|
|
.read()
|
|
.expect("Read font system")
|
|
.version()
|
|
})
|
|
}
|
|
|
|
fn measured(profile: Profile) -> Option<Arc<Vec<MeasuredLine>>> {
|
|
type Entry = (Profile, graphics_text::Version, Arc<Vec<MeasuredLine>>);
|
|
static CACHE: OnceLock<Mutex<Vec<Entry>>> = OnceLock::new();
|
|
let version = font_version(profile)?;
|
|
let mut cache = CACHE.get_or_init(|| Mutex::new(Vec::new())).lock().ok()?;
|
|
if let Some((_, _, lines)) = cache
|
|
.iter()
|
|
.find(|(p, v, _)| *p == profile && *v == version)
|
|
{
|
|
return Some(lines.clone());
|
|
}
|
|
let strings: &[&'static str] = match profile {
|
|
Profile::Baseline => return None,
|
|
Profile::E => &["Technician service", "conversation"],
|
|
Profile::F => &["Technician", "service", "conversation"],
|
|
};
|
|
let mut lines = Vec::new();
|
|
for &content in strings {
|
|
let paragraph = graphics_text::Paragraph::with_text(shaped::Text {
|
|
content,
|
|
bounds: Size::INFINITE,
|
|
size: FONT_SIZE.into(),
|
|
line_height: shaped::LineHeight::Absolute(ADVANCE.into()),
|
|
font: iced::Font::default(),
|
|
align_x: shaped::Alignment::Left,
|
|
align_y: iced::alignment::Vertical::Top,
|
|
shaping: shaped::Shaping::Basic,
|
|
wrapping: shaped::Wrapping::None,
|
|
});
|
|
let mut runs = paragraph.buffer().layout_runs();
|
|
let run = runs.next()?;
|
|
if runs.next().is_some() || run.rtl || run.glyphs.is_empty() {
|
|
return None;
|
|
}
|
|
let width = paragraph.min_width();
|
|
if !width.is_finite() || width <= 0. || paragraph.min_height() > ADVANCE + 0.01 {
|
|
return None;
|
|
}
|
|
// This fixed English witness must resolve to one actual font face.
|
|
// Mixed fallback is not silently exported as a generic SVG font.
|
|
let font_id = run.glyphs.first()?.font_id;
|
|
if run.glyphs.iter().any(|glyph| glyph.font_id != font_id) {
|
|
return None;
|
|
}
|
|
let family = graphics_text::font_system()
|
|
.write()
|
|
.ok()?
|
|
.raw()
|
|
.db()
|
|
.face(font_id)?
|
|
.families
|
|
.first()?
|
|
.0
|
|
.clone();
|
|
lines.push(MeasuredLine {
|
|
text: content,
|
|
width,
|
|
baseline: run.line_y,
|
|
family,
|
|
});
|
|
}
|
|
let lines = Arc::new(lines);
|
|
cache.retain(|(_, v, _)| *v == version);
|
|
cache.push((profile, version, lines.clone()));
|
|
Some(lines)
|
|
}
|
|
|
|
/// Runs before any scene consumer; never edits the model or any routed points.
|
|
pub(super) fn apply(
|
|
project: &Project,
|
|
diagram: &Diagram,
|
|
view: &ProjectedView,
|
|
routed: &mut RoutedView,
|
|
profile: Profile,
|
|
) -> Option<Layout> {
|
|
if profile == Profile::Baseline || diagram.id.as_u128() != DIAGRAM {
|
|
return None;
|
|
}
|
|
let id = Id::from_u128(EDGE);
|
|
let edge = routed.edges.get(&id)?;
|
|
if edge.label.text != NAME
|
|
|| project.relations.get(&id)?.name != NAME
|
|
|| edge.custom_suspended
|
|
|| view
|
|
.nodes
|
|
.iter()
|
|
.any(|node| node.visual_parent.is_some() || node.collapsed)
|
|
{
|
|
return None;
|
|
}
|
|
let metrics = measured(profile)?;
|
|
let width = metrics.iter().map(|line| line.width).fold(0., f32::max) + 16.;
|
|
let height = 8. + ADVANCE * metrics.len() as f32;
|
|
let authored = diagram
|
|
.routes
|
|
.iter()
|
|
.find(|route| route.edge == id)
|
|
.and_then(|route| route.label.as_ref());
|
|
let fraction = authored.map_or(if profile == Profile::E { 0.5 } else { 0.55 }, |label| {
|
|
label.fraction
|
|
});
|
|
let anchor = routing::point_along_polyline(&edge.points, fraction)?;
|
|
let offset = authored.map_or(
|
|
Position {
|
|
x: if profile == Profile::E {
|
|
0.
|
|
} else {
|
|
18. + width / 2.
|
|
},
|
|
y: 0.,
|
|
},
|
|
|label| label.offset,
|
|
);
|
|
let center = Position {
|
|
x: anchor.x + offset.x,
|
|
y: anchor.y + offset.y,
|
|
};
|
|
let bounds = routing::RoutingBounds {
|
|
position: Position {
|
|
x: center.x - width / 2.,
|
|
y: center.y - height / 2.,
|
|
},
|
|
size: oa_core::Size { width, height },
|
|
};
|
|
let clear = guarded(project, view, routed, id, bounds, profile);
|
|
if authored.is_none() && !clear {
|
|
return None;
|
|
}
|
|
let geometry = routing::EdgeLabelGeometry {
|
|
text: edge.label.text.clone(),
|
|
fraction,
|
|
anchor,
|
|
center,
|
|
bounds,
|
|
};
|
|
let lines = metrics
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(index, line)| {
|
|
let top = bounds.position.y + 4. + ADVANCE * index as f32;
|
|
Line {
|
|
text: line.text,
|
|
position: Position {
|
|
x: center.x - line.width / 2.,
|
|
y: top,
|
|
},
|
|
baseline_y: top + line.baseline,
|
|
width: line.width,
|
|
family: line.family.clone(),
|
|
}
|
|
})
|
|
.collect();
|
|
let layout = Layout {
|
|
canonical_text: edge.label.text.clone(),
|
|
lines,
|
|
geometry,
|
|
clear,
|
|
};
|
|
routed.edges.get_mut(&id)?.label = layout.geometry.clone();
|
|
// Retain conservative existing bounds; add authored placements outside them.
|
|
// The fixed automatic witnesses are inside the unchanged node extents.
|
|
let old = routed.bounds;
|
|
let left = old.position.x.min(bounds.position.x);
|
|
let top = old.position.y.min(bounds.position.y);
|
|
let right = (old.position.x + old.size.width).max(bounds.position.x + width);
|
|
let bottom = (old.position.y + old.size.height).max(bounds.position.y + height);
|
|
routed.bounds = routing::RoutingBounds {
|
|
position: Position { x: left, y: top },
|
|
size: oa_core::Size {
|
|
width: right - left,
|
|
height: bottom - top,
|
|
},
|
|
};
|
|
Some(layout)
|
|
}
|
|
|
|
fn rectangle(bounds: routing::RoutingBounds, padding: f32) -> Rectangle {
|
|
Rectangle {
|
|
x: bounds.position.x - padding,
|
|
y: bounds.position.y - padding,
|
|
width: bounds.size.width + 2. * padding,
|
|
height: bounds.size.height + 2. * padding,
|
|
}
|
|
}
|
|
fn overlap(a: Rectangle, b: Rectangle) -> bool {
|
|
a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y
|
|
}
|
|
fn crossing(bounds: Rectangle, span: &[Position]) -> bool {
|
|
let [a, b] = [span[0], span[1]];
|
|
if a.x == b.x {
|
|
a.x >= bounds.x
|
|
&& a.x <= bounds.x + bounds.width
|
|
&& a.y.min(b.y) <= bounds.y + bounds.height
|
|
&& a.y.max(b.y) >= bounds.y
|
|
} else if a.y == b.y {
|
|
a.y >= bounds.y
|
|
&& a.y <= bounds.y + bounds.height
|
|
&& a.x.min(b.x) <= bounds.x + bounds.width
|
|
&& a.x.max(b.x) >= bounds.x
|
|
} else {
|
|
// The bounded witness supports orthogonal geometry only.
|
|
true
|
|
}
|
|
}
|
|
fn guarded(
|
|
project: &Project,
|
|
view: &ProjectedView,
|
|
routed: &RoutedView,
|
|
id: Id,
|
|
bounds: routing::RoutingBounds,
|
|
profile: Profile,
|
|
) -> bool {
|
|
let label = rectangle(bounds, 0.);
|
|
if view.nodes.iter().any(|node| {
|
|
overlap(
|
|
label,
|
|
Rectangle {
|
|
x: node.position.x - 12.,
|
|
y: node.position.y - 12.,
|
|
width: node.size.width + 24.,
|
|
height: node.size.height + 24.,
|
|
},
|
|
)
|
|
}) {
|
|
return false;
|
|
}
|
|
let stroke_bounds = rectangle(bounds, 3.);
|
|
let mut own_crossings = Vec::new();
|
|
for edge in routed.edges.values() {
|
|
if edge.id != id
|
|
&& !edge.label.text.is_empty()
|
|
&& overlap(label, rectangle(edge.label.bounds, 4.))
|
|
{
|
|
return false;
|
|
}
|
|
if let Some(relation) = project.relations.get(&edge.id) {
|
|
for end in routing::association_end_geometry(relation, edge) {
|
|
if !end.label.text.is_empty() && overlap(label, rectangle(end.label.bounds, 4.)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
for span in edge.points.windows(2) {
|
|
if crossing(stroke_bounds, span) {
|
|
if edge.id != id || profile == Profile::F {
|
|
return false;
|
|
}
|
|
own_crossings.push(span);
|
|
}
|
|
}
|
|
}
|
|
if profile == Profile::E {
|
|
let [span] = own_crossings.as_slice() else {
|
|
return false;
|
|
};
|
|
let [a, b] = [span[0], span[1]];
|
|
// One complete straight crossing, not a bend, lead, tip, or shared run.
|
|
(a.x == b.x
|
|
&& a.y.min(b.y) < stroke_bounds.y
|
|
&& a.y.max(b.y) > stroke_bounds.y + stroke_bounds.height)
|
|
|| (a.y == b.y
|
|
&& a.x.min(b.x) < stroke_bounds.x
|
|
&& a.x.max(b.x) > stroke_bounds.x + stroke_bounds.width)
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
|
|
impl Layout {
|
|
pub(super) fn canvas_lines(&self, color: Color) -> impl Iterator<Item = Text> + '_ {
|
|
self.lines.iter().map(move |line| Text {
|
|
content: line.text.into(),
|
|
position: point(line.position),
|
|
color,
|
|
size: FONT_SIZE.into(),
|
|
line_height: shaped::LineHeight::Absolute(ADVANCE.into()),
|
|
font: iced::Font::default(),
|
|
align_x: shaped::Alignment::Left,
|
|
align_y: iced::alignment::Vertical::Top,
|
|
shaping: shaped::Shaping::Basic,
|
|
max_width: f32::INFINITY,
|
|
})
|
|
}
|
|
pub(super) fn svg(&self) -> String {
|
|
let bounds = self.geometry.bounds;
|
|
let mut out = format!(
|
|
"<g data-label-experiment=\"wrapped\"><title>{}</title><rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" fill=\"white\"/>",
|
|
escape(&self.canonical_text),
|
|
bounds.position.x,
|
|
bounds.position.y,
|
|
bounds.size.width,
|
|
bounds.size.height
|
|
);
|
|
for line in &self.lines {
|
|
out.push_str(&format!("<text x=\"{}\" y=\"{}\" font-family=\"{}\" font-size=\"13\" fill=\"#637487\">{}</text>",
|
|
line.position.x, line.baseline_y, escape(&line.family), escape(line.text)));
|
|
}
|
|
out.push_str("</g>");
|
|
out
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn fixture() -> (Project, Id) {
|
|
let p: Project = serde_json::from_slice(include_bytes!(
|
|
"../../../../harness/experiments/wrapped-label-20261001/fixture/eight-card.json"
|
|
))
|
|
.unwrap();
|
|
(p, Id::from_u128(DIAGRAM))
|
|
}
|
|
fn canvas(p: &Project, diagram: Id) -> DiagramCanvas<'_> {
|
|
DiagramCanvas {
|
|
selection: None,
|
|
allocation: None,
|
|
project: p,
|
|
view_index: None,
|
|
diagram,
|
|
selected: edge_id(),
|
|
navigation_focus: None,
|
|
zoom: 1.,
|
|
pan: Vector::ZERO,
|
|
context: Context {
|
|
document: p.id,
|
|
diagram,
|
|
revision: 0,
|
|
epoch: 0,
|
|
},
|
|
selected_bend: None,
|
|
selected_attachment: None,
|
|
editable: true,
|
|
placing: false,
|
|
connection: None,
|
|
}
|
|
}
|
|
#[test]
|
|
fn baseline_is_exact_and_wrapping_changes_only_target_derived_geometry() {
|
|
let (p, id) = fixture();
|
|
let before = serde_json::to_vec(&p).unwrap();
|
|
let diagram = &p.diagrams[&id];
|
|
let view = project_view(&p, id);
|
|
let baseline = route_diagram(&p, diagram, &view);
|
|
assert_eq!(baseline.edges.len(), 12);
|
|
let mut unchanged = baseline.clone();
|
|
assert!(apply(&p, diagram, &view, &mut unchanged, Profile::Baseline).is_none());
|
|
assert_eq!(unchanged, baseline);
|
|
for profile in [Profile::E, Profile::F] {
|
|
let mut routed = baseline.clone();
|
|
let layout =
|
|
apply(&p, diagram, &view, &mut routed, profile).expect("Measured witness must fit");
|
|
assert!(layout.clear);
|
|
assert_eq!(layout.canonical_text, NAME);
|
|
assert_eq!(
|
|
layout
|
|
.lines
|
|
.iter()
|
|
.map(|line| line.text)
|
|
.collect::<Vec<_>>()
|
|
.join(" "),
|
|
NAME
|
|
);
|
|
assert_eq!(routed.edges[&edge_id()].label.text, NAME);
|
|
for (id, old) in &baseline.edges {
|
|
assert_eq!(routed.edges[id].points, old.points);
|
|
if *id != edge_id() {
|
|
assert_eq!(&routed.edges[id], old);
|
|
}
|
|
}
|
|
let mut restored = routed.clone();
|
|
restored.edges.get_mut(&edge_id()).unwrap().label =
|
|
baseline.edges[&edge_id()].label.clone();
|
|
assert_eq!(
|
|
restored, baseline,
|
|
"No paths, ports, solver state, other labels or scene extents change"
|
|
);
|
|
assert_eq!(
|
|
layout.geometry.bounds.size.height,
|
|
8. + ADVANCE * layout.lines.len() as f32
|
|
);
|
|
let max_width = layout
|
|
.lines
|
|
.iter()
|
|
.map(|line| line.width)
|
|
.fold(0., f32::max);
|
|
assert_eq!(layout.geometry.bounds.size.width, max_width + 16.);
|
|
assert!(
|
|
layout
|
|
.lines
|
|
.iter()
|
|
.all(|line| !line.family.is_empty() && line.width > 0.)
|
|
);
|
|
if profile == Profile::F {
|
|
assert_eq!(layout.geometry.fraction, 0.55);
|
|
assert!(
|
|
(layout.geometry.bounds.position.x - layout.geometry.anchor.x - 18.).abs()
|
|
< 0.001
|
|
);
|
|
assert!(
|
|
layout.geometry.bounds.size.width < 114.5,
|
|
"Measured F loses own-is-closer margin"
|
|
);
|
|
} else {
|
|
assert_eq!(layout.geometry.fraction, 0.5);
|
|
assert_eq!(layout.geometry.center, layout.geometry.anchor);
|
|
}
|
|
println!(
|
|
"{profile:?}: bounds={:?}; lines={:?}",
|
|
layout.geometry.bounds, layout.lines
|
|
);
|
|
}
|
|
assert_eq!(serde_json::to_vec(&p).unwrap(), before);
|
|
}
|
|
#[test]
|
|
fn measured_backing_canvas_svg_picking_and_focus_share_geometry() {
|
|
let (p, id) = fixture();
|
|
let c = canvas(&p, id);
|
|
for profile in [Profile::E, Profile::F] {
|
|
let state = State::default();
|
|
let scene = c.scene_with_profile(&state, profile);
|
|
let layout = scene.label_layout.as_ref().unwrap();
|
|
let bounds = rectangle(layout.geometry.bounds, 0.);
|
|
let texts = layout.canvas_lines(Color::BLACK).collect::<Vec<_>>();
|
|
let svg = super::super::export_svg_with_profile(&p, id, profile).unwrap();
|
|
assert!(svg.contains(&layout.svg()));
|
|
assert_eq!(
|
|
crate::diagram_usage::edge_focus_bounds(&scene.routed.edges[&edge_id()]),
|
|
bounds
|
|
);
|
|
for (line, text) in layout.lines.iter().zip(texts) {
|
|
assert_eq!(text.content, line.text);
|
|
assert_eq!(text.position, point(line.position));
|
|
assert_eq!(text.size.0, FONT_SIZE);
|
|
assert_eq!(
|
|
text.line_height,
|
|
shaped::LineHeight::Absolute(ADVANCE.into())
|
|
);
|
|
assert!(line.position.x >= bounds.x + 7.99);
|
|
assert!(line.position.x + line.width <= bounds.x + bounds.width - 7.99);
|
|
assert_eq!(
|
|
c.hit_label(
|
|
&scene.routed,
|
|
Point::new(line.position.x + line.width / 2., line.position.y + 8.)
|
|
),
|
|
Some(edge_id())
|
|
);
|
|
assert!(svg.contains(&format!(
|
|
"x=\"{}\" y=\"{}\" font-family=\"{}\"",
|
|
line.position.x,
|
|
line.baseline_y,
|
|
escape(&line.family)
|
|
)));
|
|
}
|
|
assert_eq!(
|
|
c.hit_label(&scene.routed, Point::new(bounds.x + 1., bounds.y + 1.)),
|
|
Some(edge_id())
|
|
);
|
|
}
|
|
}
|
|
#[test]
|
|
fn automatic_guard_refuses_foreign_coincident_stroke_and_card() {
|
|
let (p, id) = fixture();
|
|
let diagram = &p.diagrams[&id];
|
|
let view = project_view(&p, id);
|
|
let baseline = route_diagram(&p, diagram, &view);
|
|
for profile in [Profile::E, Profile::F] {
|
|
let mut valid = baseline.clone();
|
|
let layout = apply(&p, diagram, &view, &mut valid, profile).unwrap();
|
|
let mut blocked = baseline.clone();
|
|
let foreign = blocked
|
|
.edges
|
|
.keys()
|
|
.copied()
|
|
.find(|id| *id != edge_id())
|
|
.unwrap();
|
|
let bounds = layout.geometry.bounds;
|
|
blocked.edges.get_mut(&foreign).unwrap().points = vec![
|
|
Position {
|
|
x: layout.geometry.center.x,
|
|
y: bounds.position.y - 40.,
|
|
},
|
|
Position {
|
|
x: layout.geometry.center.x,
|
|
y: bounds.position.y + bounds.size.height + 40.,
|
|
},
|
|
];
|
|
let snapshot = blocked.clone();
|
|
assert!(apply(&p, diagram, &view, &mut blocked, profile).is_none());
|
|
assert_eq!(blocked, snapshot);
|
|
let mut occluded_view = view.clone();
|
|
occluded_view.nodes[0].position = bounds.position;
|
|
let mut routed = baseline.clone();
|
|
assert!(apply(&p, diagram, &occluded_view, &mut routed, profile).is_none());
|
|
assert_eq!(routed, baseline);
|
|
}
|
|
}
|
|
#[test]
|
|
fn authored_fraction_offset_keep_shape_even_during_conflicting_move() {
|
|
let (mut p, id) = fixture();
|
|
let view = project_view(&p, id);
|
|
for profile in [Profile::E, Profile::F] {
|
|
let original = route_diagram(&p, &p.diagrams[&id], &view);
|
|
let mut automatic = original.clone();
|
|
let initial = apply(&p, &p.diagrams[&id], &view, &mut automatic, profile).unwrap();
|
|
let offset = Position {
|
|
x: initial.geometry.center.x - initial.geometry.anchor.x + 17.,
|
|
y: -9.,
|
|
};
|
|
p.diagrams.get_mut(&id).unwrap().routes = vec![EdgeRoute {
|
|
edge: edge_id(),
|
|
source_attachment: None,
|
|
target_attachment: None,
|
|
waypoints: vec![],
|
|
label: Some(EdgeLabel {
|
|
fraction: initial.geometry.fraction,
|
|
offset,
|
|
}),
|
|
}];
|
|
let c = canvas(&p, id);
|
|
let state = State::default();
|
|
let scene = c.scene_with_profile(&state, profile);
|
|
let moved = scene.label_layout.as_ref().unwrap();
|
|
assert_eq!(
|
|
moved.lines.iter().map(|line| line.text).collect::<Vec<_>>(),
|
|
initial
|
|
.lines
|
|
.iter()
|
|
.map(|line| line.text)
|
|
.collect::<Vec<_>>()
|
|
);
|
|
assert_eq!(moved.geometry.fraction, initial.geometry.fraction);
|
|
assert!((moved.geometry.center.x - moved.geometry.anchor.x - offset.x).abs() < 0.001);
|
|
assert!((moved.geometry.center.y - moved.geometry.anchor.y - offset.y).abs() < 0.001);
|
|
let mut preview = State {
|
|
context: Some(c.context),
|
|
..State::default()
|
|
};
|
|
preview.route_drag = Some(Gesture {
|
|
context: c.context,
|
|
start: point(moved.geometry.center),
|
|
handle: Handle::Label { original: offset },
|
|
route: p.diagrams[&id].routes[0].clone(),
|
|
changed: true,
|
|
baseline: scene.core_routed.clone(),
|
|
});
|
|
preview
|
|
.route_drag
|
|
.as_mut()
|
|
.unwrap()
|
|
.route
|
|
.label
|
|
.as_mut()
|
|
.unwrap()
|
|
.offset
|
|
.x += 100.;
|
|
let dragged = c.scene_with_profile(&preview, profile);
|
|
let layout = dragged.label_layout.as_ref().unwrap();
|
|
assert_eq!(layout.lines.len(), moved.lines.len());
|
|
assert!(
|
|
(layout.geometry.center.x - layout.geometry.anchor.x - offset.x - 100.).abs()
|
|
< 0.001
|
|
);
|
|
for (id, edge) in &scene.routed.edges {
|
|
assert_eq!(dragged.routed.edges[id].points, edge.points);
|
|
}
|
|
p.diagrams.get_mut(&id).unwrap().routes.clear();
|
|
}
|
|
}
|
|
#[test]
|
|
fn different_edge_preview_guard_rejection_restores_complete_core_geometry() {
|
|
// Exercise the actual gesture capture and core preview, with a pure
|
|
// explicit profile. Moving another label onto the wrapped target must
|
|
// reject wrapping, leaving the full original one-line target geometry.
|
|
// All twelve point arrays and every other frozen label remain exact.
|
|
let (p, id) = fixture();
|
|
let view = project_view(&p, id);
|
|
let core = route_diagram(&p, &p.diagrams[&id], &view);
|
|
let foreign = core
|
|
.edges
|
|
.values()
|
|
.find(|edge| edge.id != edge_id() && !edge.label.text.is_empty())
|
|
.unwrap();
|
|
let original_offset = Position {
|
|
x: foreign.label.center.x - foreign.label.anchor.x,
|
|
y: foreign.label.center.y - foreign.label.anchor.y,
|
|
};
|
|
let c = DiagramCanvas {
|
|
selected: foreign.id,
|
|
..canvas(&p, id)
|
|
};
|
|
for profile in [Profile::E, Profile::F] {
|
|
let mut state = State {
|
|
context: Some(c.context),
|
|
..State::default()
|
|
};
|
|
let initial = c.scene_with_profile(&state, profile);
|
|
let target = initial
|
|
.label_layout
|
|
.as_ref()
|
|
.expect("Initial witness must wrap");
|
|
c.begin_route_with_profile(
|
|
&mut state,
|
|
point(foreign.label.center),
|
|
Handle::Label {
|
|
original: original_offset,
|
|
},
|
|
EdgeRoute {
|
|
edge: foreign.id,
|
|
source_attachment: None,
|
|
target_attachment: None,
|
|
waypoints: vec![],
|
|
label: Some(EdgeLabel {
|
|
fraction: foreign.label.fraction,
|
|
offset: original_offset,
|
|
}),
|
|
},
|
|
profile,
|
|
);
|
|
state
|
|
.route_drag
|
|
.as_mut()
|
|
.unwrap()
|
|
.move_to(point(target.geometry.center), 1.);
|
|
let diagram = c.preview_diagram(&state);
|
|
let expected = routing::route_edge_preview(&p, &diagram, &view, &core, foreign.id);
|
|
let mut guard_probe = expected.clone();
|
|
assert!(
|
|
apply(&p, &diagram, &view, &mut guard_probe, profile).is_none(),
|
|
"Foreign label now overlaps target: wrapping must be refused"
|
|
);
|
|
assert_eq!(guard_probe, expected);
|
|
let actual = c.scene_with_profile(&state, profile);
|
|
assert!(actual.label_layout.is_none());
|
|
for (edge, before) in &core.edges {
|
|
assert_eq!(
|
|
actual.routed.edges[edge].points, before.points,
|
|
"Label drag must not reroute"
|
|
);
|
|
if *edge != foreign.id && *edge != edge_id() {
|
|
assert_eq!(
|
|
actual.routed.edges[edge].label, before.label,
|
|
"Other labels remain frozen"
|
|
);
|
|
}
|
|
}
|
|
assert_eq!(
|
|
actual.routed.edges[&edge_id()].label,
|
|
core.edges[&edge_id()].label,
|
|
"No Layout means original complete one-line target geometry, never stale wrapped hit bounds"
|
|
);
|
|
assert_eq!(
|
|
*actual.routed, expected,
|
|
"Fallback is the complete core preview result"
|
|
);
|
|
}
|
|
}
|
|
#[test]
|
|
fn scene_and_measurement_cache_reuse_and_geometry_invalidation() {
|
|
let (p, id) = fixture();
|
|
let c = canvas(&p, id);
|
|
let mut state = State::default();
|
|
let first = c.scene_with_profile(&state, Profile::F);
|
|
state.pointer = Some(Point::new(100., 200.));
|
|
let changed_camera = DiagramCanvas {
|
|
pan: Vector::new(40., 30.),
|
|
zoom: 0.5,
|
|
selected: p.id,
|
|
..c
|
|
};
|
|
assert!(Arc::ptr_eq(
|
|
&first,
|
|
&changed_camera.scene_with_profile(&state, Profile::F)
|
|
));
|
|
assert!(Arc::ptr_eq(
|
|
&measured(Profile::F).unwrap(),
|
|
&measured(Profile::F).unwrap()
|
|
));
|
|
let other_profile = c.scene_with_profile(&state, Profile::E);
|
|
assert!(!Arc::ptr_eq(&first, &other_profile));
|
|
let next_revision = DiagramCanvas {
|
|
context: Context {
|
|
revision: 1,
|
|
..c.context
|
|
},
|
|
..c
|
|
};
|
|
assert!(!Arc::ptr_eq(
|
|
&other_profile,
|
|
&next_revision.scene_with_profile(&state, Profile::E)
|
|
));
|
|
let plain = c.scene_with_profile(&state, Profile::Baseline);
|
|
assert!(plain.label_layout.is_none());
|
|
assert!(Arc::ptr_eq(&plain.core_routed, &plain.routed));
|
|
assert_eq!(
|
|
*plain.routed,
|
|
route_diagram(&p, &p.diagrams[&id], &plain.view)
|
|
);
|
|
}
|
|
}
|