use egui::{Color32, Painter, Pos2, Stroke, pos2};
use crate::look::EffectsPolicy;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EdgeStyle {
Straight,
Spline,
Soft,
}
pub fn spline_points(p0: Pos2, p3: Pos2, curvature: f32, n: usize) -> Vec<Pos2> {
let n = n.max(1);
let chord = p3 - p0;
let perp = egui::vec2(-chord.y, chord.x).normalized() * chord.length() * curvature;
let c1 = p0 + chord * (1.0 / 3.0) + perp;
let c2 = p0 + chord * (2.0 / 3.0) + perp;
(0..=n)
.map(|i| {
let t = i as f32 / n as f32;
cubic(p0, c1, c2, p3, t)
})
.collect()
}
fn cubic(p0: Pos2, c1: Pos2, c2: Pos2, p3: Pos2, t: f32) -> Pos2 {
let u = 1.0 - t;
let w0 = u * u * u;
let w1 = 3.0 * u * u * t;
let w2 = 3.0 * u * t * t;
let w3 = t * t * t;
pos2(
w0 * p0.x + w1 * c1.x + w2 * c2.x + w3 * p3.x,
w0 * p0.y + w1 * c1.y + w2 * c2.y + w3 * p3.y,
)
}
#[allow(clippy::too_many_arguments)] pub fn draw_edge(
painter: &Painter,
p0: Pos2,
p3: Pos2,
style: EdgeStyle,
color: Color32,
width: f32,
curvature: f32,
glow: Color32,
effects: EffectsPolicy,
) {
let crisp = !effects.allows_decorative_motion(); match style {
EdgeStyle::Straight => {
painter.line_segment([p0, p3], Stroke::new(width, color));
}
EdgeStyle::Spline => {
let pts = spline_points(p0, p3, curvature, 24);
for w in pts.windows(2) {
painter.line_segment([w[0], w[1]], Stroke::new(width, color));
}
}
EdgeStyle::Soft => {
let pts = spline_points(p0, p3, curvature, 24);
if crisp {
for w in pts.windows(2) {
painter.line_segment([w[0], w[1]], Stroke::new(width.max(1.0), color));
}
return;
}
let glow_c = Color32::from_rgba_unmultiplied(glow.r(), glow.g(), glow.b(), 40);
for w in pts.windows(2) {
painter.line_segment([w[0], w[1]], Stroke::new(width * 3.0, glow_c));
}
for layer in 0..3 {
let f = layer as f32 / 3.0;
let a = (180.0 * (1.0 - f)) as u8;
let c = Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), a);
let lw = width * (1.0 + f * 1.5);
for w in pts.windows(2) {
painter.line_segment([w[0], w[1]], Stroke::new(lw, c));
}
}
}
}
}
#[cfg(test)]
mod tests {
use egui::pos2;
use super::*;
#[test]
fn spline_starts_and_ends_at_the_endpoints() {
let p0 = pos2(10.0, 10.0);
let p3 = pos2(110.0, 60.0);
let pts = spline_points(p0, p3, 0.3, 16);
assert_eq!(pts.len(), 17);
assert!((pts[0] - p0).length() < 1e-3, "starts at p0");
assert!((*pts.last().unwrap() - p3).length() < 1e-3, "ends at p3");
}
#[test]
fn curvature_lifts_the_midpoint_off_the_chord() {
let p0 = pos2(0.0, 0.0);
let p3 = pos2(100.0, 0.0);
let straight_mid = pos2(50.0, 0.0);
let pts = spline_points(p0, p3, 0.4, 16);
let mid = pts[pts.len() / 2];
assert!((mid - straight_mid).length() > 5.0, "a curved spline bows away from the chord");
}
#[test]
fn zero_curvature_is_essentially_straight() {
let p0 = pos2(0.0, 0.0);
let p3 = pos2(100.0, 0.0);
let pts = spline_points(p0, p3, 0.0, 16);
let mid = pts[pts.len() / 2];
assert!((mid - pos2(50.0, 0.0)).length() < 1e-2, "no curvature → straight");
}
#[test]
#[allow(deprecated)]
fn soft_edge_renders_without_glow_on_device() {
let count = |effects: EffectsPolicy| {
let ctx = egui::Context::default();
let out = ctx.run(egui::RawInput::default(), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
let painter = ui.painter().clone();
draw_edge(
&painter,
pos2(50.0, 50.0),
pos2(300.0, 200.0),
EdgeStyle::Soft,
Color32::WHITE,
2.0,
0.3,
Color32::from_rgb(0, 200, 255),
effects,
);
});
});
ctx.tessellate(out.shapes, out.pixels_per_point)
.iter()
.map(|p| match &p.primitive {
egui::epaint::Primitive::Mesh(m) => m.vertices.len(),
_ => 0,
})
.sum::<usize>()
};
let full = count(EffectsPolicy::Full);
let device = count(EffectsPolicy::None);
assert!(device > 0, "Device still draws the edge");
assert!(full > device, "Full glow/feather draws more than Device's crisp hairline: full={full} device={device}");
}
}