1#![forbid(unsafe_code)]
2
3use sequoia_core::{Point, Rect};
4
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
6pub enum Orientation {
7 #[default]
8 Horizontal,
9 Vertical,
10}
11
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct Track1D {
14 start: f32,
15 len: f32,
16 pad: f32,
17}
18
19impl Track1D {
20 pub fn new(start: f32, len: f32, pad: f32) -> Self {
21 Self {
22 start,
23 len: len.max(0.0),
24 pad: pad.max(0.0),
25 }
26 }
27
28 pub fn horizontal(rect: Rect, pad: f32) -> Self {
29 Self::new(rect.left(), rect.size.width, pad)
30 }
31
32 pub fn vertical(rect: Rect, pad: f32) -> Self {
33 Self::new(rect.top(), rect.size.height, pad)
34 }
35
36 fn inner(&self) -> f32 {
37 (self.len - self.pad * 2.0).max(1.0)
38 }
39
40 pub fn t_of(&self, pos: f32) -> f32 {
41 if !pos.is_finite() {
42 return 0.0;
43 }
44 ((pos - (self.start + self.pad)) / self.inner()).clamp(0.0, 1.0)
45 }
46
47 pub fn pos_of(&self, t: f32) -> f32 {
48 let t = if t.is_finite() {
49 t.clamp(0.0, 1.0)
50 } else {
51 0.0
52 };
53 self.start + self.pad + t * self.inner()
54 }
55
56 pub fn value_of(&self, pos: f32, min: f32, max: f32) -> f32 {
57 let (lo, hi) = if max >= min { (min, max) } else { (max, min) };
58 lo + self.t_of(pos) * (hi - lo).max(0.0)
59 }
60
61 pub fn pos_of_value(&self, v: f32, min: f32, max: f32) -> f32 {
62 let span = (max - min).abs().max(f32::MIN_POSITIVE);
63 self.pos_of((v - min.min(max)) / span)
64 }
65}
66
67#[derive(Clone, Copy, Debug, PartialEq)]
68pub struct Pad2D {
69 rect: Rect,
70 pad: f32,
71}
72
73impl Pad2D {
74 pub fn new(rect: Rect, pad: f32) -> Self {
75 Self {
76 rect,
77 pad: pad.max(0.0),
78 }
79 }
80
81 fn track_x(&self) -> Track1D {
82 Track1D::horizontal(self.rect, self.pad)
83 }
84
85 fn track_y(&self) -> Track1D {
86 Track1D::vertical(self.rect, self.pad)
87 }
88
89 pub fn sv_of(&self, p: Point) -> (f32, f32) {
90 let s = self.track_x().t_of(p.x);
91 let v = 1.0 - self.track_y().t_of(p.y);
92 (s, v)
93 }
94
95 pub fn pos_of(&self, s: f32, v: f32) -> Point {
96 Point::new(self.track_x().pos_of(s), self.track_y().pos_of(1.0 - v))
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 fn r(x: f32, y: f32, w: f32, h: f32) -> Rect {
105 Rect::from_xywh(x, y, w, h)
106 }
107
108 #[test]
109 fn track_roundtrips_with_knob_pad() {
110 let t = Track1D::horizontal(r(0.0, 0.0, 200.0, 26.0), 7.0);
111 assert!((t.pos_of(0.0) - 7.0).abs() < 1e-4);
112 assert!((t.pos_of(1.0) - 193.0).abs() < 1e-4);
113 assert!((t.t_of(7.0)).abs() < 1e-4);
114 assert!((t.t_of(193.0) - 1.0).abs() < 1e-4);
115
116 assert_eq!(t.t_of(0.0), 0.0);
117 assert_eq!(t.t_of(200.0), 1.0);
118 assert!((t.pos_of(0.5) - 100.0).abs() < 1e-4);
119 assert!((t.t_of(100.0) - 0.5).abs() < 1e-4);
120 }
121
122 #[test]
123 fn track_without_pad_is_full_bleed() {
124 let t = Track1D::horizontal(r(10.0, 0.0, 200.0, 26.0), 0.0);
125 assert!((t.pos_of(0.0) - 10.0).abs() < 1e-4);
126 assert!((t.pos_of(1.0) - 210.0).abs() < 1e-4);
127 assert!((t.t_of(110.0) - 0.5).abs() < 1e-4);
128 }
129
130 #[test]
131 fn track_maps_values_and_survives_garbage() {
132 let t = Track1D::horizontal(r(0.0, 0.0, 200.0, 26.0), 7.0);
133 assert!((t.value_of(7.0, 0.0, 360.0)).abs() < 1e-3);
134 assert!((t.value_of(193.0, 0.0, 360.0) - 360.0).abs() < 1e-3);
135 assert!((t.pos_of_value(180.0, 0.0, 360.0) - t.pos_of(0.5)).abs() < 1e-4);
136 assert_eq!(t.t_of(f32::NAN), 0.0);
137 assert_eq!(t.pos_of(f32::NAN), 7.0);
138
139 let flat = Track1D::horizontal(r(0.0, 0.0, 0.0, 0.0), 7.0);
140 assert!(flat.pos_of(0.5).is_finite());
141 assert!(flat.t_of(0.0).is_finite());
142 }
143
144 #[test]
145 fn pad2d_roundtrips_sv() {
146 let pad = Pad2D::new(r(0.0, 0.0, 200.0, 200.0), 7.0);
147 let c = pad.pos_of(0.5, 0.5);
148 assert!((c.x - 100.0).abs() < 1e-4 && (c.y - 100.0).abs() < 1e-4);
149 let (s, v) = pad.sv_of(c);
150 assert!((s - 0.5).abs() < 1e-4 && (v - 0.5).abs() < 1e-4);
151
152 assert_eq!(pad.sv_of(Point::new(0.0, 0.0)), (0.0, 1.0));
153 assert_eq!(pad.sv_of(Point::new(200.0, 200.0)), (1.0, 0.0));
154 }
155}