Skip to main content

agg_rust/
line_aa_basics.rs

1//! Anti-aliased line rendering basics.
2//!
3//! Port of `agg_line_aa_basics.h` + `agg_line_aa_basics.cpp`.
4//! Constants, coordinate converters, `LineParameters` struct, and bisectrix.
5
6use crate::basics::iround;
7
8// ============================================================================
9// Constants
10// ============================================================================
11
12pub const LINE_SUBPIXEL_SHIFT: i32 = 8;
13pub const LINE_SUBPIXEL_SCALE: i32 = 1 << LINE_SUBPIXEL_SHIFT; // 256
14pub const LINE_SUBPIXEL_MASK: i32 = LINE_SUBPIXEL_SCALE - 1; // 255
15pub const LINE_MAX_COORD: i32 = (1 << 28) - 1;
16pub const LINE_MAX_LENGTH: i32 = 1 << 18; // ~262144
17
18pub const LINE_MR_SUBPIXEL_SHIFT: i32 = 4;
19pub const LINE_MR_SUBPIXEL_SCALE: i32 = 1 << LINE_MR_SUBPIXEL_SHIFT; // 16
20pub const LINE_MR_SUBPIXEL_MASK: i32 = LINE_MR_SUBPIXEL_SCALE - 1; // 15
21
22/// High-to-medium resolution.
23#[inline]
24pub fn line_mr(x: i32) -> i32 {
25    x >> (LINE_SUBPIXEL_SHIFT - LINE_MR_SUBPIXEL_SHIFT)
26}
27
28/// Medium-to-high resolution.
29#[inline]
30pub fn line_hr(x: i32) -> i32 {
31    x << (LINE_SUBPIXEL_SHIFT - LINE_MR_SUBPIXEL_SHIFT)
32}
33
34/// Integer to double-high resolution.
35#[inline]
36pub fn line_dbl_hr(x: i32) -> i32 {
37    x << LINE_SUBPIXEL_SHIFT
38}
39
40// ============================================================================
41// Coordinate converters
42// ============================================================================
43
44/// Convert f64 to subpixel integer coordinate.
45#[inline]
46pub fn line_coord(x: f64) -> i32 {
47    iround(x * LINE_SUBPIXEL_SCALE as f64)
48}
49
50/// Convert f64 to subpixel integer coordinate with saturation.
51#[inline]
52pub fn line_coord_sat(x: f64) -> i32 {
53    let v = iround(x * LINE_SUBPIXEL_SCALE as f64);
54    v.max(-LINE_MAX_COORD).min(LINE_MAX_COORD)
55}
56
57// ============================================================================
58// LineParameters
59// ============================================================================
60
61/// Orthogonal quadrant lookup by octant.
62const S_ORTHOGONAL_QUADRANT: [u8; 8] = [0, 0, 1, 1, 3, 3, 2, 2];
63
64/// Diagonal quadrant lookup by octant.
65const S_DIAGONAL_QUADRANT: [u8; 8] = [0, 1, 2, 1, 0, 3, 2, 3];
66
67/// Parameters describing a line segment for AA rendering.
68///
69/// Port of C++ `line_parameters`.
70#[derive(Debug, Clone, Copy)]
71pub struct LineParameters {
72    pub x1: i32,
73    pub y1: i32,
74    pub x2: i32,
75    pub y2: i32,
76    pub dx: i32,
77    pub dy: i32,
78    pub sx: i32,
79    pub sy: i32,
80    pub vertical: bool,
81    pub inc: i32,
82    pub len: i32,
83    pub octant: usize,
84}
85
86impl LineParameters {
87    pub fn new(x1: i32, y1: i32, x2: i32, y2: i32, len: i32) -> Self {
88        let dx = (x2 - x1).abs();
89        let dy = (y2 - y1).abs();
90        let sx = if x2 > x1 { 1 } else { -1 };
91        let sy = if y2 > y1 { 1 } else { -1 };
92        let vertical = dy >= dx;
93        let inc = if vertical { sy } else { sx };
94        // octant = (sy & 4) | (sx & 2) | int(vertical)
95        let octant = ((sy as u32 & 4) | (sx as u32 & 2) | (vertical as u32)) as usize;
96        Self {
97            x1, y1, x2, y2, dx, dy, sx, sy, vertical, inc, len, octant,
98        }
99    }
100
101    pub fn orthogonal_quadrant(&self) -> u8 {
102        S_ORTHOGONAL_QUADRANT[self.octant]
103    }
104
105    pub fn diagonal_quadrant(&self) -> u8 {
106        S_DIAGONAL_QUADRANT[self.octant]
107    }
108
109    pub fn same_orthogonal_quadrant(&self, other: &LineParameters) -> bool {
110        S_ORTHOGONAL_QUADRANT[self.octant] == S_ORTHOGONAL_QUADRANT[other.octant]
111    }
112
113    pub fn same_diagonal_quadrant(&self, other: &LineParameters) -> bool {
114        S_DIAGONAL_QUADRANT[self.octant] == S_DIAGONAL_QUADRANT[other.octant]
115    }
116
117    /// Split line at midpoint into two halves.
118    pub fn divide(&self) -> (LineParameters, LineParameters) {
119        let xmid = (self.x1 + self.x2) >> 1;
120        let ymid = (self.y1 + self.y2) >> 1;
121        let len2 = self.len >> 1;
122
123        let lp1 = LineParameters::new(self.x1, self.y1, xmid, ymid, len2);
124        let lp2 = LineParameters::new(xmid, ymid, self.x2, self.y2, len2);
125
126        (lp1, lp2)
127    }
128}
129
130// ============================================================================
131// Bisectrix
132// ============================================================================
133
134/// Calculate the angle bisector at the junction between two consecutive lines.
135///
136/// Port of C++ `bisectrix()`. Used for line joins.
137pub fn bisectrix(l1: &LineParameters, l2: &LineParameters, x: &mut i32, y: &mut i32) {
138    let k = l2.len as f64 / l1.len as f64;
139    let mut tx = l2.x2 as f64 - (l2.x1 - l1.x1) as f64 * k;
140    let mut ty = l2.y2 as f64 - (l2.y1 - l1.y1) as f64 * k;
141
142    // All bisectrices must be on the right of the line.
143    // If the next point is on the left (l1 => l2.2)
144    // then the bisectrix should be rotated by 180 degrees.
145    if ((l2.x2 - l2.x1) as f64 * (l2.y1 - l1.y1) as f64)
146        < ((l2.y2 - l2.y1) as f64 * (l2.x1 - l1.x1) as f64 + 100.0)
147    {
148        tx -= (tx - l2.x1 as f64) * 2.0;
149        ty -= (ty - l2.y1 as f64) * 2.0;
150    }
151
152    // Check if the bisectrix is too short
153    let dx = tx - l2.x1 as f64;
154    let dy = ty - l2.y1 as f64;
155    if (dx * dx + dy * dy).sqrt() as i32 >= LINE_SUBPIXEL_SCALE {
156        *x = iround(tx);
157        *y = iround(ty);
158    } else {
159        *x = (l2.x1 + l2.x1 + (l2.y1 - l1.y1) + (l2.y2 - l2.y1)) >> 1;
160        *y = (l2.y1 + l2.y1 - (l2.x1 - l1.x1) - (l2.x2 - l2.x1)) >> 1;
161    }
162}
163
164/// Fix degenerate bisectrix at line start.
165#[inline]
166pub fn fix_degenerate_bisectrix_start(lp: &LineParameters, x: &mut i32, y: &mut i32) {
167    let d = iround(
168        ((*x - lp.x2) as f64 * (lp.y2 - lp.y1) as f64
169            - (*y - lp.y2) as f64 * (lp.x2 - lp.x1) as f64)
170            / lp.len as f64,
171    );
172    if d < LINE_SUBPIXEL_SCALE / 2 {
173        *x = lp.x1 + (lp.y2 - lp.y1);
174        *y = lp.y1 - (lp.x2 - lp.x1);
175    }
176}
177
178/// Fix degenerate bisectrix at line end.
179#[inline]
180pub fn fix_degenerate_bisectrix_end(lp: &LineParameters, x: &mut i32, y: &mut i32) {
181    let d = iround(
182        ((*x - lp.x2) as f64 * (lp.y2 - lp.y1) as f64
183            - (*y - lp.y2) as f64 * (lp.x2 - lp.x1) as f64)
184            / lp.len as f64,
185    );
186    if d < LINE_SUBPIXEL_SCALE / 2 {
187        *x = lp.x2 + (lp.y2 - lp.y1);
188        *y = lp.y2 - (lp.x2 - lp.x1);
189    }
190}
191
192/// Integer line segment clipping (Liang-Barsky).
193///
194/// Returns: >= 4 means fully clipped.
195/// Bit 0: first point was moved. Bit 1: second point was moved.
196pub fn clip_line_segment(
197    x1: &mut i32,
198    y1: &mut i32,
199    x2: &mut i32,
200    y2: &mut i32,
201    clip: &crate::basics::RectI,
202) -> u32 {
203    // Use f64 version and convert
204    let mut fx1 = *x1 as f64;
205    let mut fy1 = *y1 as f64;
206    let mut fx2 = *x2 as f64;
207    let mut fy2 = *y2 as f64;
208    let fclip = crate::basics::Rect::new(
209        clip.x1 as f64,
210        clip.y1 as f64,
211        clip.x2 as f64,
212        clip.y2 as f64,
213    );
214    let ret = crate::clip_liang_barsky::clip_line_segment_f64(
215        &mut fx1, &mut fy1, &mut fx2, &mut fy2, &fclip,
216    );
217    *x1 = iround(fx1);
218    *y1 = iround(fy1);
219    *x2 = iround(fx2);
220    *y2 = iround(fy2);
221    ret
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_constants() {
230        assert_eq!(LINE_SUBPIXEL_SCALE, 256);
231        assert_eq!(LINE_SUBPIXEL_MASK, 255);
232        assert_eq!(LINE_MR_SUBPIXEL_SCALE, 16);
233    }
234
235    #[test]
236    fn test_line_mr_hr() {
237        assert_eq!(line_mr(256), 16); // 256 >> 4
238        assert_eq!(line_hr(16), 256); // 16 << 4
239        assert_eq!(line_dbl_hr(1), 256); // 1 << 8
240    }
241
242    #[test]
243    fn test_line_coord() {
244        assert_eq!(line_coord(1.0), 256);
245        assert_eq!(line_coord(0.5), 128);
246        assert_eq!(line_coord(0.0), 0);
247    }
248
249    #[test]
250    fn test_line_parameters_horizontal() {
251        let lp = LineParameters::new(0, 0, 1000, 0, 1000);
252        assert!(!lp.vertical);
253        assert_eq!(lp.dx, 1000);
254        assert_eq!(lp.dy, 0);
255        assert_eq!(lp.sx, 1);
256        assert_eq!(lp.sy, -1); // y2 == y1, so y2 > y1 is false → sy = -1
257        assert_eq!(lp.inc, 1); // sx since horizontal
258    }
259
260    #[test]
261    fn test_line_parameters_vertical() {
262        let lp = LineParameters::new(0, 0, 0, 1000, 1000);
263        assert!(lp.vertical);
264        assert_eq!(lp.dx, 0);
265        assert_eq!(lp.dy, 1000);
266        assert_eq!(lp.inc, 1); // sy since vertical
267    }
268
269    #[test]
270    fn test_line_parameters_divide() {
271        let lp = LineParameters::new(0, 0, 1000, 1000, 1414);
272        let (lp1, lp2) = lp.divide();
273        assert_eq!(lp1.x1, 0);
274        assert_eq!(lp1.y1, 0);
275        assert_eq!(lp1.x2, 500);
276        assert_eq!(lp1.y2, 500);
277        assert_eq!(lp2.x1, 500);
278        assert_eq!(lp2.y1, 500);
279        assert_eq!(lp2.x2, 1000);
280        assert_eq!(lp2.y2, 1000);
281    }
282
283    #[test]
284    fn test_quadrant_lookups() {
285        let lp = LineParameters::new(0, 0, 100, 100, 141);
286        // sx=1, sy=1, vertical(dy>=dx) → octant = (1&4)|(1&2)|1 = 0|0|1 = 1
287        assert_eq!(lp.octant, 1);
288        assert_eq!(lp.orthogonal_quadrant(), 0);
289        assert_eq!(lp.diagonal_quadrant(), 1);
290    }
291
292    #[test]
293    fn test_bisectrix_right_angle() {
294        // Two perpendicular lines: (0,0)→(256,0) then (256,0)→(256,256)
295        let l1 = LineParameters::new(0, 0, 256, 0, 256);
296        let l2 = LineParameters::new(256, 0, 256, 256, 256);
297        let (mut x, mut y) = (0, 0);
298        bisectrix(&l1, &l2, &mut x, &mut y);
299        // Bisector should point diagonally at ~45°
300        assert!(x > 256, "bisectrix x={x} should be > 256");
301        assert!(y < 0, "bisectrix y={y} should be < 0");
302    }
303
304    #[test]
305    fn test_clip_line_segment_visible() {
306        let clip = crate::basics::RectI::new(0, 0, 1000, 1000);
307        let (mut x1, mut y1, mut x2, mut y2) = (100, 100, 500, 500);
308        let ret = clip_line_segment(&mut x1, &mut y1, &mut x2, &mut y2, &clip);
309        assert_eq!(ret, 0); // fully visible
310    }
311
312    #[test]
313    fn test_clip_line_segment_clipped() {
314        let clip = crate::basics::RectI::new(100, 100, 400, 400);
315        let (mut x1, mut y1, mut x2, mut y2) = (0, 0, 500, 500);
316        let ret = clip_line_segment(&mut x1, &mut y1, &mut x2, &mut y2, &clip);
317        assert!(ret < 4); // not fully clipped
318        assert_ne!(ret, 0); // at least one point moved
319    }
320}