Skip to main content

agg_rust/
curves.rs

1//! Bezier curve generators (quadratic and cubic).
2//!
3//! Port of `agg_curves.h` / `agg_curves.cpp` — provides two algorithms for
4//! flattening Bezier curves into line segments:
5//!
6//! - **Incremental** (`Curve3Inc`, `Curve4Inc`): forward-differencing, fast but
7//!   less precise for extreme curvatures.
8//! - **Subdivision** (`Curve3Div`, `Curve4Div`): recursive de Casteljau
9//!   subdivision, adaptive and high-quality.
10//!
11//! The facade types `Curve3` and `Curve4` delegate to either algorithm.
12//!
13//! Also provides conversion functions: `catrom_to_bezier`,
14//! `ubspline_to_bezier`, `hermite_to_bezier`.
15
16use crate::basics::{PointD, VertexSource, PATH_CMD_LINE_TO, PATH_CMD_MOVE_TO, PATH_CMD_STOP, PI};
17use crate::math::calc_sq_distance;
18
19// ============================================================================
20// Constants
21// ============================================================================
22
23const CURVE_COLLINEARITY_EPSILON: f64 = 1e-30;
24const CURVE_ANGLE_TOLERANCE_EPSILON: f64 = 0.01;
25const CURVE_RECURSION_LIMIT: u32 = 32;
26
27// ============================================================================
28// Curve approximation method
29// ============================================================================
30
31/// Algorithm selection for curve flattening.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum CurveApproximationMethod {
34    Inc,
35    Div,
36}
37
38// ============================================================================
39// Curve4Points
40// ============================================================================
41
42/// Eight control-point coordinates for a cubic Bezier curve.
43#[derive(Debug, Clone, Copy)]
44pub struct Curve4Points {
45    pub cp: [f64; 8],
46}
47
48impl Curve4Points {
49    #[allow(clippy::too_many_arguments)]
50    pub fn new(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) -> Self {
51        Self {
52            cp: [x1, y1, x2, y2, x3, y3, x4, y4],
53        }
54    }
55
56    #[allow(clippy::too_many_arguments)]
57    pub fn init(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) {
58        self.cp = [x1, y1, x2, y2, x3, y3, x4, y4];
59    }
60}
61
62impl std::ops::Index<usize> for Curve4Points {
63    type Output = f64;
64    fn index(&self, i: usize) -> &f64 {
65        &self.cp[i]
66    }
67}
68
69impl std::ops::IndexMut<usize> for Curve4Points {
70    fn index_mut(&mut self, i: usize) -> &mut f64 {
71        &mut self.cp[i]
72    }
73}
74
75// ============================================================================
76// Curve conversion functions
77// ============================================================================
78
79/// Convert Catmull-Rom spline segment to cubic Bezier control points.
80#[allow(clippy::too_many_arguments)]
81pub fn catrom_to_bezier(
82    x1: f64,
83    y1: f64,
84    x2: f64,
85    y2: f64,
86    x3: f64,
87    y3: f64,
88    x4: f64,
89    y4: f64,
90) -> Curve4Points {
91    Curve4Points::new(
92        x2,
93        y2,
94        (-x1 + 6.0 * x2 + x3) / 6.0,
95        (-y1 + 6.0 * y2 + y3) / 6.0,
96        (x2 + 6.0 * x3 - x4) / 6.0,
97        (y2 + 6.0 * y3 - y4) / 6.0,
98        x3,
99        y3,
100    )
101}
102
103/// Convert uniform B-spline segment to cubic Bezier control points.
104#[allow(clippy::too_many_arguments)]
105pub fn ubspline_to_bezier(
106    x1: f64,
107    y1: f64,
108    x2: f64,
109    y2: f64,
110    x3: f64,
111    y3: f64,
112    x4: f64,
113    y4: f64,
114) -> Curve4Points {
115    Curve4Points::new(
116        (x1 + 4.0 * x2 + x3) / 6.0,
117        (y1 + 4.0 * y2 + y3) / 6.0,
118        (4.0 * x2 + 2.0 * x3) / 6.0,
119        (4.0 * y2 + 2.0 * y3) / 6.0,
120        (2.0 * x2 + 4.0 * x3) / 6.0,
121        (2.0 * y2 + 4.0 * y3) / 6.0,
122        (x2 + 4.0 * x3 + x4) / 6.0,
123        (y2 + 4.0 * y3 + y4) / 6.0,
124    )
125}
126
127/// Convert Hermite spline segment to cubic Bezier control points.
128#[allow(clippy::too_many_arguments)]
129pub fn hermite_to_bezier(
130    x1: f64,
131    y1: f64,
132    x2: f64,
133    y2: f64,
134    x3: f64,
135    y3: f64,
136    x4: f64,
137    y4: f64,
138) -> Curve4Points {
139    Curve4Points::new(
140        x1,
141        y1,
142        (3.0 * x1 + x3) / 3.0,
143        (3.0 * y1 + y3) / 3.0,
144        (3.0 * x2 - x4) / 3.0,
145        (3.0 * y2 - y4) / 3.0,
146        x2,
147        y2,
148    )
149}
150
151// ============================================================================
152// Curve3Inc — incremental (forward differences) quadratic Bezier
153// ============================================================================
154
155/// Incremental quadratic Bezier curve flattener using forward differences.
156///
157/// Port of C++ `agg::curve3_inc`.
158pub struct Curve3Inc {
159    num_steps: i32,
160    step: i32,
161    scale: f64,
162    start_x: f64,
163    start_y: f64,
164    end_x: f64,
165    end_y: f64,
166    fx: f64,
167    fy: f64,
168    dfx: f64,
169    dfy: f64,
170    ddfx: f64,
171    ddfy: f64,
172    saved_fx: f64,
173    saved_fy: f64,
174    saved_dfx: f64,
175    saved_dfy: f64,
176}
177
178impl Curve3Inc {
179    pub fn new() -> Self {
180        Self {
181            num_steps: 0,
182            step: 0,
183            scale: 1.0,
184            start_x: 0.0,
185            start_y: 0.0,
186            end_x: 0.0,
187            end_y: 0.0,
188            fx: 0.0,
189            fy: 0.0,
190            dfx: 0.0,
191            dfy: 0.0,
192            ddfx: 0.0,
193            ddfy: 0.0,
194            saved_fx: 0.0,
195            saved_fy: 0.0,
196            saved_dfx: 0.0,
197            saved_dfy: 0.0,
198        }
199    }
200
201    pub fn new_with_points(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> Self {
202        let mut c = Self::new();
203        c.init(x1, y1, x2, y2, x3, y3);
204        c
205    }
206
207    pub fn reset(&mut self) {
208        self.num_steps = 0;
209        self.step = -1;
210    }
211
212    pub fn init(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) {
213        self.start_x = x1;
214        self.start_y = y1;
215        self.end_x = x3;
216        self.end_y = y3;
217
218        let dx1 = x2 - x1;
219        let dy1 = y2 - y1;
220        let dx2 = x3 - x2;
221        let dy2 = y3 - y2;
222
223        let len = (dx1 * dx1 + dy1 * dy1).sqrt() + (dx2 * dx2 + dy2 * dy2).sqrt();
224
225        self.num_steps = crate::basics::uround(len * 0.25 * self.scale) as i32;
226
227        if self.num_steps < 4 {
228            self.num_steps = 4;
229        }
230
231        let subdivide_step = 1.0 / self.num_steps as f64;
232        let subdivide_step2 = subdivide_step * subdivide_step;
233
234        let tmpx = (x1 - x2 * 2.0 + x3) * subdivide_step2;
235        let tmpy = (y1 - y2 * 2.0 + y3) * subdivide_step2;
236
237        self.fx = x1;
238        self.saved_fx = x1;
239        self.fy = y1;
240        self.saved_fy = y1;
241
242        self.dfx = tmpx + (x2 - x1) * (2.0 * subdivide_step);
243        self.saved_dfx = self.dfx;
244        self.dfy = tmpy + (y2 - y1) * (2.0 * subdivide_step);
245        self.saved_dfy = self.dfy;
246
247        self.ddfx = tmpx * 2.0;
248        self.ddfy = tmpy * 2.0;
249
250        self.step = self.num_steps;
251    }
252
253    pub fn set_approximation_scale(&mut self, s: f64) {
254        self.scale = s;
255    }
256
257    pub fn approximation_scale(&self) -> f64 {
258        self.scale
259    }
260}
261
262impl Default for Curve3Inc {
263    fn default() -> Self {
264        Self::new()
265    }
266}
267
268impl VertexSource for Curve3Inc {
269    fn rewind(&mut self, _path_id: u32) {
270        if self.num_steps == 0 {
271            self.step = -1;
272            return;
273        }
274        self.step = self.num_steps;
275        self.fx = self.saved_fx;
276        self.fy = self.saved_fy;
277        self.dfx = self.saved_dfx;
278        self.dfy = self.saved_dfy;
279    }
280
281    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
282        if self.step < 0 {
283            return PATH_CMD_STOP;
284        }
285        if self.step == self.num_steps {
286            *x = self.start_x;
287            *y = self.start_y;
288            self.step -= 1;
289            return PATH_CMD_MOVE_TO;
290        }
291        if self.step == 0 {
292            *x = self.end_x;
293            *y = self.end_y;
294            self.step -= 1;
295            return PATH_CMD_LINE_TO;
296        }
297        self.fx += self.dfx;
298        self.fy += self.dfy;
299        self.dfx += self.ddfx;
300        self.dfy += self.ddfy;
301        *x = self.fx;
302        *y = self.fy;
303        self.step -= 1;
304        PATH_CMD_LINE_TO
305    }
306}
307
308// ============================================================================
309// Curve3Div — recursive subdivision quadratic Bezier
310// ============================================================================
311
312/// Recursive subdivision quadratic Bezier curve flattener.
313///
314/// Port of C++ `agg::curve3_div`.
315pub struct Curve3Div {
316    approximation_scale: f64,
317    distance_tolerance_square: f64,
318    angle_tolerance: f64,
319    count: usize,
320    points: Vec<PointD>,
321}
322
323impl Curve3Div {
324    pub fn new() -> Self {
325        Self {
326            approximation_scale: 1.0,
327            distance_tolerance_square: 0.0,
328            angle_tolerance: 0.0,
329            count: 0,
330            points: Vec::new(),
331        }
332    }
333
334    pub fn new_with_points(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> Self {
335        let mut c = Self::new();
336        c.init(x1, y1, x2, y2, x3, y3);
337        c
338    }
339
340    pub fn reset(&mut self) {
341        self.points.clear();
342        self.count = 0;
343    }
344
345    pub fn init(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) {
346        self.points.clear();
347        self.distance_tolerance_square = 0.5 / self.approximation_scale;
348        self.distance_tolerance_square *= self.distance_tolerance_square;
349        self.bezier(x1, y1, x2, y2, x3, y3);
350        self.count = 0;
351    }
352
353    pub fn set_approximation_scale(&mut self, s: f64) {
354        self.approximation_scale = s;
355    }
356
357    pub fn approximation_scale(&self) -> f64 {
358        self.approximation_scale
359    }
360
361    pub fn set_angle_tolerance(&mut self, a: f64) {
362        self.angle_tolerance = a;
363    }
364
365    pub fn angle_tolerance(&self) -> f64 {
366        self.angle_tolerance
367    }
368
369    fn bezier(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) {
370        self.points.push(PointD { x: x1, y: y1 });
371        self.recursive_bezier(x1, y1, x2, y2, x3, y3, 0);
372        self.points.push(PointD { x: x3, y: y3 });
373    }
374
375    #[allow(clippy::too_many_arguments)]
376    fn recursive_bezier(
377        &mut self,
378        x1: f64,
379        y1: f64,
380        x2: f64,
381        y2: f64,
382        x3: f64,
383        y3: f64,
384        level: u32,
385    ) {
386        if level > CURVE_RECURSION_LIMIT {
387            return;
388        }
389
390        // Calculate midpoints
391        let x12 = (x1 + x2) / 2.0;
392        let y12 = (y1 + y2) / 2.0;
393        let x23 = (x2 + x3) / 2.0;
394        let y23 = (y2 + y3) / 2.0;
395        let x123 = (x12 + x23) / 2.0;
396        let y123 = (y12 + y23) / 2.0;
397
398        let dx = x3 - x1;
399        let dy = y3 - y1;
400        let d = ((x2 - x3) * dy - (y2 - y3) * dx).abs();
401
402        if d > CURVE_COLLINEARITY_EPSILON {
403            // Regular case
404            if d * d <= self.distance_tolerance_square * (dx * dx + dy * dy) {
405                if self.angle_tolerance < CURVE_ANGLE_TOLERANCE_EPSILON {
406                    self.points.push(PointD { x: x123, y: y123 });
407                    return;
408                }
409
410                // Angle & Cusp Condition
411                let mut da = ((y3 - y2).atan2(x3 - x2) - (y2 - y1).atan2(x2 - x1)).abs();
412                if da >= PI {
413                    da = 2.0 * PI - da;
414                }
415
416                if da < self.angle_tolerance {
417                    self.points.push(PointD { x: x123, y: y123 });
418                    return;
419                }
420            }
421        } else {
422            // Collinear case
423            let da = dx * dx + dy * dy;
424            let d_val = if da == 0.0 {
425                calc_sq_distance(x1, y1, x2, y2)
426            } else {
427                let d_param = ((x2 - x1) * dx + (y2 - y1) * dy) / da;
428                if d_param > 0.0 && d_param < 1.0 {
429                    // Simple collinear case, 1---2---3
430                    return;
431                }
432                if d_param <= 0.0 {
433                    calc_sq_distance(x2, y2, x1, y1)
434                } else if d_param >= 1.0 {
435                    calc_sq_distance(x2, y2, x3, y3)
436                } else {
437                    calc_sq_distance(x2, y2, x1 + d_param * dx, y1 + d_param * dy)
438                }
439            };
440            if d_val < self.distance_tolerance_square {
441                self.points.push(PointD { x: x2, y: y2 });
442                return;
443            }
444        }
445
446        // Continue subdivision
447        self.recursive_bezier(x1, y1, x12, y12, x123, y123, level + 1);
448        self.recursive_bezier(x123, y123, x23, y23, x3, y3, level + 1);
449    }
450}
451
452impl Default for Curve3Div {
453    fn default() -> Self {
454        Self::new()
455    }
456}
457
458impl VertexSource for Curve3Div {
459    fn rewind(&mut self, _path_id: u32) {
460        self.count = 0;
461    }
462
463    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
464        if self.count >= self.points.len() {
465            return PATH_CMD_STOP;
466        }
467        let p = &self.points[self.count];
468        *x = p.x;
469        *y = p.y;
470        self.count += 1;
471        if self.count == 1 {
472            PATH_CMD_MOVE_TO
473        } else {
474            PATH_CMD_LINE_TO
475        }
476    }
477}
478
479// ============================================================================
480// Curve4Inc — incremental (forward differences) cubic Bezier
481// ============================================================================
482
483/// Incremental cubic Bezier curve flattener using forward differences.
484///
485/// Port of C++ `agg::curve4_inc`.
486pub struct Curve4Inc {
487    num_steps: i32,
488    step: i32,
489    scale: f64,
490    start_x: f64,
491    start_y: f64,
492    end_x: f64,
493    end_y: f64,
494    fx: f64,
495    fy: f64,
496    dfx: f64,
497    dfy: f64,
498    ddfx: f64,
499    ddfy: f64,
500    dddfx: f64,
501    dddfy: f64,
502    saved_fx: f64,
503    saved_fy: f64,
504    saved_dfx: f64,
505    saved_dfy: f64,
506    saved_ddfx: f64,
507    saved_ddfy: f64,
508}
509
510impl Curve4Inc {
511    pub fn new() -> Self {
512        Self {
513            num_steps: 0,
514            step: 0,
515            scale: 1.0,
516            start_x: 0.0,
517            start_y: 0.0,
518            end_x: 0.0,
519            end_y: 0.0,
520            fx: 0.0,
521            fy: 0.0,
522            dfx: 0.0,
523            dfy: 0.0,
524            ddfx: 0.0,
525            ddfy: 0.0,
526            dddfx: 0.0,
527            dddfy: 0.0,
528            saved_fx: 0.0,
529            saved_fy: 0.0,
530            saved_dfx: 0.0,
531            saved_dfy: 0.0,
532            saved_ddfx: 0.0,
533            saved_ddfy: 0.0,
534        }
535    }
536
537    #[allow(clippy::too_many_arguments)]
538    pub fn new_with_points(
539        x1: f64,
540        y1: f64,
541        x2: f64,
542        y2: f64,
543        x3: f64,
544        y3: f64,
545        x4: f64,
546        y4: f64,
547    ) -> Self {
548        let mut c = Self::new();
549        c.init(x1, y1, x2, y2, x3, y3, x4, y4);
550        c
551    }
552
553    pub fn new_with_curve4_points(cp: &Curve4Points) -> Self {
554        let mut c = Self::new();
555        c.init(cp[0], cp[1], cp[2], cp[3], cp[4], cp[5], cp[6], cp[7]);
556        c
557    }
558
559    pub fn reset(&mut self) {
560        self.num_steps = 0;
561        self.step = -1;
562    }
563
564    #[allow(clippy::too_many_arguments)]
565    pub fn init(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) {
566        self.start_x = x1;
567        self.start_y = y1;
568        self.end_x = x4;
569        self.end_y = y4;
570
571        let dx1 = x2 - x1;
572        let dy1 = y2 - y1;
573        let dx2 = x3 - x2;
574        let dy2 = y3 - y2;
575        let dx3 = x4 - x3;
576        let dy3 = y4 - y3;
577
578        let len = ((dx1 * dx1 + dy1 * dy1).sqrt()
579            + (dx2 * dx2 + dy2 * dy2).sqrt()
580            + (dx3 * dx3 + dy3 * dy3).sqrt())
581            * 0.25
582            * self.scale;
583
584        self.num_steps = crate::basics::uround(len) as i32;
585
586        if self.num_steps < 4 {
587            self.num_steps = 4;
588        }
589
590        let subdivide_step = 1.0 / self.num_steps as f64;
591        let subdivide_step2 = subdivide_step * subdivide_step;
592        let subdivide_step3 = subdivide_step * subdivide_step * subdivide_step;
593
594        let pre1 = 3.0 * subdivide_step;
595        let pre2 = 3.0 * subdivide_step2;
596        let pre4 = 6.0 * subdivide_step2;
597        let pre5 = 6.0 * subdivide_step3;
598
599        let tmp1x = x1 - x2 * 2.0 + x3;
600        let tmp1y = y1 - y2 * 2.0 + y3;
601
602        let tmp2x = (x2 - x3) * 3.0 - x1 + x4;
603        let tmp2y = (y2 - y3) * 3.0 - y1 + y4;
604
605        self.saved_fx = x1;
606        self.fx = x1;
607        self.saved_fy = y1;
608        self.fy = y1;
609
610        self.saved_dfx = (x2 - x1) * pre1 + tmp1x * pre2 + tmp2x * subdivide_step3;
611        self.dfx = self.saved_dfx;
612        self.saved_dfy = (y2 - y1) * pre1 + tmp1y * pre2 + tmp2y * subdivide_step3;
613        self.dfy = self.saved_dfy;
614
615        self.saved_ddfx = tmp1x * pre4 + tmp2x * pre5;
616        self.ddfx = self.saved_ddfx;
617        self.saved_ddfy = tmp1y * pre4 + tmp2y * pre5;
618        self.ddfy = self.saved_ddfy;
619
620        self.dddfx = tmp2x * pre5;
621        self.dddfy = tmp2y * pre5;
622
623        self.step = self.num_steps;
624    }
625
626    pub fn init_with_curve4_points(&mut self, cp: &Curve4Points) {
627        self.init(cp[0], cp[1], cp[2], cp[3], cp[4], cp[5], cp[6], cp[7]);
628    }
629
630    pub fn set_approximation_scale(&mut self, s: f64) {
631        self.scale = s;
632    }
633
634    pub fn approximation_scale(&self) -> f64 {
635        self.scale
636    }
637}
638
639impl Default for Curve4Inc {
640    fn default() -> Self {
641        Self::new()
642    }
643}
644
645impl VertexSource for Curve4Inc {
646    fn rewind(&mut self, _path_id: u32) {
647        if self.num_steps == 0 {
648            self.step = -1;
649            return;
650        }
651        self.step = self.num_steps;
652        self.fx = self.saved_fx;
653        self.fy = self.saved_fy;
654        self.dfx = self.saved_dfx;
655        self.dfy = self.saved_dfy;
656        self.ddfx = self.saved_ddfx;
657        self.ddfy = self.saved_ddfy;
658    }
659
660    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
661        if self.step < 0 {
662            return PATH_CMD_STOP;
663        }
664        if self.step == self.num_steps {
665            *x = self.start_x;
666            *y = self.start_y;
667            self.step -= 1;
668            return PATH_CMD_MOVE_TO;
669        }
670        if self.step == 0 {
671            *x = self.end_x;
672            *y = self.end_y;
673            self.step -= 1;
674            return PATH_CMD_LINE_TO;
675        }
676
677        self.fx += self.dfx;
678        self.fy += self.dfy;
679        self.dfx += self.ddfx;
680        self.dfy += self.ddfy;
681        self.ddfx += self.dddfx;
682        self.ddfy += self.dddfy;
683
684        *x = self.fx;
685        *y = self.fy;
686        self.step -= 1;
687        PATH_CMD_LINE_TO
688    }
689}
690
691// ============================================================================
692// Curve4Div — recursive subdivision cubic Bezier
693// ============================================================================
694
695/// Recursive subdivision cubic Bezier curve flattener.
696///
697/// Port of C++ `agg::curve4_div`.
698pub struct Curve4Div {
699    approximation_scale: f64,
700    distance_tolerance_square: f64,
701    angle_tolerance: f64,
702    cusp_limit: f64,
703    count: usize,
704    points: Vec<PointD>,
705}
706
707impl Curve4Div {
708    pub fn new() -> Self {
709        Self {
710            approximation_scale: 1.0,
711            distance_tolerance_square: 0.0,
712            angle_tolerance: 0.0,
713            cusp_limit: 0.0,
714            count: 0,
715            points: Vec::new(),
716        }
717    }
718
719    #[allow(clippy::too_many_arguments)]
720    pub fn new_with_points(
721        x1: f64,
722        y1: f64,
723        x2: f64,
724        y2: f64,
725        x3: f64,
726        y3: f64,
727        x4: f64,
728        y4: f64,
729    ) -> Self {
730        let mut c = Self::new();
731        c.init(x1, y1, x2, y2, x3, y3, x4, y4);
732        c
733    }
734
735    pub fn new_with_curve4_points(cp: &Curve4Points) -> Self {
736        let mut c = Self::new();
737        c.init(cp[0], cp[1], cp[2], cp[3], cp[4], cp[5], cp[6], cp[7]);
738        c
739    }
740
741    pub fn reset(&mut self) {
742        self.points.clear();
743        self.count = 0;
744    }
745
746    #[allow(clippy::too_many_arguments)]
747    pub fn init(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) {
748        self.points.clear();
749        self.distance_tolerance_square = 0.5 / self.approximation_scale;
750        self.distance_tolerance_square *= self.distance_tolerance_square;
751        self.bezier(x1, y1, x2, y2, x3, y3, x4, y4);
752        self.count = 0;
753    }
754
755    pub fn init_with_curve4_points(&mut self, cp: &Curve4Points) {
756        self.init(cp[0], cp[1], cp[2], cp[3], cp[4], cp[5], cp[6], cp[7]);
757    }
758
759    pub fn set_approximation_scale(&mut self, s: f64) {
760        self.approximation_scale = s;
761    }
762
763    pub fn approximation_scale(&self) -> f64 {
764        self.approximation_scale
765    }
766
767    pub fn set_angle_tolerance(&mut self, a: f64) {
768        self.angle_tolerance = a;
769    }
770
771    pub fn angle_tolerance(&self) -> f64 {
772        self.angle_tolerance
773    }
774
775    pub fn set_cusp_limit(&mut self, v: f64) {
776        self.cusp_limit = if v == 0.0 { 0.0 } else { PI - v };
777    }
778
779    pub fn cusp_limit(&self) -> f64 {
780        if self.cusp_limit == 0.0 {
781            0.0
782        } else {
783            PI - self.cusp_limit
784        }
785    }
786
787    #[allow(clippy::too_many_arguments)]
788    fn bezier(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) {
789        self.points.push(PointD { x: x1, y: y1 });
790        self.recursive_bezier(x1, y1, x2, y2, x3, y3, x4, y4, 0);
791        self.points.push(PointD { x: x4, y: y4 });
792    }
793
794    #[allow(clippy::too_many_arguments)]
795    fn recursive_bezier(
796        &mut self,
797        x1: f64,
798        y1: f64,
799        x2: f64,
800        y2: f64,
801        x3: f64,
802        y3: f64,
803        x4: f64,
804        y4: f64,
805        level: u32,
806    ) {
807        if level > CURVE_RECURSION_LIMIT {
808            return;
809        }
810
811        // Calculate all the mid-points of the line segments
812        let x12 = (x1 + x2) / 2.0;
813        let y12 = (y1 + y2) / 2.0;
814        let x23 = (x2 + x3) / 2.0;
815        let y23 = (y2 + y3) / 2.0;
816        let x34 = (x3 + x4) / 2.0;
817        let y34 = (y3 + y4) / 2.0;
818        let x123 = (x12 + x23) / 2.0;
819        let y123 = (y12 + y23) / 2.0;
820        let x234 = (x23 + x34) / 2.0;
821        let y234 = (y23 + y34) / 2.0;
822        let x1234 = (x123 + x234) / 2.0;
823        let y1234 = (y123 + y234) / 2.0;
824
825        // Try to approximate the full cubic curve by a single straight line
826        let dx = x4 - x1;
827        let dy = y4 - y1;
828
829        let mut d2 = ((x2 - x4) * dy - (y2 - y4) * dx).abs();
830        let mut d3 = ((x3 - x4) * dy - (y3 - y4) * dx).abs();
831
832        let case = ((d2 > CURVE_COLLINEARITY_EPSILON) as u32) << 1
833            | (d3 > CURVE_COLLINEARITY_EPSILON) as u32;
834
835        match case {
836            0 => {
837                // All collinear OR p1==p4
838                let k = dx * dx + dy * dy;
839                if k == 0.0 {
840                    d2 = calc_sq_distance(x1, y1, x2, y2);
841                    d3 = calc_sq_distance(x4, y4, x3, y3);
842                } else {
843                    let k = 1.0 / k;
844                    let da1 = x2 - x1;
845                    let da2 = y2 - y1;
846                    d2 = k * (da1 * dx + da2 * dy);
847                    let da1 = x3 - x1;
848                    let da2 = y3 - y1;
849                    d3 = k * (da1 * dx + da2 * dy);
850                    if d2 > 0.0 && d2 < 1.0 && d3 > 0.0 && d3 < 1.0 {
851                        // Simple collinear case, 1---2---3---4
852                        return;
853                    }
854                    if d2 <= 0.0 {
855                        d2 = calc_sq_distance(x2, y2, x1, y1);
856                    } else if d2 >= 1.0 {
857                        d2 = calc_sq_distance(x2, y2, x4, y4);
858                    } else {
859                        d2 = calc_sq_distance(x2, y2, x1 + d2 * dx, y1 + d2 * dy);
860                    }
861
862                    if d3 <= 0.0 {
863                        d3 = calc_sq_distance(x3, y3, x1, y1);
864                    } else if d3 >= 1.0 {
865                        d3 = calc_sq_distance(x3, y3, x4, y4);
866                    } else {
867                        d3 = calc_sq_distance(x3, y3, x1 + d3 * dx, y1 + d3 * dy);
868                    }
869                }
870                if d2 > d3 {
871                    if d2 < self.distance_tolerance_square {
872                        self.points.push(PointD { x: x2, y: y2 });
873                        return;
874                    }
875                } else if d3 < self.distance_tolerance_square {
876                    self.points.push(PointD { x: x3, y: y3 });
877                    return;
878                }
879            }
880
881            1 => {
882                // p1,p2,p4 are collinear, p3 is significant
883                if d3 * d3 <= self.distance_tolerance_square * (dx * dx + dy * dy) {
884                    if self.angle_tolerance < CURVE_ANGLE_TOLERANCE_EPSILON {
885                        self.points.push(PointD { x: x23, y: y23 });
886                        return;
887                    }
888
889                    // Angle Condition
890                    let mut da1 = ((y4 - y3).atan2(x4 - x3) - (y3 - y2).atan2(x3 - x2)).abs();
891                    if da1 >= PI {
892                        da1 = 2.0 * PI - da1;
893                    }
894
895                    if da1 < self.angle_tolerance {
896                        self.points.push(PointD { x: x2, y: y2 });
897                        self.points.push(PointD { x: x3, y: y3 });
898                        return;
899                    }
900
901                    if self.cusp_limit != 0.0 && da1 > self.cusp_limit {
902                        self.points.push(PointD { x: x3, y: y3 });
903                        return;
904                    }
905                }
906            }
907
908            2 => {
909                // p1,p3,p4 are collinear, p2 is significant
910                if d2 * d2 <= self.distance_tolerance_square * (dx * dx + dy * dy) {
911                    if self.angle_tolerance < CURVE_ANGLE_TOLERANCE_EPSILON {
912                        self.points.push(PointD { x: x23, y: y23 });
913                        return;
914                    }
915
916                    // Angle Condition
917                    let mut da1 = ((y3 - y2).atan2(x3 - x2) - (y2 - y1).atan2(x2 - x1)).abs();
918                    if da1 >= PI {
919                        da1 = 2.0 * PI - da1;
920                    }
921
922                    if da1 < self.angle_tolerance {
923                        self.points.push(PointD { x: x2, y: y2 });
924                        self.points.push(PointD { x: x3, y: y3 });
925                        return;
926                    }
927
928                    if self.cusp_limit != 0.0 && da1 > self.cusp_limit {
929                        self.points.push(PointD { x: x2, y: y2 });
930                        return;
931                    }
932                }
933            }
934
935            3 => {
936                // Regular case
937                if (d2 + d3) * (d2 + d3) <= self.distance_tolerance_square * (dx * dx + dy * dy) {
938                    if self.angle_tolerance < CURVE_ANGLE_TOLERANCE_EPSILON {
939                        self.points.push(PointD { x: x23, y: y23 });
940                        return;
941                    }
942
943                    // Angle & Cusp Condition
944                    let k = (y3 - y2).atan2(x3 - x2);
945                    let mut da1 = (k - (y2 - y1).atan2(x2 - x1)).abs();
946                    let mut da2 = ((y4 - y3).atan2(x4 - x3) - k).abs();
947                    if da1 >= PI {
948                        da1 = 2.0 * PI - da1;
949                    }
950                    if da2 >= PI {
951                        da2 = 2.0 * PI - da2;
952                    }
953
954                    if da1 + da2 < self.angle_tolerance {
955                        self.points.push(PointD { x: x23, y: y23 });
956                        return;
957                    }
958
959                    if self.cusp_limit != 0.0 {
960                        if da1 > self.cusp_limit {
961                            self.points.push(PointD { x: x2, y: y2 });
962                            return;
963                        }
964
965                        if da2 > self.cusp_limit {
966                            self.points.push(PointD { x: x3, y: y3 });
967                            return;
968                        }
969                    }
970                }
971            }
972
973            _ => unreachable!(),
974        }
975
976        // Continue subdivision
977        self.recursive_bezier(x1, y1, x12, y12, x123, y123, x1234, y1234, level + 1);
978        self.recursive_bezier(x1234, y1234, x234, y234, x34, y34, x4, y4, level + 1);
979    }
980}
981
982impl Default for Curve4Div {
983    fn default() -> Self {
984        Self::new()
985    }
986}
987
988impl VertexSource for Curve4Div {
989    fn rewind(&mut self, _path_id: u32) {
990        self.count = 0;
991    }
992
993    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
994        if self.count >= self.points.len() {
995            return PATH_CMD_STOP;
996        }
997        let p = &self.points[self.count];
998        *x = p.x;
999        *y = p.y;
1000        self.count += 1;
1001        if self.count == 1 {
1002            PATH_CMD_MOVE_TO
1003        } else {
1004            PATH_CMD_LINE_TO
1005        }
1006    }
1007}
1008
1009// ============================================================================
1010// Curve3 — facade
1011// ============================================================================
1012
1013/// Quadratic Bezier curve with selectable algorithm.
1014///
1015/// Defaults to subdivision (`Div`). Delegates to `Curve3Inc` or `Curve3Div`.
1016///
1017/// Port of C++ `agg::curve3`.
1018pub struct Curve3 {
1019    curve_inc: Curve3Inc,
1020    curve_div: Curve3Div,
1021    approximation_method: CurveApproximationMethod,
1022}
1023
1024impl Curve3 {
1025    pub fn new() -> Self {
1026        Self {
1027            curve_inc: Curve3Inc::new(),
1028            curve_div: Curve3Div::new(),
1029            approximation_method: CurveApproximationMethod::Div,
1030        }
1031    }
1032
1033    pub fn new_with_points(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> Self {
1034        let mut c = Self::new();
1035        c.init(x1, y1, x2, y2, x3, y3);
1036        c
1037    }
1038
1039    pub fn reset(&mut self) {
1040        self.curve_inc.reset();
1041        self.curve_div.reset();
1042    }
1043
1044    pub fn init(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) {
1045        if self.approximation_method == CurveApproximationMethod::Inc {
1046            self.curve_inc.init(x1, y1, x2, y2, x3, y3);
1047        } else {
1048            self.curve_div.init(x1, y1, x2, y2, x3, y3);
1049        }
1050    }
1051
1052    pub fn set_approximation_method(&mut self, v: CurveApproximationMethod) {
1053        self.approximation_method = v;
1054    }
1055
1056    pub fn approximation_method(&self) -> CurveApproximationMethod {
1057        self.approximation_method
1058    }
1059
1060    pub fn set_approximation_scale(&mut self, s: f64) {
1061        self.curve_inc.set_approximation_scale(s);
1062        self.curve_div.set_approximation_scale(s);
1063    }
1064
1065    pub fn approximation_scale(&self) -> f64 {
1066        self.curve_inc.approximation_scale()
1067    }
1068
1069    pub fn set_angle_tolerance(&mut self, a: f64) {
1070        self.curve_div.set_angle_tolerance(a);
1071    }
1072
1073    pub fn angle_tolerance(&self) -> f64 {
1074        self.curve_div.angle_tolerance()
1075    }
1076}
1077
1078impl Default for Curve3 {
1079    fn default() -> Self {
1080        Self::new()
1081    }
1082}
1083
1084impl VertexSource for Curve3 {
1085    fn rewind(&mut self, path_id: u32) {
1086        if self.approximation_method == CurveApproximationMethod::Inc {
1087            self.curve_inc.rewind(path_id);
1088        } else {
1089            self.curve_div.rewind(path_id);
1090        }
1091    }
1092
1093    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
1094        if self.approximation_method == CurveApproximationMethod::Inc {
1095            self.curve_inc.vertex(x, y)
1096        } else {
1097            self.curve_div.vertex(x, y)
1098        }
1099    }
1100}
1101
1102// ============================================================================
1103// Curve4 — facade
1104// ============================================================================
1105
1106/// Cubic Bezier curve with selectable algorithm.
1107///
1108/// Defaults to subdivision (`Div`). Delegates to `Curve4Inc` or `Curve4Div`.
1109///
1110/// Port of C++ `agg::curve4`.
1111pub struct Curve4 {
1112    curve_inc: Curve4Inc,
1113    curve_div: Curve4Div,
1114    approximation_method: CurveApproximationMethod,
1115}
1116
1117impl Curve4 {
1118    pub fn new() -> Self {
1119        Self {
1120            curve_inc: Curve4Inc::new(),
1121            curve_div: Curve4Div::new(),
1122            approximation_method: CurveApproximationMethod::Div,
1123        }
1124    }
1125
1126    #[allow(clippy::too_many_arguments)]
1127    pub fn new_with_points(
1128        x1: f64,
1129        y1: f64,
1130        x2: f64,
1131        y2: f64,
1132        x3: f64,
1133        y3: f64,
1134        x4: f64,
1135        y4: f64,
1136    ) -> Self {
1137        let mut c = Self::new();
1138        c.init(x1, y1, x2, y2, x3, y3, x4, y4);
1139        c
1140    }
1141
1142    pub fn new_with_curve4_points(cp: &Curve4Points) -> Self {
1143        let mut c = Self::new();
1144        c.init(cp[0], cp[1], cp[2], cp[3], cp[4], cp[5], cp[6], cp[7]);
1145        c
1146    }
1147
1148    pub fn reset(&mut self) {
1149        self.curve_inc.reset();
1150        self.curve_div.reset();
1151    }
1152
1153    #[allow(clippy::too_many_arguments)]
1154    pub fn init(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) {
1155        if self.approximation_method == CurveApproximationMethod::Inc {
1156            self.curve_inc.init(x1, y1, x2, y2, x3, y3, x4, y4);
1157        } else {
1158            self.curve_div.init(x1, y1, x2, y2, x3, y3, x4, y4);
1159        }
1160    }
1161
1162    pub fn init_with_curve4_points(&mut self, cp: &Curve4Points) {
1163        self.init(cp[0], cp[1], cp[2], cp[3], cp[4], cp[5], cp[6], cp[7]);
1164    }
1165
1166    pub fn set_approximation_method(&mut self, v: CurveApproximationMethod) {
1167        self.approximation_method = v;
1168    }
1169
1170    pub fn approximation_method(&self) -> CurveApproximationMethod {
1171        self.approximation_method
1172    }
1173
1174    pub fn set_approximation_scale(&mut self, s: f64) {
1175        self.curve_inc.set_approximation_scale(s);
1176        self.curve_div.set_approximation_scale(s);
1177    }
1178
1179    pub fn approximation_scale(&self) -> f64 {
1180        self.curve_inc.approximation_scale()
1181    }
1182
1183    pub fn set_angle_tolerance(&mut self, v: f64) {
1184        self.curve_div.set_angle_tolerance(v);
1185    }
1186
1187    pub fn angle_tolerance(&self) -> f64 {
1188        self.curve_div.angle_tolerance()
1189    }
1190
1191    pub fn set_cusp_limit(&mut self, v: f64) {
1192        self.curve_div.set_cusp_limit(v);
1193    }
1194
1195    pub fn cusp_limit(&self) -> f64 {
1196        self.curve_div.cusp_limit()
1197    }
1198}
1199
1200impl Default for Curve4 {
1201    fn default() -> Self {
1202        Self::new()
1203    }
1204}
1205
1206impl VertexSource for Curve4 {
1207    fn rewind(&mut self, path_id: u32) {
1208        if self.approximation_method == CurveApproximationMethod::Inc {
1209            self.curve_inc.rewind(path_id);
1210        } else {
1211            self.curve_div.rewind(path_id);
1212        }
1213    }
1214
1215    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
1216        if self.approximation_method == CurveApproximationMethod::Inc {
1217            self.curve_inc.vertex(x, y)
1218        } else {
1219            self.curve_div.vertex(x, y)
1220        }
1221    }
1222}
1223
1224// ============================================================================
1225// Tests
1226// ============================================================================
1227
1228#[cfg(test)]
1229mod tests {
1230    use super::*;
1231    use crate::basics::is_stop;
1232
1233    /// Collect all vertices from a vertex source.
1234    fn collect_vertices(vs: &mut dyn VertexSource) -> Vec<(f64, f64, u32)> {
1235        vs.rewind(0);
1236        let mut result = Vec::new();
1237        loop {
1238            let mut x = 0.0;
1239            let mut y = 0.0;
1240            let cmd = vs.vertex(&mut x, &mut y);
1241            if is_stop(cmd) {
1242                break;
1243            }
1244            result.push((x, y, cmd));
1245        }
1246        result
1247    }
1248
1249    // --- Curve3Inc tests ---
1250
1251    #[test]
1252    fn test_curve3_inc_basic() {
1253        let mut c = Curve3Inc::new_with_points(0.0, 0.0, 50.0, 100.0, 100.0, 0.0);
1254        let verts = collect_vertices(&mut c);
1255        assert!(verts.len() >= 4);
1256        assert_eq!(verts[0].2, PATH_CMD_MOVE_TO);
1257        assert!((verts[0].0).abs() < 1e-6);
1258        assert!((verts[0].1).abs() < 1e-6);
1259        let last = &verts[verts.len() - 1];
1260        assert!((last.0 - 100.0).abs() < 1e-6);
1261        assert!((last.1).abs() < 1e-6);
1262    }
1263
1264    #[test]
1265    fn test_curve3_inc_reset() {
1266        let mut c = Curve3Inc::new();
1267        c.reset();
1268        let mut x = 0.0;
1269        let mut y = 0.0;
1270        c.rewind(0);
1271        let cmd = c.vertex(&mut x, &mut y);
1272        assert!(is_stop(cmd));
1273    }
1274
1275    #[test]
1276    fn test_curve3_inc_rewind_replays() {
1277        let mut c = Curve3Inc::new_with_points(0.0, 0.0, 50.0, 100.0, 100.0, 0.0);
1278        let verts1 = collect_vertices(&mut c);
1279        let verts2 = collect_vertices(&mut c);
1280        assert_eq!(verts1.len(), verts2.len());
1281        for (a, b) in verts1.iter().zip(verts2.iter()) {
1282            assert!((a.0 - b.0).abs() < 1e-10);
1283            assert!((a.1 - b.1).abs() < 1e-10);
1284        }
1285    }
1286
1287    #[test]
1288    fn test_curve3_inc_scale() {
1289        let mut c1 = Curve3Inc::new();
1290        c1.set_approximation_scale(1.0);
1291        c1.init(0.0, 0.0, 50.0, 100.0, 100.0, 0.0);
1292        let v1 = collect_vertices(&mut c1);
1293
1294        let mut c2 = Curve3Inc::new();
1295        c2.set_approximation_scale(4.0);
1296        c2.init(0.0, 0.0, 50.0, 100.0, 100.0, 0.0);
1297        let v2 = collect_vertices(&mut c2);
1298
1299        assert!(v2.len() > v1.len());
1300    }
1301
1302    // --- Curve3Div tests ---
1303
1304    #[test]
1305    fn test_curve3_div_basic() {
1306        let mut c = Curve3Div::new_with_points(0.0, 0.0, 50.0, 100.0, 100.0, 0.0);
1307        let verts = collect_vertices(&mut c);
1308        assert!(verts.len() >= 3);
1309        assert_eq!(verts[0].2, PATH_CMD_MOVE_TO);
1310        assert!((verts[0].0).abs() < 1e-6);
1311        let last = &verts[verts.len() - 1];
1312        assert!((last.0 - 100.0).abs() < 1e-6);
1313        assert!((last.1).abs() < 1e-6);
1314    }
1315
1316    #[test]
1317    fn test_curve3_div_straight_line() {
1318        // Control point on the line: should produce few points
1319        let mut c = Curve3Div::new_with_points(0.0, 0.0, 50.0, 0.0, 100.0, 0.0);
1320        let verts = collect_vertices(&mut c);
1321        // All y should be 0
1322        for v in &verts {
1323            assert!(v.1.abs() < 1e-6);
1324        }
1325    }
1326
1327    #[test]
1328    fn test_curve3_div_angle_tolerance() {
1329        let mut c = Curve3Div::new();
1330        c.set_angle_tolerance(0.1);
1331        assert!((c.angle_tolerance() - 0.1).abs() < 1e-10);
1332        c.init(0.0, 0.0, 50.0, 100.0, 100.0, 0.0);
1333        let verts = collect_vertices(&mut c);
1334        assert!(verts.len() >= 3);
1335    }
1336
1337    // --- Curve4Inc tests ---
1338
1339    #[test]
1340    fn test_curve4_inc_basic() {
1341        let mut c = Curve4Inc::new_with_points(0.0, 0.0, 33.0, 100.0, 66.0, 100.0, 100.0, 0.0);
1342        let verts = collect_vertices(&mut c);
1343        assert!(verts.len() >= 4);
1344        assert_eq!(verts[0].2, PATH_CMD_MOVE_TO);
1345        assert!((verts[0].0).abs() < 1e-6);
1346        let last = &verts[verts.len() - 1];
1347        assert!((last.0 - 100.0).abs() < 1e-6);
1348        assert!((last.1).abs() < 1e-6);
1349    }
1350
1351    #[test]
1352    fn test_curve4_inc_reset() {
1353        let mut c = Curve4Inc::new();
1354        c.reset();
1355        c.rewind(0);
1356        let mut x = 0.0;
1357        let mut y = 0.0;
1358        let cmd = c.vertex(&mut x, &mut y);
1359        assert!(is_stop(cmd));
1360    }
1361
1362    #[test]
1363    fn test_curve4_inc_curve4_points() {
1364        let cp = Curve4Points::new(0.0, 0.0, 33.0, 100.0, 66.0, 100.0, 100.0, 0.0);
1365        let mut c = Curve4Inc::new_with_curve4_points(&cp);
1366        let verts = collect_vertices(&mut c);
1367        assert!(verts.len() >= 4);
1368    }
1369
1370    #[test]
1371    fn test_curve4_inc_scale() {
1372        let mut c1 = Curve4Inc::new();
1373        c1.set_approximation_scale(1.0);
1374        c1.init(0.0, 0.0, 33.0, 100.0, 66.0, 100.0, 100.0, 0.0);
1375        let v1 = collect_vertices(&mut c1);
1376
1377        let mut c2 = Curve4Inc::new();
1378        c2.set_approximation_scale(4.0);
1379        c2.init(0.0, 0.0, 33.0, 100.0, 66.0, 100.0, 100.0, 0.0);
1380        let v2 = collect_vertices(&mut c2);
1381
1382        assert!(v2.len() > v1.len());
1383    }
1384
1385    // --- Curve4Div tests ---
1386
1387    #[test]
1388    fn test_curve4_div_basic() {
1389        let mut c = Curve4Div::new_with_points(0.0, 0.0, 33.0, 100.0, 66.0, 100.0, 100.0, 0.0);
1390        let verts = collect_vertices(&mut c);
1391        assert!(verts.len() >= 3);
1392        assert_eq!(verts[0].2, PATH_CMD_MOVE_TO);
1393        let last = &verts[verts.len() - 1];
1394        assert!((last.0 - 100.0).abs() < 1e-6);
1395        assert!((last.1).abs() < 1e-6);
1396    }
1397
1398    #[test]
1399    fn test_curve4_div_straight_line() {
1400        let mut c = Curve4Div::new_with_points(0.0, 0.0, 33.0, 0.0, 66.0, 0.0, 100.0, 0.0);
1401        let verts = collect_vertices(&mut c);
1402        for v in &verts {
1403            assert!(v.1.abs() < 1e-6);
1404        }
1405    }
1406
1407    #[test]
1408    fn test_curve4_div_cusp_limit() {
1409        let mut c = Curve4Div::new();
1410        c.set_cusp_limit(0.0);
1411        assert!((c.cusp_limit() - 0.0).abs() < 1e-10);
1412        c.set_cusp_limit(0.5);
1413        assert!((c.cusp_limit() - 0.5).abs() < 1e-10);
1414    }
1415
1416    #[test]
1417    fn test_curve4_div_curve4_points() {
1418        let cp = Curve4Points::new(0.0, 0.0, 33.0, 100.0, 66.0, 100.0, 100.0, 0.0);
1419        let mut c = Curve4Div::new_with_curve4_points(&cp);
1420        let verts = collect_vertices(&mut c);
1421        assert!(verts.len() >= 3);
1422    }
1423
1424    // --- Curve3 facade tests ---
1425
1426    #[test]
1427    fn test_curve3_facade_default_div() {
1428        let c = Curve3::new();
1429        assert_eq!(c.approximation_method(), CurveApproximationMethod::Div);
1430    }
1431
1432    #[test]
1433    fn test_curve3_facade_switch_method() {
1434        let mut c = Curve3::new();
1435        c.set_approximation_method(CurveApproximationMethod::Inc);
1436        c.init(0.0, 0.0, 50.0, 100.0, 100.0, 0.0);
1437        let v_inc = collect_vertices(&mut c);
1438
1439        c.set_approximation_method(CurveApproximationMethod::Div);
1440        c.init(0.0, 0.0, 50.0, 100.0, 100.0, 0.0);
1441        let v_div = collect_vertices(&mut c);
1442
1443        // Both should produce valid output
1444        assert!(v_inc.len() >= 3);
1445        assert!(v_div.len() >= 3);
1446        // Same start point
1447        assert!((v_inc[0].0).abs() < 1e-6);
1448        assert!((v_div[0].0).abs() < 1e-6);
1449    }
1450
1451    // --- Curve4 facade tests ---
1452
1453    #[test]
1454    fn test_curve4_facade_default_div() {
1455        let c = Curve4::new();
1456        assert_eq!(c.approximation_method(), CurveApproximationMethod::Div);
1457    }
1458
1459    #[test]
1460    fn test_curve4_facade_switch_method() {
1461        let mut c = Curve4::new();
1462        c.set_approximation_method(CurveApproximationMethod::Inc);
1463        c.init(0.0, 0.0, 33.0, 100.0, 66.0, 100.0, 100.0, 0.0);
1464        let v_inc = collect_vertices(&mut c);
1465
1466        c.set_approximation_method(CurveApproximationMethod::Div);
1467        c.init(0.0, 0.0, 33.0, 100.0, 66.0, 100.0, 100.0, 0.0);
1468        let v_div = collect_vertices(&mut c);
1469
1470        assert!(v_inc.len() >= 4);
1471        assert!(v_div.len() >= 3);
1472    }
1473
1474    // --- Curve4Points tests ---
1475
1476    #[test]
1477    fn test_curve4_points_index() {
1478        let cp = Curve4Points::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0);
1479        assert_eq!(cp[0], 1.0);
1480        assert_eq!(cp[1], 2.0);
1481        assert_eq!(cp[6], 7.0);
1482        assert_eq!(cp[7], 8.0);
1483    }
1484
1485    #[test]
1486    fn test_curve4_points_init() {
1487        let mut cp = Curve4Points::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1488        cp.init(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0);
1489        assert_eq!(cp[4], 5.0);
1490    }
1491
1492    // --- Conversion function tests ---
1493
1494    #[test]
1495    fn test_catrom_to_bezier() {
1496        let cp = catrom_to_bezier(0.0, 0.0, 10.0, 0.0, 20.0, 0.0, 30.0, 0.0);
1497        // First point = p2
1498        assert!((cp[0] - 10.0).abs() < 1e-6);
1499        assert!(cp[1].abs() < 1e-6);
1500        // Last point = p3
1501        assert!((cp[6] - 20.0).abs() < 1e-6);
1502        assert!(cp[7].abs() < 1e-6);
1503    }
1504
1505    #[test]
1506    fn test_ubspline_to_bezier() {
1507        let cp = ubspline_to_bezier(0.0, 0.0, 10.0, 0.0, 20.0, 0.0, 30.0, 0.0);
1508        // Points should be between the input control points
1509        assert!(cp[0] > 0.0 && cp[0] < 30.0);
1510        assert!(cp[6] > 0.0 && cp[6] < 30.0);
1511    }
1512
1513    #[test]
1514    fn test_hermite_to_bezier() {
1515        let cp = hermite_to_bezier(0.0, 0.0, 100.0, 0.0, 30.0, 0.0, 30.0, 0.0);
1516        // First point = p1
1517        assert!(cp[0].abs() < 1e-6);
1518        assert!(cp[1].abs() < 1e-6);
1519        // Last point = p2
1520        assert!((cp[6] - 100.0).abs() < 1e-6);
1521        assert!(cp[7].abs() < 1e-6);
1522    }
1523
1524    #[test]
1525    fn test_curve3_inc_and_div_same_endpoints() {
1526        // Both algorithms should hit the same start and end points
1527        let mut inc = Curve3Inc::new_with_points(10.0, 20.0, 50.0, 80.0, 90.0, 20.0);
1528        let mut div = Curve3Div::new_with_points(10.0, 20.0, 50.0, 80.0, 90.0, 20.0);
1529        let vi = collect_vertices(&mut inc);
1530        let vd = collect_vertices(&mut div);
1531
1532        // Same start
1533        assert!((vi[0].0 - 10.0).abs() < 1e-6);
1534        assert!((vi[0].1 - 20.0).abs() < 1e-6);
1535        assert!((vd[0].0 - 10.0).abs() < 1e-6);
1536        assert!((vd[0].1 - 20.0).abs() < 1e-6);
1537
1538        // Same end
1539        let li = &vi[vi.len() - 1];
1540        let ld = &vd[vd.len() - 1];
1541        assert!((li.0 - 90.0).abs() < 1e-6);
1542        assert!((li.1 - 20.0).abs() < 1e-6);
1543        assert!((ld.0 - 90.0).abs() < 1e-6);
1544        assert!((ld.1 - 20.0).abs() < 1e-6);
1545    }
1546
1547    #[test]
1548    fn test_curve4_inc_and_div_same_endpoints() {
1549        let mut inc = Curve4Inc::new_with_points(10.0, 20.0, 30.0, 80.0, 70.0, 80.0, 90.0, 20.0);
1550        let mut div = Curve4Div::new_with_points(10.0, 20.0, 30.0, 80.0, 70.0, 80.0, 90.0, 20.0);
1551        let vi = collect_vertices(&mut inc);
1552        let vd = collect_vertices(&mut div);
1553
1554        assert!((vi[0].0 - 10.0).abs() < 1e-6);
1555        assert!((vi[0].1 - 20.0).abs() < 1e-6);
1556        assert!((vd[0].0 - 10.0).abs() < 1e-6);
1557        assert!((vd[0].1 - 20.0).abs() < 1e-6);
1558
1559        let li = &vi[vi.len() - 1];
1560        let ld = &vd[vd.len() - 1];
1561        assert!((li.0 - 90.0).abs() < 1e-6);
1562        assert!((li.1 - 20.0).abs() < 1e-6);
1563        assert!((ld.0 - 90.0).abs() < 1e-6);
1564        assert!((ld.1 - 20.0).abs() < 1e-6);
1565    }
1566}