Fix #909: a Width tool profile's preview fits the Profile menu

The Profile menu's preview drew a stroke's width profile assuming its
widths stay within the stroke weight. A profile made with the Width tool
can be many times wider, so its silhouette spilled over the Stroke panel.
It is now scaled to the preview by its widest point and clipped to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic 2026-10-10 13:30:18 +07:00
parent 71003386ac
commit 8d2a3f48a1
1 changed files with 38 additions and 8 deletions

View File

@ -378,18 +378,21 @@ fn arrow_dropdown(ui: &mut Ui, id: &str, cur: Option<Arrowhead>, start: bool) ->
out
}
/// Draw a width profile's silhouette (its points; None: the plain bar) into `r`.
/// Draw a width profile's silhouette (its points; None: the plain bar) into `r`. A profile made
/// with the Width tool can be many times the stroke's weight: the silhouette is scaled to fit `r`
/// by its widest point, and clipped to it (#909).
pub fn paint_profile(ui: &Ui, r: Rect, points: Option<&[(f64, f64, f64)]>, color: Color32) {
let n = 32;
let half = r.height() / 2.0 - 1.0;
let widths: Vec<(f64, f64)> = (0..=n).map(|i| points.map_or((0.35, 0.35), |p| WidthProfile::at_points(p, i as f64 / n as f64))).collect();
let widest = widths.iter().map(|(l, rr)| l.max(*rr)).filter(|w| w.is_finite()).fold(1.0, f64::max);
let mut top = vec![];
let mut bot = vec![];
for i in 0..=n {
let tt = i as f64 / n as f64;
let (l, rr) = points.map_or((0.35, 0.35), |p| WidthProfile::at_points(p, tt));
let x = r.left() + tt as f32 * r.width();
top.push(pos2(x, r.center().y - l as f32 * half));
bot.push(pos2(x, r.center().y + rr as f32 * half));
for (i, (l, rr)) in widths.iter().enumerate() {
let x = r.left() + i as f32 / n as f32 * r.width();
let side = |w: f64| if w.is_finite() { (w / widest).clamp(0.0, 1.0) as f32 * half } else { 0.0 };
top.push(pos2(x, r.center().y - side(*l)));
bot.push(pos2(x, r.center().y + side(*rr)));
}
let mut mesh = egui::Mesh::default();
for i in 0..=n {
@ -401,7 +404,7 @@ pub fn paint_profile(ui: &Ui, r: Rect, points: Option<&[(f64, f64, f64)]>, color
mesh.add_triangle(a, a + 1, a + 2);
mesh.add_triangle(a + 1, a + 3, a + 2);
}
ui.painter().add(egui::Shape::mesh(mesh));
ui.painter().with_clip_rect(r).add(egui::Shape::mesh(mesh));
}
/// The Profile dropdown (Stroke panel, Control bar): the stroke's own silhouette on the button
@ -490,6 +493,33 @@ mod tests {
assert!(dash_pattern(&[Some(0.0), Some(0.0), None, None, None, None]).is_empty());
}
/// #909: a profile many times the stroke's weight (made with the Width tool) draws inside its
/// box, scaled by its widest point, and the plain bar keeps its look.
#[test]
fn wide_profiles_fit_their_preview() {
let ctx = egui::Context::default();
let r = Rect::from_min_size(pos2(10.0, 10.0), egui::vec2(80.0, 16.0));
let drawn = |points: Option<&[(f64, f64, f64)]>| {
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| paint_profile(ui, r, points, Color32::WHITE));
out.textures_delta.clear();
out.shapes
.into_iter()
.filter_map(|c| match c.shape {
egui::Shape::Mesh(m) => Some((c.clip_rect, m.vertices.iter().map(|v| v.pos).collect::<Vec<_>>())),
_ => None,
})
.next()
.unwrap()
};
let (clip, wide) = drawn(Some(&[(0.0, 1.0, 1.0), (0.5, 10.0, 10.0), (1.0, 1.0, 1.0)]));
assert!(clip.min.y >= r.min.y - 0.5 && clip.max.y <= r.max.y + 0.5, "{clip:?}");
assert!(wide.iter().all(|p| p.y >= r.top() && p.y <= r.bottom()), "{wide:?}");
let middle = wide.iter().map(|p| (p.y - r.center().y).abs()).fold(0.0, f32::max);
assert!((middle - (r.height() / 2.0 - 1.0)).abs() < 1e-3, "the widest point fills the box ({middle})");
let (_, plain) = drawn(None);
assert!(plain.iter().all(|p| (p.y - r.center().y).abs() <= 0.35 * (r.height() / 2.0 - 1.0) + 1e-3));
}
#[test]
fn profile_silhouettes_and_arrow_labels() {
assert!(silhouette(WidthProfile::PRESETS[0].points).is_none(), "uniform draws the plain bar");