Repository navigation
feat(labels): try alternate positions along lines #1213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
aaa526f
0961a7b
04301fb
3ce5731
2075727
aeb25bf
02c78bc
98aa0e6
05e83ed
39310fd
48d66dd
715fcd4
ee1fc63
16f17ff
400936e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| //! Deferred curved placements: transformations are retained until collision placement needs them. | ||
| use super::*; | ||
| use std::sync::Arc; | ||
|
|
||
| type Projection = Arc<dyn Fn(f32, f32) -> Option<(f32, f32)> + Send + Sync>; | ||
|
|
||
| #[derive(Clone)] | ||
| enum Change { | ||
| Project(Projection), | ||
| PlaceShifts(Vec<(f32, f32)>), | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
| pub(super) struct CurveSpec { | ||
| pub line: Arc<line_labels::Line>, | ||
| pub chars: Vec<String>, | ||
| pub advances: Vec<f32>, | ||
| pub gap: f32, | ||
| pub overrun: f32, | ||
| } | ||
|
|
||
| #[derive(Clone)] | ||
| pub(super) struct Curves { | ||
| pub spec: Arc<CurveSpec>, | ||
| pub positions: Vec<f32>, | ||
| pub locked: Option<usize>, | ||
| changes: Vec<Change>, | ||
| } | ||
|
|
||
| impl std::fmt::Debug for Curves { | ||
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| f.debug_struct("Curves").field("positions", &self.positions).field("locked", &self.locked).finish_non_exhaustive() | ||
| } | ||
| } | ||
|
|
||
| impl Curves { | ||
| pub fn new(spec: Arc<CurveSpec>, positions: Vec<f32>, locked: Option<usize>) -> Self { | ||
| Self { spec, positions, locked, changes: Vec::new() } | ||
| } | ||
|
|
||
| pub fn project(&mut self, at: impl Fn(f32, f32) -> Option<(f32, f32)> + Send + Sync + 'static) { | ||
| self.changes.push(Change::Project(Arc::new(at))); | ||
| } | ||
|
|
||
| /// Compute each placement's relief shift at its own transformed anchor, without laying out text. | ||
| pub fn shift_each(&mut self, offset: &impl Fn(f32, f32) -> Option<(f32, f32)>) { | ||
| let shifts = self | ||
| .positions | ||
| .iter() | ||
| .enumerate() | ||
| .map(|(i, d)| { | ||
| let Some((mut x, mut y)) = self.spec.line.anchor(*d) else { return (0.0, 0.0) }; | ||
| for change in &self.changes { | ||
| match change { | ||
| Change::Project(at) => { | ||
| let Some(p) = at(x, y) else { return (0.0, 0.0) }; | ||
| (x, y) = p; | ||
| } | ||
| Change::PlaceShifts(shifts) => { | ||
| if let Some((dx, dy)) = shifts.get(i) { | ||
| x += dx; | ||
| y += dy; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| offset(x, y).unwrap_or((0.0, 0.0)) | ||
| }) | ||
| .collect(); | ||
| self.changes.push(Change::PlaceShifts(shifts)); | ||
| } | ||
|
|
||
| /// The first in-sight preview, with its own line direction projected at its source anchor; | ||
| /// no character layout. Even a too-short stretch has a tangent for View Unplaced. | ||
| pub fn preview_in(&self, template: &mut PlacedLabel, at: &impl Fn(f32, f32) -> Option<(f32, f32)>) -> Option<LabelPlace> { | ||
| let width = line_labels::curved_len(&self.spec.advances, self.spec.gap); | ||
| for (i, d) in self.positions.iter().enumerate() { | ||
| if self.locked.is_some_and(|won| won != i) { | ||
| continue; | ||
| } | ||
| let Some((x, y)) = self.spec.line.anchor(*d) else { continue }; | ||
| let angle = | ||
| self.spec.line.parallel(width, self.spec.overrun, Some(*d)).map(|(_, _, a)| a).or_else(|| self.spec.line.direction(*d)); | ||
| let mut preview = LabelPlace { x, y, angle, glyphs: Vec::new() }; | ||
| if preview.transform(template, at) { | ||
| return Some(preview); | ||
| } | ||
| } | ||
| None | ||
| } | ||
|
|
||
| pub fn place(&self, i: usize, template: &mut PlacedLabel) -> Option<LabelPlace> { | ||
| if self.locked.is_some_and(|won| won != i) { | ||
| return None; | ||
| } | ||
| let at = *self.positions.get(i)?; | ||
| #[cfg(test)] | ||
| LAYOUTS.with(|n| n.set(n.get() + 1)); | ||
| let ((x, y), turns) = self.spec.line.curved(&self.spec.advances, self.spec.gap, self.spec.overrun, Some(at))?; | ||
| // Reuse the preceding rejected place's vector and String capacities. The accepted | ||
| // middle preview retains its own glyphs; later failed attempts can share this pool. | ||
| let mut glyphs = std::mem::take(&mut template.glyphs); | ||
| let mut count = 0; | ||
| for (i, (text, (dx, dy, angle))) in self.spec.chars.iter().zip(turns).enumerate() { | ||
| if let Some(glyph) = glyphs.get_mut(i) { | ||
| glyph.text.clone_from(text); | ||
| (glyph.dx, glyph.dy, glyph.angle) = (dx, dy, angle); | ||
| } else { | ||
| glyphs.push(LabelGlyph { text: text.clone(), dx, dy, angle }); | ||
| } | ||
| count += 1; | ||
| } | ||
| glyphs.truncate(count); | ||
| let mut p = LabelPlace { x, y, angle: None, glyphs }; | ||
| for change in &self.changes { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Performance: the layout budget counts attempts, but each attempt projects every glyph several times (~40% confidence). Options: weight the budget by glyph count, or validate the glyphs once per candidate instead of on every attempt, since the projection is the same for every attempt on a band.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not changing this in this PR: the attempt count is bounded and this is a non-blocking tuning suggestion (unmeasured). Weighting the budget by glyph count or validating glyphs once per band can be a follow-up if profiling shows it matters. |
||
| match change { | ||
| Change::Project(at) => { | ||
| if !p.transform(template, &|x, y| at(x, y)) { | ||
| return None; | ||
| } | ||
| } | ||
|
choupeanut marked this conversation as resolved.
Resolved
|
||
| Change::PlaceShifts(shifts) => { | ||
| if let Some((dx, dy)) = shifts.get(i) { | ||
| (p.x, p.y) = (p.x + dx, p.y + dy); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Some(p) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| thread_local! { | ||
| pub(super) static LAYOUTS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) }; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.