Skip to main content

agg_rust/
span_gouraud.rs

1//! Gouraud shading base — triangle with per-vertex colors.
2//!
3//! Port of `agg_span_gouraud.h` — stores triangle vertices with associated
4//! colors, sorts them by Y, and provides a `VertexSource` interface for
5//! feeding the triangle outline to the rasterizer.
6
7use crate::basics::{VertexSource, PATH_CMD_LINE_TO, PATH_CMD_MOVE_TO, PATH_CMD_STOP};
8use crate::math::{calc_intersection, dilate_triangle};
9
10// ============================================================================
11// CoordType — vertex with position and color
12// ============================================================================
13
14/// Triangle vertex with position and color.
15///
16/// Port of C++ `span_gouraud::coord_type`.
17#[derive(Clone)]
18pub struct CoordType<C: Clone> {
19    pub x: f64,
20    pub y: f64,
21    pub color: C,
22}
23
24impl<C: Clone + Default> Default for CoordType<C> {
25    fn default() -> Self {
26        Self {
27            x: 0.0,
28            y: 0.0,
29            color: C::default(),
30        }
31    }
32}
33
34// ============================================================================
35// SpanGouraud
36// ============================================================================
37
38/// Gouraud shading base class.
39///
40/// Stores a triangle with per-vertex colors. Implements `VertexSource`
41/// to feed the (possibly dilated) triangle outline to the rasterizer.
42///
43/// Dilation produces a 6-vertex beveled polygon for numerical stability,
44/// while the color interpolation coordinates use miter joins calculated
45/// via `calc_intersection`.
46///
47/// Port of C++ `span_gouraud<ColorT>`.
48pub struct SpanGouraud<C: Clone> {
49    coord: [CoordType<C>; 3],
50    x: [f64; 8],
51    y: [f64; 8],
52    cmd: [u32; 8],
53    vertex: usize,
54}
55
56impl<C: Clone + Default> SpanGouraud<C> {
57    pub fn new() -> Self {
58        let mut s = Self {
59            coord: [
60                CoordType::default(),
61                CoordType::default(),
62                CoordType::default(),
63            ],
64            x: [0.0; 8],
65            y: [0.0; 8],
66            cmd: [PATH_CMD_STOP; 8],
67            vertex: 0,
68        };
69        s.cmd[0] = PATH_CMD_STOP;
70        s
71    }
72
73    /// Construct with colors and triangle geometry.
74    #[allow(clippy::too_many_arguments)]
75    pub fn new_with_triangle(
76        c1: C,
77        c2: C,
78        c3: C,
79        x1: f64,
80        y1: f64,
81        x2: f64,
82        y2: f64,
83        x3: f64,
84        y3: f64,
85        d: f64,
86    ) -> Self {
87        let mut s = Self::new();
88        s.colors(c1, c2, c3);
89        s.triangle(x1, y1, x2, y2, x3, y3, d);
90        s
91    }
92
93    /// Set the vertex colors.
94    pub fn colors(&mut self, c1: C, c2: C, c3: C) {
95        self.coord[0].color = c1;
96        self.coord[1].color = c2;
97        self.coord[2].color = c3;
98    }
99
100    /// Set the triangle geometry and optionally dilate it.
101    ///
102    /// When `d != 0`, the triangle is dilated to form a 6-vertex beveled
103    /// polygon for numerical stability. The color interpolation coordinates
104    /// are recalculated using miter-join intersections.
105    #[allow(clippy::too_many_arguments)]
106    pub fn triangle(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, d: f64) {
107        self.coord[0].x = x1;
108        self.coord[0].y = y1;
109        self.coord[1].x = x2;
110        self.coord[1].y = y2;
111        self.coord[2].x = x3;
112        self.coord[2].y = y3;
113
114        self.x[0] = x1;
115        self.y[0] = y1;
116        self.x[1] = x2;
117        self.y[1] = y2;
118        self.x[2] = x3;
119        self.y[2] = y3;
120
121        self.cmd[0] = PATH_CMD_MOVE_TO;
122        self.cmd[1] = PATH_CMD_LINE_TO;
123        self.cmd[2] = PATH_CMD_LINE_TO;
124        self.cmd[3] = PATH_CMD_STOP;
125
126        if d != 0.0 {
127            let (dx, dy) = dilate_triangle(
128                self.coord[0].x,
129                self.coord[0].y,
130                self.coord[1].x,
131                self.coord[1].y,
132                self.coord[2].x,
133                self.coord[2].y,
134                d,
135            );
136            self.x[..6].copy_from_slice(&dx);
137            self.y[..6].copy_from_slice(&dy);
138
139            // Recalculate color interpolation coords using miter joins
140            if let Some((ix, iy)) = calc_intersection(
141                self.x[4], self.y[4], self.x[5], self.y[5], self.x[0], self.y[0], self.x[1],
142                self.y[1],
143            ) {
144                self.coord[0].x = ix;
145                self.coord[0].y = iy;
146            }
147
148            if let Some((ix, iy)) = calc_intersection(
149                self.x[0], self.y[0], self.x[1], self.y[1], self.x[2], self.y[2], self.x[3],
150                self.y[3],
151            ) {
152                self.coord[1].x = ix;
153                self.coord[1].y = iy;
154            }
155
156            if let Some((ix, iy)) = calc_intersection(
157                self.x[2], self.y[2], self.x[3], self.y[3], self.x[4], self.y[4], self.x[5],
158                self.y[5],
159            ) {
160                self.coord[2].x = ix;
161                self.coord[2].y = iy;
162            }
163
164            self.cmd[3] = PATH_CMD_LINE_TO;
165            self.cmd[4] = PATH_CMD_LINE_TO;
166            self.cmd[5] = PATH_CMD_LINE_TO;
167            self.cmd[6] = PATH_CMD_STOP;
168        }
169    }
170
171    /// Sort vertices by Y coordinate (top to bottom).
172    ///
173    /// Returns an array of three `CoordType` sorted so that
174    /// `result[0].y <= result[1].y <= result[2].y`.
175    pub fn arrange_vertices(&self) -> [CoordType<C>; 3] {
176        let mut coord = [
177            self.coord[0].clone(),
178            self.coord[1].clone(),
179            self.coord[2].clone(),
180        ];
181
182        if self.coord[0].y > self.coord[2].y {
183            coord[0] = self.coord[2].clone();
184            coord[2] = self.coord[0].clone();
185        }
186
187        if coord[0].y > coord[1].y {
188            let tmp = coord[0].clone();
189            coord[0] = coord[1].clone();
190            coord[1] = tmp;
191        }
192
193        if coord[1].y > coord[2].y {
194            let tmp = coord[1].clone();
195            coord[1] = coord[2].clone();
196            coord[2] = tmp;
197        }
198
199        coord
200    }
201}
202
203impl<C: Clone + Default> Default for SpanGouraud<C> {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209impl<C: Clone + Default> VertexSource for SpanGouraud<C> {
210    fn rewind(&mut self, _path_id: u32) {
211        self.vertex = 0;
212    }
213
214    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
215        let idx = self.vertex;
216        *x = self.x[idx];
217        *y = self.y[idx];
218        let cmd = self.cmd[idx];
219        self.vertex += 1;
220        cmd
221    }
222}
223
224// ============================================================================
225// Tests
226// ============================================================================
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::color::Rgba8;
232
233    #[test]
234    fn test_new_default() {
235        let sg = SpanGouraud::<Rgba8>::new();
236        assert_eq!(sg.cmd[0], PATH_CMD_STOP);
237    }
238
239    #[test]
240    fn test_colors() {
241        let mut sg = SpanGouraud::<Rgba8>::new();
242        let red = Rgba8::new(255, 0, 0, 255);
243        let green = Rgba8::new(0, 255, 0, 255);
244        let blue = Rgba8::new(0, 0, 255, 255);
245        sg.colors(red, green, blue);
246        assert_eq!(sg.coord[0].color.r, 255);
247        assert_eq!(sg.coord[1].color.g, 255);
248        assert_eq!(sg.coord[2].color.b, 255);
249    }
250
251    #[test]
252    fn test_triangle_no_dilation() {
253        let mut sg = SpanGouraud::<Rgba8>::new();
254        sg.triangle(0.0, 0.0, 100.0, 0.0, 50.0, 100.0, 0.0);
255
256        assert_eq!(sg.cmd[0], PATH_CMD_MOVE_TO);
257        assert_eq!(sg.cmd[1], PATH_CMD_LINE_TO);
258        assert_eq!(sg.cmd[2], PATH_CMD_LINE_TO);
259        assert_eq!(sg.cmd[3], PATH_CMD_STOP);
260
261        assert_eq!(sg.x[0], 0.0);
262        assert_eq!(sg.y[0], 0.0);
263        assert_eq!(sg.x[1], 100.0);
264        assert_eq!(sg.y[1], 0.0);
265        assert_eq!(sg.x[2], 50.0);
266        assert_eq!(sg.y[2], 100.0);
267    }
268
269    #[test]
270    fn test_triangle_with_dilation() {
271        let mut sg = SpanGouraud::<Rgba8>::new();
272        sg.triangle(0.0, 0.0, 100.0, 0.0, 50.0, 100.0, 1.0);
273
274        // Should have 6 vertices + stop
275        assert_eq!(sg.cmd[0], PATH_CMD_MOVE_TO);
276        assert_eq!(sg.cmd[5], PATH_CMD_LINE_TO);
277        assert_eq!(sg.cmd[6], PATH_CMD_STOP);
278    }
279
280    #[test]
281    fn test_vertex_source_no_dilation() {
282        let mut sg = SpanGouraud::<Rgba8>::new();
283        sg.triangle(10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 0.0);
284        sg.rewind(0);
285
286        let mut x = 0.0;
287        let mut y = 0.0;
288
289        assert_eq!(sg.vertex(&mut x, &mut y), PATH_CMD_MOVE_TO);
290        assert_eq!(x, 10.0);
291        assert_eq!(y, 20.0);
292
293        assert_eq!(sg.vertex(&mut x, &mut y), PATH_CMD_LINE_TO);
294        assert_eq!(x, 30.0);
295        assert_eq!(y, 40.0);
296
297        assert_eq!(sg.vertex(&mut x, &mut y), PATH_CMD_LINE_TO);
298        assert_eq!(x, 50.0);
299        assert_eq!(y, 60.0);
300
301        assert_eq!(sg.vertex(&mut x, &mut y), PATH_CMD_STOP);
302    }
303
304    #[test]
305    fn test_arrange_vertices() {
306        let mut sg = SpanGouraud::<Rgba8>::new();
307        let red = Rgba8::new(255, 0, 0, 255);
308        let green = Rgba8::new(0, 255, 0, 255);
309        let blue = Rgba8::new(0, 0, 255, 255);
310        sg.colors(red, green, blue);
311        // Vertices: (50, 100), (0, 0), (100, 50) — intentionally unsorted
312        sg.triangle(50.0, 100.0, 0.0, 0.0, 100.0, 50.0, 0.0);
313
314        let sorted = sg.arrange_vertices();
315        assert!(sorted[0].y <= sorted[1].y);
316        assert!(sorted[1].y <= sorted[2].y);
317        // Top vertex should have y=0 (originally vertex 1)
318        assert_eq!(sorted[0].y, 0.0);
319        assert_eq!(sorted[0].color.g, 255); // green vertex
320    }
321
322    #[test]
323    fn test_new_with_triangle() {
324        let red = Rgba8::new(255, 0, 0, 255);
325        let green = Rgba8::new(0, 255, 0, 255);
326        let blue = Rgba8::new(0, 0, 255, 255);
327        let sg = SpanGouraud::new_with_triangle(
328            red, green, blue, 0.0, 0.0, 100.0, 0.0, 50.0, 100.0, 0.0,
329        );
330        assert_eq!(sg.coord[0].color.r, 255);
331        assert_eq!(sg.coord[1].color.g, 255);
332        assert_eq!(sg.x[2], 50.0);
333    }
334
335    #[test]
336    fn test_arrange_already_sorted() {
337        let mut sg = SpanGouraud::<Rgba8>::new();
338        let c = Rgba8::new(128, 128, 128, 255);
339        sg.colors(c, c, c);
340        sg.triangle(0.0, 0.0, 50.0, 50.0, 100.0, 100.0, 0.0);
341
342        let sorted = sg.arrange_vertices();
343        assert_eq!(sorted[0].y, 0.0);
344        assert_eq!(sorted[1].y, 50.0);
345        assert_eq!(sorted[2].y, 100.0);
346    }
347
348    #[test]
349    fn test_rewind_resets_vertex() {
350        let mut sg = SpanGouraud::<Rgba8>::new();
351        sg.triangle(0.0, 0.0, 10.0, 10.0, 20.0, 0.0, 0.0);
352
353        let mut x = 0.0;
354        let mut y = 0.0;
355        sg.rewind(0);
356        sg.vertex(&mut x, &mut y);
357        sg.vertex(&mut x, &mut y);
358
359        // Rewind should reset
360        sg.rewind(0);
361        let cmd = sg.vertex(&mut x, &mut y);
362        assert_eq!(cmd, PATH_CMD_MOVE_TO);
363        assert_eq!(x, 0.0);
364    }
365}