diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index 41368628..976e58ab 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -146,6 +146,8 @@ impl Pen16<'_> { let v: Vec = 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 = 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 = (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]