1use egui::{Color32, Painter, Pos2, Stroke, pos2};
8
9use crate::look::EffectsPolicy;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum EdgeStyle {
14 Straight,
15 Spline,
17 Soft,
19}
20
21pub fn spline_points(p0: Pos2, p3: Pos2, curvature: f32, n: usize) -> Vec<Pos2> {
24 let n = n.max(1);
25 let chord = p3 - p0;
26 let perp = egui::vec2(-chord.y, chord.x).normalized() * chord.length() * curvature;
28 let c1 = p0 + chord * (1.0 / 3.0) + perp;
29 let c2 = p0 + chord * (2.0 / 3.0) + perp;
30 (0..=n)
31 .map(|i| {
32 let t = i as f32 / n as f32;
33 cubic(p0, c1, c2, p3, t)
34 })
35 .collect()
36}
37
38fn cubic(p0: Pos2, c1: Pos2, c2: Pos2, p3: Pos2, t: f32) -> Pos2 {
39 let u = 1.0 - t;
40 let w0 = u * u * u;
41 let w1 = 3.0 * u * u * t;
42 let w2 = 3.0 * u * t * t;
43 let w3 = t * t * t;
44 pos2(
45 w0 * p0.x + w1 * c1.x + w2 * c2.x + w3 * p3.x,
46 w0 * p0.y + w1 * c1.y + w2 * c2.y + w3 * p3.y,
47 )
48}
49
50#[allow(clippy::too_many_arguments)] pub fn draw_edge(
56 painter: &Painter,
57 p0: Pos2,
58 p3: Pos2,
59 style: EdgeStyle,
60 color: Color32,
61 width: f32,
62 curvature: f32,
63 glow: Color32,
64 effects: EffectsPolicy,
65) {
66 let crisp = !effects.allows_decorative_motion(); match style {
68 EdgeStyle::Straight => {
69 painter.line_segment([p0, p3], Stroke::new(width, color));
70 }
71 EdgeStyle::Spline => {
72 let pts = spline_points(p0, p3, curvature, 24);
73 for w in pts.windows(2) {
74 painter.line_segment([w[0], w[1]], Stroke::new(width, color));
75 }
76 }
77 EdgeStyle::Soft => {
78 let pts = spline_points(p0, p3, curvature, 24);
79 if crisp {
80 for w in pts.windows(2) {
82 painter.line_segment([w[0], w[1]], Stroke::new(width.max(1.0), color));
83 }
84 return;
85 }
86 let glow_c = Color32::from_rgba_unmultiplied(glow.r(), glow.g(), glow.b(), 40);
88 for w in pts.windows(2) {
89 painter.line_segment([w[0], w[1]], Stroke::new(width * 3.0, glow_c));
90 }
91 for layer in 0..3 {
92 let f = layer as f32 / 3.0;
93 let a = (180.0 * (1.0 - f)) as u8;
94 let c = Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), a);
95 let lw = width * (1.0 + f * 1.5);
96 for w in pts.windows(2) {
97 painter.line_segment([w[0], w[1]], Stroke::new(lw, c));
98 }
99 }
100 }
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use egui::pos2;
107
108 use super::*;
109
110 #[test]
111 fn spline_starts_and_ends_at_the_endpoints() {
112 let p0 = pos2(10.0, 10.0);
113 let p3 = pos2(110.0, 60.0);
114 let pts = spline_points(p0, p3, 0.3, 16);
115 assert_eq!(pts.len(), 17);
116 assert!((pts[0] - p0).length() < 1e-3, "starts at p0");
117 assert!((*pts.last().unwrap() - p3).length() < 1e-3, "ends at p3");
118 }
119
120 #[test]
121 fn curvature_lifts_the_midpoint_off_the_chord() {
122 let p0 = pos2(0.0, 0.0);
123 let p3 = pos2(100.0, 0.0);
124 let straight_mid = pos2(50.0, 0.0);
125 let pts = spline_points(p0, p3, 0.4, 16);
126 let mid = pts[pts.len() / 2];
127 assert!((mid - straight_mid).length() > 5.0, "a curved spline bows away from the chord");
128 }
129
130 #[test]
131 fn zero_curvature_is_essentially_straight() {
132 let p0 = pos2(0.0, 0.0);
133 let p3 = pos2(100.0, 0.0);
134 let pts = spline_points(p0, p3, 0.0, 16);
135 let mid = pts[pts.len() / 2];
136 assert!((mid - pos2(50.0, 0.0)).length() < 1e-2, "no curvature → straight");
137 }
138
139 #[test]
140 #[allow(deprecated)]
141 fn soft_edge_renders_without_glow_on_device() {
142 let count = |effects: EffectsPolicy| {
145 let ctx = egui::Context::default();
146 let out = ctx.run(egui::RawInput::default(), |ctx| {
147 egui::CentralPanel::default().show(ctx, |ui| {
148 let painter = ui.painter().clone();
149 draw_edge(
150 &painter,
151 pos2(50.0, 50.0),
152 pos2(300.0, 200.0),
153 EdgeStyle::Soft,
154 Color32::WHITE,
155 2.0,
156 0.3,
157 Color32::from_rgb(0, 200, 255),
158 effects,
159 );
160 });
161 });
162 ctx.tessellate(out.shapes, out.pixels_per_point)
163 .iter()
164 .map(|p| match &p.primitive {
165 egui::epaint::Primitive::Mesh(m) => m.vertices.len(),
166 _ => 0,
167 })
168 .sum::<usize>()
169 };
170 let full = count(EffectsPolicy::Full);
171 let device = count(EffectsPolicy::None);
172 assert!(device > 0, "Device still draws the edge");
173 assert!(full > device, "Full glow/feather draws more than Device's crisp hairline: full={full} device={device}");
174 }
175}