Experiment with measured multiline label layout on bounded witness

This commit is contained in:
2026-10-01 19:21:24 -07:00
parent 6ea7518e7b
commit d8ccd6ed49
13 changed files with 5590 additions and 20 deletions
+7 -1
View File
@@ -5417,11 +5417,17 @@ impl App {
self.pan = iced::Vector::new(30., 30.);
return;
}
let geometry = oa_core::routing::route_diagram(
let mut geometry = oa_core::routing::route_diagram(
self.project(),
&self.project().diagrams[&self.active_diagram.unwrap()],
&view,
);
crate::diagram::apply_label_experiment(
self.project(),
&self.project().diagrams[&self.active_diagram.unwrap()],
&view,
&mut geometry,
);
let left = geometry.bounds.position.x;
let top = geometry.bounds.position.y;
let right = left + geometry.bounds.size.width;
+101 -14
View File
@@ -37,6 +37,9 @@ mod port_hit_tests;
#[path = "diagram/view_options_tests.rs"]
mod view_options_tests;
#[path = "diagram/label_layout_experiment.rs"]
mod label_layout_experiment;
#[path = "diagram/port_labels.rs"]
mod port_labels;
pub use port_labels::PortLabels;
@@ -88,6 +91,9 @@ pub struct State {
cache: RefCell<Option<CachedScene>>,
}
struct Scene {
label_layout: Option<label_layout_experiment::Layout>,
profile: label_layout_experiment::Profile,
font_version: Option<iced::advanced::graphics::text::Version>,
work: Option<Arc<crate::app::allocation_drag::Preview>>,
view: ProjectedView,
routed: Arc<RoutedView>,
@@ -514,6 +520,14 @@ impl DiagramCanvas<'_> {
}
}
fn scene(&self, state: &State) -> Arc<Scene> {
self.scene_with_profile(state, label_layout_experiment::profile())
}
fn scene_with_profile(
&self,
state: &State,
profile: label_layout_experiment::Profile,
) -> Arc<Scene> {
let font_version = label_layout_experiment::font_version(profile);
if let Some(preview) = self.work_preview()
&& state
.work
@@ -522,13 +536,30 @@ impl DiagramCanvas<'_> {
{
if let Some(scene) = state.work_cache.borrow().as_ref()
&& scene.work.as_ref().is_some_and(|p| Arc::ptr_eq(p, preview))
&& scene.profile == profile
&& scene.font_version == font_version
{
return scene.clone();
}
let mut routed = preview.routed.clone();
let label_layout = if profile == label_layout_experiment::Profile::Baseline {
None
} else {
label_layout_experiment::apply(
preview.project(),
&preview.placement.diagram,
&preview.placement.view,
Arc::make_mut(&mut routed),
profile,
)
};
let scene = Arc::new(Scene {
label_layout,
profile,
font_version,
work: Some(preview.clone()),
view: preview.placement.view.clone(),
routed: preview.routed.clone(),
routed,
});
*state.work_cache.borrow_mut() = Some(scene.clone());
return scene;
@@ -554,6 +585,8 @@ impl DiagramCanvas<'_> {
&& cache.preview == preview
&& cache.route == route
&& cache.port == port
&& cache.scene.profile == profile
&& cache.scene.font_version == font_version
{
return cache.scene.clone();
}
@@ -563,7 +596,7 @@ impl DiagramCanvas<'_> {
self.project.diagrams[&self.diagram].clone()
};
let view = self.projected(&diagram);
let routed = Arc::new(
let mut routed = Arc::new(
if let Some(gesture) = state
.route_drag
.as_ref()
@@ -580,7 +613,21 @@ impl DiagramCanvas<'_> {
route_diagram(self.project, &diagram, &view)
},
);
let label_layout = if profile == label_layout_experiment::Profile::Baseline {
None
} else {
label_layout_experiment::apply(
self.project,
&diagram,
&view,
Arc::make_mut(&mut routed),
profile,
)
};
let scene = Arc::new(Scene {
label_layout,
profile,
font_version,
work: None,
view,
routed,
@@ -1355,16 +1402,27 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
Size::new(label.bounds.size.width, label.bounds.size.height),
Color::WHITE,
);
f.fill_text(Text {
content: label.text.clone(),
position: point(label.center),
color: if active { style::TEAL } else { style::MUTED },
size: 13.into(),
align_x: iced::widget::text::Alignment::Center,
align_y: iced::alignment::Vertical::Center,
max_width: f32::INFINITY,
..Default::default()
});
if let Some(layout) = scene
.label_layout
.as_ref()
.filter(|_| edge.id == label_layout_experiment::edge_id())
{
for line in layout.canvas_lines(if active { style::TEAL } else { style::MUTED })
{
f.fill_text(line);
}
} else {
f.fill_text(Text {
content: label.text.clone(),
position: point(label.center),
color: if active { style::TEAL } else { style::MUTED },
size: 13.into(),
align_x: iced::widget::text::Alignment::Center,
align_y: iced::alignment::Vertical::Center,
max_width: f32::INFINITY,
..Default::default()
});
}
}
if let Some(relation) = project.relations.get(&edge.id) {
for end in routing::association_end_geometry(relation, routed) {
@@ -1943,7 +2001,30 @@ impl std::fmt::Display for ExportError {
}
impl std::error::Error for ExportError {}
/// Private opt-in trial adapter for consumers that already own routed geometry.
pub(crate) fn apply_label_experiment(
p: &Project,
diagram: &Diagram,
view: &ProjectedView,
routed: &mut RoutedView,
) {
let _ = label_layout_experiment::apply(
p,
diagram,
view,
routed,
label_layout_experiment::profile(),
);
}
pub fn export_svg(p: &Project, diagram: Id) -> Result<String, ExportError> {
export_svg_with_profile(p, diagram, label_layout_experiment::profile())
}
fn export_svg_with_profile(
p: &Project,
diagram: Id,
profile: label_layout_experiment::Profile,
) -> Result<String, ExportError> {
let d = p
.diagrams
.get(&diagram)
@@ -1952,7 +2033,8 @@ pub fn export_svg(p: &Project, diagram: Id) -> Result<String, ExportError> {
return Err(ExportError::UnsupportedScenario(diagram));
}
let view = project_view(p, diagram);
let routed = route_diagram(p, d, &view);
let mut routed = route_diagram(p, d, &view);
let label_layout = label_layout_experiment::apply(p, d, &view, &mut routed, profile);
let left = routed.bounds.position.x - 30.;
let top = routed.bounds.position.y - 30.;
let right = routed.bounds.position.x + routed.bounds.size.width + 30.;
@@ -2029,7 +2111,12 @@ pub fn export_svg(p: &Project, diagram: Id) -> Result<String, ExportError> {
.join(" ");
svg.push_str(&format!("<path data-edge=\"{}\" d=\"{path}\" fill=\"none\" stroke=\"#8999a9\" stroke-width=\"1.5\"{dashed}{arrow}><title>{}</title></path>",edge.id,escape(p.named(edge.id))));
let label = &route.label;
if !label.text.is_empty() {
if let Some(layout) = label_layout
.as_ref()
.filter(|_| edge.id == label_layout_experiment::edge_id())
{
svg.push_str(&layout.svg());
} else if !label.text.is_empty() {
svg.push_str(&format!("<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" fill=\"white\"/><text x=\"{}\" y=\"{}\" text-anchor=\"middle\" dominant-baseline=\"central\" font-family=\"sans-serif\" font-size=\"13\" fill=\"#637487\">{}</text>",label.bounds.position.x,label.bounds.position.y,label.bounds.size.width,label.bounds.size.height,label.center.x,label.center.y,escape(&label.text)));
}
if let Some(relation) = p.relations.get(&edge.id) {
@@ -422,6 +422,9 @@ fn coincident_attachment_handles_honor_explicit_end_choice() {
)));
let c = canvas(&app, 1.);
let mut scene = Scene {
label_layout: None,
profile: label_layout_experiment::Profile::Baseline,
font_version: None,
work: None,
view: projection::project_view(c.project, c.diagram),
routed: Arc::new(route_diagram(
@@ -0,0 +1,676 @@
//! 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(),
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.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 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_eq!(
*plain.routed,
route_diagram(&p, &p.diagrams[&id], &plain.view)
);
}
}
+2 -1
View File
@@ -6,7 +6,8 @@ impl DiagramCanvas<'_> {
pub(crate) fn work_dirty_point(&self, bounds: Rectangle, screen: Point) -> bool {
let diagram = &self.project.diagrams[&self.diagram];
let view = self.projected(diagram);
let routed = routing::route_diagram(self.project, diagram, &view);
let mut routed = routing::route_diagram(self.project, diagram, &view);
super::apply_label_experiment(self.project, diagram, &view, &mut routed);
self.work_dirty_press(
&view,
&routed,
+12 -4
View File
@@ -327,7 +327,11 @@ pub(crate) fn saved_usages_for(p: &Project, subject: Id, only_diagram: Option<Id
"state machine view"
}
.into(),
bounds: routed_bounds(routing::route_diagram(p, diagram, &view).bounds),
bounds: {
let mut routed = routing::route_diagram(p, diagram, &view);
crate::diagram::apply_label_experiment(p, diagram, &view, &mut routed);
routed_bounds(routed.bounds)
},
});
}
for node in &view.nodes {
@@ -407,9 +411,13 @@ pub(crate) fn saved_usages_for(p: &Project, subject: Id, only_diagram: Option<Id
}
}
if view.edges.iter().any(|edge| edge.id == subject)
&& let Some(edge) = routing::route_diagram(p, diagram, &view)
.edges
.get(&subject)
&& let Some(edge) = {
let mut routed = routing::route_diagram(p, diagram, &view);
crate::diagram::apply_label_experiment(p, diagram, &view, &mut routed);
routed
}
.edges
.get(&subject)
{
usages.push(Usage {
subject,