fix(annot,ui): stabilize ink width bounds, search translated tool labels, and fix hotbar settings label

This commit is contained in:
archamon committed 2026-11-01 20:58:16 +00:00
1 parent 53c79ca9b0
commit faebcfd264
7 files changed
+131 -36

No files matched your search

+11 -7
View File
@@ -33,6 +33,16 @@ pub(crate) fn grow(r: [f64; 4], by: f64) -> [f64; 4] {
[r[0] - by, r[1] - by, r[2] + by, r[3] + by]
}
/// Tight bounding box of ink strokes, including Bezier control points for strokes with three or
/// more points (matching [`appearance::smooth_segments`]).
pub(crate) fn ink_bounds(strokes: &[Vec<[f64; 2]>]) -> Option<[f64; 4]> {
let controls = strokes.iter().filter(|s| s.len() > 2).flat_map(|s| {
let pts: Vec<(f64, f64)> = s.iter().map(|p| (p[0], p[1])).collect();
appearance::smooth_segments(&pts).into_iter().flat_map(|[a, b, _]| [[a.0, a.1], [b.0, b.1]])
});
bounds(strokes.iter().flatten().copied().chain(controls))
}
pub(crate) fn ending_draws(ending: LineEnding) -> bool {
ending != LineEnding::None
}
@@ -91,13 +101,7 @@ pub(crate) fn rect_for(shape: &Shape, style: &Style) -> Result<[f64; 4], AnnotEr
if strokes.iter().all(Vec::is_empty) || !strokes.iter().flatten().all(|p| finite(p)) {
return Err(bad("drawing (no points)"));
}
// Strokes of three or more points are drawn as curves, which stay within their
// points and control points.
let controls = strokes.iter().filter(|s| s.len() > 2).flat_map(|s| {
let pts: Vec<(f64, f64)> = s.iter().map(|p| (p[0], p[1])).collect();
appearance::smooth_segments(&pts).into_iter().flat_map(|[a, b, _]| [[a.0, a.1], [b.0, b.1]])
});
grow(bounds(strokes.iter().flatten().copied().chain(controls)).unwrap_or_default(), half + 1.0)
grow(ink_bounds(strokes).unwrap_or_default(), half + 1.0)
}
Shape::Polygon { vertices, cloud } => {
let b = bounds(vertices.iter().copied())
+32 -9
View File
@@ -1,5 +1,5 @@
use super::create::{
add_annotation, base_dict, bounds, callout_attach, ending_draws, ending_pair, finite, grow, normalize, num_array, rect_for, rgb,
add_annotation, base_dict, bounds, callout_attach, ending_draws, ending_pair, finite, grow, ink_bounds, normalize, num_array, rect_for, rgb,
};
use super::draw::{draw_appearance, redraw_or_restore, set_appearance};
use super::info::{line_endings_of, text_value};
@@ -529,7 +529,33 @@ fn apply_text_box(doc: &mut Document, r: ObjRef, rect: [f64; 4], meta: &Meta, ne
}
fn border_width_of(d: &Dict) -> f64 {
d.get(b"BS").and_then(|b| b.as_dict()).and_then(|b| b.get(b"W")).and_then(|w| w.as_f64()).unwrap_or(1.0).max(0.0)
appearance::border_width(d)
}
/// Recompute `/Rect` for an `Ink` annotation when its stroke width changes from `old_w` to `w`.
/// Preserves any baseline margin beyond `old_w / 2.0` (at least `1.0` pt) without accumulating
/// padding across repeated width increases and decreases.
fn ink_rect_for_width(d: &Dict, old_w: f64, w: f64) -> Option<[f64; 4]> {
let list = d.get(b"InkList")?.as_array()?;
let mut strokes: Vec<Vec<[f64; 2]>> = Vec::with_capacity(list.len());
for s in list {
let pts: Vec<f64> = s.as_array()?.iter().map(Object::as_f64).collect::<Option<_>>()?;
strokes.push(pts.as_chunks::<2>().0.iter().map(|p| [p[0], p[1]]).collect());
}
let inner = ink_bounds(&strokes)?;
let vals: Vec<f64> = d.get(b"Rect").and_then(Object::as_array).map(|a| a.iter().filter_map(Object::as_f64).collect()).unwrap_or_default();
let Some(old_rect) = four(&vals).map(normalize) else {
return Some(grow(inner, w / 2.0 + 1.0));
};
let old_half = old_w / 2.0;
let new_half = w / 2.0;
let base = [
(inner[0] - old_rect[0] - old_half).max(1.0),
(inner[1] - old_rect[1] - old_half).max(1.0),
(old_rect[2] - inner[2] - old_half).max(1.0),
(old_rect[3] - inner[3] - old_half).max(1.0),
];
Some([inner[0] - base[0] - new_half, inner[1] - base[1] - new_half, inner[2] + base[2] + new_half, inner[3] + base[3] + new_half])
}
/// Change a comment's colour, opacity, line width and/or line endings, and redraw it.
@@ -582,13 +608,10 @@ pub fn set_style(doc: &mut Document, page: usize, index: usize, change: StyleCha
bs.set(b"W".to_vec(), Object::Real(w));
d.set(b"BS".to_vec(), Object::Dict(bs));
d.remove(b"Border");
if d.name(b"Subtype") == Some(b"Ink") && w > old_w {
let vals: Vec<f64> =
d.get(b"Rect").and_then(|o| o.as_array()).map(|a| a.iter().filter_map(|x| x.as_f64()).collect()).unwrap_or_default();
if let Some(rect) = four(&vals) {
let expanded = grow(normalize(rect), (w - old_w) / 2.0);
d.set(b"Rect".to_vec(), num_array(&expanded));
}
if d.name(b"Subtype") == Some(b"Ink")
&& let Some(rect) = ink_rect_for_width(d, old_w, w)
{
d.set(b"Rect".to_vec(), num_array(&rect));
}
}
if let Some(ends) = endings {
+32 -2
View File
@@ -2214,9 +2214,39 @@ fn freehand_highlight_is_compatible_ink_with_marker_blending() {
assert_eq!(blend.name(b"BM"), Some(&b"Multiply"[..]));
let mut doc = doc;
let rect_before: Vec<f64> = list(&doc, 0)[index].get(b"Rect").unwrap().as_array().unwrap().iter().filter_map(Object::as_f64).collect();
let rect_of = |doc: &Document, idx: usize| -> Vec<f64> {
list(doc, 0)[idx].get(b"Rect").unwrap().as_array().unwrap().iter().filter_map(Object::as_f64).collect()
};
let rect_before = rect_of(&doc, index);
set_style(&mut doc, 0, index, StyleChange { color: None, opacity: None, width: Some(34.0), endings: None }, &meta("wider")).unwrap();
let rect_after: Vec<f64> = list(&doc, 0)[index].get(b"Rect").unwrap().as_array().unwrap().iter().filter_map(Object::as_f64).collect();
let rect_after = rect_of(&doc, index);
assert!((rect_after[0] - (rect_before[0] - 10.0)).abs() < 1e-4);
assert!((rect_after[2] - (rect_before[2] + 10.0)).abs() < 1e-4);
// Repeated increase/decrease cycles must keep bounds stable and never accumulate padding.
set_style(&mut doc, 0, index, StyleChange { color: None, opacity: None, width: Some(14.0), endings: None }, &meta("narrower")).unwrap();
let rect_restored = rect_of(&doc, index);
for (a, b) in rect_restored.iter().zip(rect_before.iter()) {
assert!((a - b).abs() < 1e-4, "expected {rect_before:?}, got {rect_restored:?}");
}
set_style(&mut doc, 0, index, StyleChange { color: None, opacity: None, width: Some(34.0), endings: None }, &meta("wider-again")).unwrap();
let rect_second_wide = rect_of(&doc, index);
for (a, b) in rect_second_wide.iter().zip(rect_after.iter()) {
assert!((a - b).abs() < 1e-4, "expected {rect_after:?}, got {rect_second_wide:?}");
}
// Imported ink whose width is specified only by `/Border [0 0 20]` must use 20 pt as its
// previous width (shrinking by 5 pt on a decrease to 10 pt rather than expanding as if 1 pt).
let (_, r) = lookup::annot_ref(&mut doc, 0, index).unwrap();
doc.update_dict(r, |d| {
d.remove(b"BS");
d.set(b"Border".to_vec(), Object::Array(vec![Object::Real(0.0), Object::Real(0.0), Object::Real(20.0)]));
d.set(b"Rect".to_vec(), create::num_array(&[5.0, 85.0, 135.0, 115.0]));
})
.unwrap();
set_style(&mut doc, 0, index, StyleChange { color: None, opacity: None, width: Some(10.0), endings: None }, &meta("border-shrink")).unwrap();
let rect_from_border = rect_of(&doc, index);
for (a, b) in rect_from_border.iter().zip([10.0, 90.0, 130.0, 110.0]) {
assert!((a - b).abs() < 1e-4, "expected [10, 90, 130, 110], got {rect_from_border:?}");
}
}