Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/engine/src/cmd/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ pub fn commands() -> Vec<C> {
l.transparency = st.transparency;
if relock {
s.map_mut().label_lock = None;
s.map_mut().label_lock_line_anchors.clear();
}
Ok(Value::Null)
})
Expand Down
5 changes: 4 additions & 1 deletion crates/engine/src/cmd/labels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ pub(crate) fn with_labels(s: &mut Session, p: &Value, f: impl FnOnce(&mut Labeli
l.labels = labels;
if relock {
s.map_mut().label_lock = None;
s.map_mut().label_lock_line_anchors.clear();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
Ok(Value::Null)
}
Expand All @@ -358,7 +359,9 @@ pub(crate) fn lock_holds(a: &Labeling, b: &Labeling) -> bool {
/// so another position, or trying the others turned on or off, locks the labels again; with no
/// class trying them a new position keeps the lock, as before.
fn same_places(a: &Labeling, b: &Labeling) -> bool {
let classes = |l: &Labeling| l.classes.iter().chain([&l.default]).map(|c| (c.placement, c.other_positions)).collect::<Vec<_>>();
let classes = |l: &Labeling| {
l.classes.iter().chain([&l.default]).map(|c| (c.placement, c.other_positions, c.repeat.is_some())).collect::<Vec<_>>()
};
classes(a).into_iter().zip(classes(b)).all(|(x, y)| x == y || !(x.1 || y.1))
}

Expand Down
1 change: 1 addition & 0 deletions crates/engine/src/cmd/navigate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ fn duplicate_map(s: &mut Session, p: &Value) -> CmdResult {
let mut m = s.project.maps.get(i).ok_or("no map")?.clone();
m.name = super::copy_name(ps(p, "name"), &m.name, |n| s.project.maps.iter().any(|x| x.name.eq_ignore_ascii_case(n)));
m.label_lock = None;
m.label_lock_line_anchors.clear();
m.renew_instance_id();
for l in &mut m.layers {
l.renew_instance_id();
Expand Down
10 changes: 10 additions & 0 deletions crates/engine/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3174,6 +3174,16 @@ fn a_change_to_where_point_labels_go_locks_them_again() {
assert!(s.map().label_lock.is_none(), "a style putting the labels elsewhere: locked again");
}

#[test]
fn changing_line_repeat_relocks_alternative_positions() {
let mut s = world();
let id = s.map().layer_by_name("Rivers").unwrap().id;
s.map_mut().layer_mut(id).unwrap().labels.default.other_positions = true;
s.map_mut().label_lock = Some(vec![((id, 0, 0, 0), 1)]);
s.run("labels.placement", &json!({"layer": "Rivers", "repeat": 144})).unwrap();
assert!(s.map().label_lock.is_none());
}

/// A lock names each label by its feature's id, not its row: deleting a feature (the rows after
/// it move up) neither shows a label left out when the lock was taken nor hides one placed (#1046).
#[test]
Expand Down
10 changes: 5 additions & 5 deletions crates/map/src/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,10 +360,10 @@ impl Camera {
let z_far = self.unproject(w / 2.0, 0.0, w, h).map_or(t.d * FAR, |(_, v)| z_of(v)).min(t.d * FAR);
let mut bands = Vec::new();
let mut z0 = z_near;
while z0 < z_far * 0.999 && bands.len() < MAX_BANDS {
while z0 < z_far * 0.999 && bands.len() < MAX_TILT_BANDS {
let mut z1 = (z0 * 2.0).min(z_far);
// The last band reaches the view's far end, whatever is left.
if z_far / z1 < 1.25 || bands.len() + 1 == MAX_BANDS {
if z_far / z1 < 1.25 || bands.len() + 1 == MAX_TILT_BANDS {
z1 = z_far;
}
// Ground px per band px: the scale at the band's near edge.
Expand Down Expand Up @@ -485,8 +485,8 @@ impl GroundLookup {
}
}

/// The most bands a tilted view is cut into.
const MAX_BANDS: usize = 8;
/// The most flat bands a tilted view is cut into.
pub const MAX_TILT_BANDS: usize = 8;

/// The most area a band of a tilted view is drawn with, as a multiple of the screen area it
/// covers.
Expand Down Expand Up @@ -712,7 +712,7 @@ mod tests {
c.set_pitch(70.0);
for (w, h) in [(1.0, 1.0), (0.0, 0.0), (3.0, 1e-9), (800.0, 2.0)] {
let bands = c.tilt_bands(w, h);
assert!(!bands.is_empty() && bands.len() <= MAX_BANDS, "{w}x{h}");
assert!(!bands.is_empty() && bands.len() <= MAX_TILT_BANDS, "{w}x{h}");
}
// A flat view is one band: itself.
c.set_pitch(0.0);
Expand Down
4 changes: 4 additions & 0 deletions crates/map/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1230,6 +1230,9 @@ pub struct Map {
/// the map moves. Filled by the renderer.
#[serde(skip)]
pub label_lock: Option<Vec<(LabelKey, u8)>>,
/// Geographic anchors of locked non-repeated line labels. Render cache only, never saved.
#[serde(skip)]
pub label_lock_line_anchors: std::collections::HashMap<LabelKey, Coord>,
/// Relationships, snapping, topology rules and edit settings.
#[serde(default)]
pub editing: design::MapEditing,
Expand Down Expand Up @@ -1355,6 +1358,7 @@ impl Map {
popup_mode: PopupMode::Topmost,
label_display: LabelDisplay::default(),
label_lock: None,
label_lock_line_anchors: Default::default(),
editing: design::MapEditing::default(),
swipe: None,
view: view::ViewSettings::default(),
Expand Down
7 changes: 4 additions & 3 deletions crates/map/src/symbology.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2044,8 +2044,9 @@ pub struct LabelClass {
/// Where a label sits relative to its feature.
#[serde(default)]
pub placement: LabelPlacement,
/// A point's label taken by another tries the other places round the point
/// ([`LabelPlacement::alternatives`]) before it is left out (#1011). Off by default: placed
/// A label taken by another tries nearby positions before it is left out (#1011):
/// round a point ([`LabelPlacement::alternatives`]), or along a non-repeated line.
/// Off by default: placed
/// elsewhere, a label may take the place another has today.
#[serde(default, deserialize_with = "de_flag", skip_serializing_if = "std::ops::Not::not")]
pub other_positions: bool,
Expand Down Expand Up @@ -2470,7 +2471,7 @@ impl LabelPlacement {

/// The places round a point a label tries after this one, when [`LabelClass::other_positions`]
/// is on: in QGIS's cartographic order (its `DEFAULT_PLACEMENT_ORDER`), corners first, this
/// one left out.
/// one left out. Line alternatives are generated along their geometry by the renderer.
pub fn alternatives(self) -> impl Iterator<Item = LabelPlacement> {
[
LabelPlacement::AboveRight,
Expand Down
136 changes: 136 additions & 0 deletions crates/render/src/label_places.rs
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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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). place() runs LabelPlace::transform for each Change::Project. transform (crates/render/src/lib.rs:1902-1925) checks every glyph with at and turned_angle (about four at calls per glyph), then turn_with projects them again. For a 20-glyph label that is roughly 100+ projections per attempt, and up to 8 attempts per label. In a tilted view with many colliding curved labels, a frame can reach millions of projections, and the 160,000-attempt budget does not limit that cost.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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;
}
}
Comment thread
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) };
}
3 changes: 3 additions & 0 deletions crates/render/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,7 @@ pub fn frame_coverage(map: &mut Map, center: Coord, scale: f64, w_mm: f64, h_mm:
tiles: Vec<rustgis_map::TileLayer>,
graticule: bool,
lock: Option<Vec<(rustgis_map::LabelKey, u8)>>,
anchors: std::collections::HashMap<rustgis_map::LabelKey, Coord>,
map: &'a mut Map,
}
impl Drop for Aside<'_> {
Expand All @@ -1482,13 +1483,15 @@ pub fn frame_coverage(map: &mut Map, center: Coord, scale: f64, w_mm: f64, h_mm:
self.map.tiles = std::mem::take(&mut self.tiles);
self.map.graticule = self.graticule;
self.map.label_lock = self.lock.take();
self.map.label_lock_line_anchors = std::mem::take(&mut self.anchors);
}
}
let aside = Aside {
basemap: std::mem::replace(&mut map.basemap, rustgis_map::Basemap::Blank),
tiles: std::mem::take(&mut map.tiles),
graticule: std::mem::replace(&mut map.graticule, false),
lock: map.label_lock.take(),
anchors: std::mem::take(&mut map.label_lock_line_anchors),
map,
};
let pm = crate::render(&mut *aside.map, &view)?.pixmap;
Expand Down
Loading
Loading