Merge pull request #913 from pravdomil/claude/selection-tool-glyph-fix-69364a

UI: draw concave icons as convex pieces (fixes Selection arrow, Speaker, Sparkle glyphs)
This commit is contained in:
Brandon Thomas 2026-10-12 04:20:52 -04:00
commit 54d68a90a6
1 changed files with 40 additions and 3 deletions

View File

@ -146,6 +146,8 @@ impl Pen16<'_> {
let v: Vec<Pos2> = pts.iter().map(|(x, y)| self.p(*x, *y)).collect();
self.painter.add(PathShape::closed_line(v, PathStroke::new(self.width * self.s(), self.color)));
}
/// Fill a **convex** outline: the tessellator fans a concave one into slivers across its
/// notches, so a concave glyph is drawn as overlapping convex pieces (see [`SELECTION_ARROW`]).
fn fill(&self, pts: &[(f32, f32)]) {
let v: Vec<Pos2> = pts.iter().map(|(x, y)| self.p(*x, *y)).collect();
self.painter.add(PathShape::convex_polygon(v, self.color, Stroke::NONE));
@ -205,6 +207,25 @@ impl Pen16<'_> {
}
}
/// The convex pieces of each concave filled glyph. Pieces overlap rather than abut: abutting
/// antialiased edges would leave a faint seam.
type Pieces = &'static [&'static [(f32, f32)]];
/// The Selection tool's arrow pointer (tip upper left, a notch each side of the tail): the head
/// with its left wing, the right wing, and the tail. Only the head reaches the tip: slivers
/// meeting there would antialias into a spike past it.
const SELECTION_ARROW: Pieces = &[
&[(4.5, 2.0), (4.5, 13.0), (7.2, 10.5), (10.1, 7.39)],
&[(7.5, 9.0), (8.9, 9.9), (12.5, 9.7), (10.1, 7.39)],
&[(7.8, 8.6), (7.2, 10.5), (9.2, 14.6), (10.8, 13.9), (8.9, 9.9)],
];
/// A loudspeaker: the magnet box, running a little into the cone.
const SPEAKER: Pieces = &[&[(2.0, 6.0), (5.5, 6.0), (5.5, 10.0), (2.0, 10.0)], &[(5.0, 6.0), (9.0, 2.5), (9.0, 13.5), (5.0, 10.0)]];
/// A four-pointed star: an upright and a level diamond, whose edges cross at the inner corners.
const SPARKLE: Pieces = &[&[(8.0, 1.5), (9.784, 8.0), (8.0, 14.5), (6.216, 8.0)], &[(1.5, 8.0), (8.0, 6.216), (14.5, 8.0), (8.0, 9.784)]];
#[cfg(test)]
const CONCAVE: [Pieces; 3] = [SELECTION_ARROW, SPEAKER, SPARKLE];
/// Paint `icon` centred in `rect`.
pub fn paint(painter: &Painter, rect: Rect, icon: Icon, color: Color32) {
let pen = Pen16 { painter, rect, color, width: 1.25 };
@ -214,7 +235,7 @@ pub fn paint(painter: &Painter, rect: Rect, icon: Icon, color: Color32) {
pen.closed(&[(2.5, 7.5), (8.0, 2.5), (13.5, 7.5), (13.5, 13.5), (2.5, 13.5)]);
pen.rect(6.5, 9.5, 9.5, 13.5);
}
Selection => pen.fill(&[(4.5, 2.0), (4.5, 13.0), (7.2, 10.5), (9.2, 14.6), (10.8, 13.9), (8.9, 9.9), (12.5, 9.7)]),
Selection => SELECTION_ARROW.iter().for_each(|p| pen.fill(p)),
Hand => {
pen.line(&[(5.0, 9.0), (5.0, 4.0)]);
pen.line(&[(7.3, 8.0), (7.3, 2.6)]);
@ -358,7 +379,7 @@ pub fn paint(painter: &Painter, rect: Rect, icon: Icon, color: Color32) {
pen.dot(8.0, 8.0, 2.2);
}
Speaker => {
pen.fill(&[(2.0, 6.0), (5.0, 6.0), (9.0, 2.5), (9.0, 13.5), (5.0, 10.0), (2.0, 10.0)]);
SPEAKER.iter().for_each(|p| pen.fill(p));
pen.arc(9.0, 8.0, 3.0, -50.0, 50.0);
pen.arc(9.0, 8.0, 5.5, -50.0, 50.0);
}
@ -634,7 +655,7 @@ pub fn paint(painter: &Painter, rect: Rect, icon: Icon, color: Color32) {
pen.line(&[(9.4, 2.8), (6.6, 13.2)]);
}
Sparkle => {
pen.fill(&[(8.0, 1.5), (9.4, 6.6), (14.5, 8.0), (9.4, 9.4), (8.0, 14.5), (6.6, 9.4), (1.5, 8.0), (6.6, 6.6)]);
SPARKLE.iter().for_each(|p| pen.fill(p));
}
Monitor => {
pen.rrect(1.8, 2.5, 14.2, 11.0, 1.2);
@ -740,6 +761,22 @@ fn keyframe_shapes(
mod tests {
use effectcraft_engine::keyframe::KeyHalf as H;
/// The Selection arrow (and the Speaker and Sparkle) are concave; filled as one polygon they
/// rendered as a broken fan. Each is drawn as convex pieces instead.
#[test]
fn concave_glyphs_are_filled_as_convex_pieces() {
for piece in super::CONCAVE.iter().flat_map(|g| g.iter()) {
let n = piece.len();
let turns: Vec<f32> = (0..n)
.map(|i| {
let (a, b, c) = (piece[i], piece[(i + 1) % n], piece[(i + 2) % n]);
(b.0 - a.0) * (c.1 - b.1) - (b.1 - a.1) * (c.0 - b.0)
})
.collect();
assert!(turns.iter().all(|t| *t < 0.0) || turns.iter().all(|t| *t > 0.0), "{piece:?} is not convex: {turns:?}");
}
}
/// #284: keyframes have no dark border (overlapping keys looked messy): each half is filled
/// and edged in the key's own colour.
#[test]