Skip to main content

fret_core/scene/
paint.rs

1use crate::MaterialId;
2use crate::geometry::{Point, Size};
3
4use super::Color;
5
6pub const MAX_STOPS: usize = 8;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum PaintEvalSpaceV1 {
10    /// Evaluate paints in the op's local scene space (ADR 0233 D4).
11    #[default]
12    LocalPx,
13    /// Evaluate paints in viewport pixel space (after transforms).
14    ViewportPx,
15    /// Evaluate paints in a 1D stroke arclength domain: `paint_pos = (s01, 0)`.
16    StrokeS01,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct PaintBindingV1 {
21    pub paint: Paint,
22    pub eval_space: PaintEvalSpaceV1,
23}
24
25impl PaintBindingV1 {
26    pub const fn new(paint: Paint) -> Self {
27        Self {
28            paint,
29            eval_space: PaintEvalSpaceV1::LocalPx,
30        }
31    }
32
33    pub const fn with_eval_space(paint: Paint, eval_space: PaintEvalSpaceV1) -> Self {
34        Self { paint, eval_space }
35    }
36
37    pub fn sanitize(self) -> Self {
38        Self {
39            paint: self.paint.sanitize(),
40            eval_space: self.eval_space,
41        }
42    }
43}
44
45impl From<Paint> for PaintBindingV1 {
46    fn from(value: Paint) -> Self {
47        Self::new(value)
48    }
49}
50
51impl From<Color> for PaintBindingV1 {
52    fn from(value: Color) -> Self {
53        Self::new(Paint::Solid(value))
54    }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum TileMode {
59    Clamp,
60    Repeat,
61    Mirror,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum ColorSpace {
66    Srgb,
67    Oklab,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq)]
71pub struct GradientStop {
72    pub offset: f32,
73    pub color: Color,
74}
75
76impl GradientStop {
77    pub const fn new(offset: f32, color: Color) -> Self {
78        Self { offset, color }
79    }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq)]
83pub struct LinearGradient {
84    pub start: Point,
85    pub end: Point,
86    pub tile_mode: TileMode,
87    pub color_space: ColorSpace,
88    pub stop_count: u8,
89    pub stops: [GradientStop; MAX_STOPS],
90}
91
92#[derive(Debug, Clone, Copy, PartialEq)]
93pub struct RadialGradient {
94    pub center: Point,
95    pub radius: Size,
96    pub tile_mode: TileMode,
97    pub color_space: ColorSpace,
98    pub stop_count: u8,
99    pub stops: [GradientStop; MAX_STOPS],
100}
101
102#[derive(Debug, Clone, Copy, PartialEq)]
103pub struct SweepGradient {
104    pub center: Point,
105    /// Start angle in turns (1.0 = full rotation), counter-clockwise from +X.
106    pub start_angle_turns: f32,
107    /// End angle in turns (1.0 = full rotation), counter-clockwise from +X.
108    pub end_angle_turns: f32,
109    pub tile_mode: TileMode,
110    pub color_space: ColorSpace,
111    pub stop_count: u8,
112    pub stops: [GradientStop; MAX_STOPS],
113}
114
115#[repr(C)]
116#[derive(Debug, Clone, Copy, PartialEq)]
117pub struct MaterialParams {
118    pub vec4s: [[f32; 4]; 4],
119}
120
121impl MaterialParams {
122    pub const ZERO: Self = Self {
123        vec4s: [[0.0; 4]; 4],
124    };
125
126    pub fn sanitize(self) -> Self {
127        let mut out = self;
128        for v in &mut out.vec4s {
129            for x in v {
130                if !x.is_finite() {
131                    *x = 0.0;
132                }
133            }
134        }
135        out
136    }
137
138    pub fn is_finite(self) -> bool {
139        self.vec4s.iter().flatten().all(|&x| x.is_finite())
140    }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq)]
144pub enum Paint {
145    Solid(Color),
146    LinearGradient(LinearGradient),
147    RadialGradient(RadialGradient),
148    SweepGradient(SweepGradient),
149    Material {
150        id: MaterialId,
151        params: MaterialParams,
152    },
153}
154
155impl From<Color> for Paint {
156    fn from(value: Color) -> Self {
157        Paint::Solid(value)
158    }
159}
160
161impl Paint {
162    pub const TRANSPARENT: Self = Self::Solid(Color::TRANSPARENT);
163
164    pub fn sanitize(self) -> Self {
165        fn color_is_finite(c: Color) -> bool {
166            c.r.is_finite() && c.g.is_finite() && c.b.is_finite() && c.a.is_finite()
167        }
168
169        fn point_is_finite(p: Point) -> bool {
170            p.x.0.is_finite() && p.y.0.is_finite()
171        }
172
173        fn size_is_finite(s: Size) -> bool {
174            s.width.0.is_finite() && s.height.0.is_finite()
175        }
176
177        fn stops_all_finite(count: u8, stops: &[GradientStop; MAX_STOPS]) -> bool {
178            let n = usize::from(count).min(MAX_STOPS);
179            for s in stops.iter().take(n) {
180                if !s.offset.is_finite() || !color_is_finite(s.color) {
181                    return false;
182                }
183            }
184            true
185        }
186
187        fn clamp01(x: f32) -> f32 {
188            x.clamp(0.0, 1.0)
189        }
190
191        fn sort_stops(
192            count: u8,
193            mut stops: [GradientStop; MAX_STOPS],
194        ) -> [GradientStop; MAX_STOPS] {
195            let n = usize::from(count).min(MAX_STOPS);
196
197            for stop in stops.iter_mut().take(n) {
198                stop.offset = clamp01(stop.offset);
199            }
200
201            // Stable in-place sort (no heap) for small fixed arrays.
202            for i in 1..n {
203                let key = stops[i];
204                let mut j = i;
205                while j > 0 && stops[j - 1].offset > key.offset {
206                    stops[j] = stops[j - 1];
207                    j -= 1;
208                }
209                stops[j] = key;
210            }
211
212            stops
213        }
214
215        fn normalize_stop_count(count: u8) -> u8 {
216            count.min(MAX_STOPS as u8)
217        }
218
219        fn degrade_tile_mode(tile_mode: TileMode) -> TileMode {
220            tile_mode
221        }
222
223        fn degrade_color_space(color_space: ColorSpace) -> ColorSpace {
224            color_space
225        }
226
227        fn maybe_solid_from_degenerate(
228            count: u8,
229            stops: &[GradientStop; MAX_STOPS],
230        ) -> Option<Paint> {
231            let n = usize::from(count).min(MAX_STOPS);
232            if n == 0 {
233                return Some(Paint::TRANSPARENT);
234            }
235            if n == 1 {
236                return Some(Paint::Solid(stops[0].color));
237            }
238            let first = stops[0].offset;
239            let all_same = (1..n).all(|i| stops[i].offset == first);
240            if all_same {
241                return Some(Paint::Solid(stops[n - 1].color));
242            }
243            None
244        }
245
246        match self {
247            Paint::Solid(c) => {
248                if !color_is_finite(c) {
249                    Paint::TRANSPARENT
250                } else {
251                    Paint::Solid(c)
252                }
253            }
254            Paint::LinearGradient(mut g) => {
255                g.stop_count = normalize_stop_count(g.stop_count);
256                g.tile_mode = degrade_tile_mode(g.tile_mode);
257                g.color_space = degrade_color_space(g.color_space);
258
259                if !point_is_finite(g.start)
260                    || !point_is_finite(g.end)
261                    || !stops_all_finite(g.stop_count, &g.stops)
262                {
263                    return Paint::TRANSPARENT;
264                }
265
266                g.stops = sort_stops(g.stop_count, g.stops);
267                if let Some(solid) = maybe_solid_from_degenerate(g.stop_count, &g.stops) {
268                    return solid;
269                }
270
271                Paint::LinearGradient(g)
272            }
273            Paint::RadialGradient(mut g) => {
274                g.stop_count = normalize_stop_count(g.stop_count);
275                g.tile_mode = degrade_tile_mode(g.tile_mode);
276                g.color_space = degrade_color_space(g.color_space);
277
278                if !point_is_finite(g.center)
279                    || !size_is_finite(g.radius)
280                    || !stops_all_finite(g.stop_count, &g.stops)
281                {
282                    return Paint::TRANSPARENT;
283                }
284
285                g.stops = sort_stops(g.stop_count, g.stops);
286                if let Some(solid) = maybe_solid_from_degenerate(g.stop_count, &g.stops) {
287                    return solid;
288                }
289
290                Paint::RadialGradient(g)
291            }
292            Paint::SweepGradient(mut g) => {
293                g.stop_count = normalize_stop_count(g.stop_count);
294                g.tile_mode = degrade_tile_mode(g.tile_mode);
295                g.color_space = degrade_color_space(g.color_space);
296
297                if !point_is_finite(g.center)
298                    || !g.start_angle_turns.is_finite()
299                    || !g.end_angle_turns.is_finite()
300                    || !stops_all_finite(g.stop_count, &g.stops)
301                {
302                    return Paint::TRANSPARENT;
303                }
304
305                g.stops = sort_stops(g.stop_count, g.stops);
306                if let Some(solid) = maybe_solid_from_degenerate(g.stop_count, &g.stops) {
307                    return solid;
308                }
309
310                let start = g.start_angle_turns.rem_euclid(1.0);
311                let span_raw = g.end_angle_turns - g.start_angle_turns;
312                let span_mod = span_raw.rem_euclid(1.0);
313                let span = if span_mod <= 1e-6 && span_raw.abs() >= 1.0 - 1e-6 {
314                    1.0
315                } else {
316                    span_mod
317                };
318
319                if span <= 1e-6 {
320                    let n = usize::from(g.stop_count).min(MAX_STOPS);
321                    let c = g.stops[n.saturating_sub(1)].color;
322                    return Paint::Solid(c);
323                }
324
325                g.start_angle_turns = start;
326                g.end_angle_turns = start + span;
327
328                Paint::SweepGradient(g)
329            }
330            Paint::Material { id, params } => Paint::Material {
331                id,
332                params: params.sanitize(),
333            },
334        }
335    }
336}