Skip to main content

bymsdfgen_core/geometry/
edge_segment.rs

1//! The edge segment — a line, a quadratic Bézier, or a cubic Bézier.
2//!
3//! This is the single most important data-oriented change versus the C++ original.
4//! There, `EdgeSegment` is an abstract base with ~11 virtual methods and three
5//! heap-allocated subclasses stored behind `EdgeSegment*` pointers
6//! (`std::vector<EdgeSegment*>`). Every distance evaluation chases a pointer and
7//! pays a vtable lookup.
8//!
9//! Here it is a plain `Copy` enum whose control points live inline. Dispatch is a
10//! `match` (a direct jump the optimizer can inline and vectorize) and contours can
11//! store these contiguously, so the CPU streams the XY data through L1/L2 instead
12//! of pointer-chasing. No heap, no vtable, no manual `clone()`/`delete`.
13
14use crate::geometry::equation_solver::{solve_cubic, solve_quadratic};
15use crate::math::scalar::{non_zero_sign, sign};
16use crate::math::{SignedDistance, Vector2, cross, dot};
17
18const CUBIC_SEARCH_STARTS: i32 = 4;
19const CUBIC_SEARCH_STEPS: i32 = 4;
20
21/// Linear interpolation of two points, matching msdfgen's `mix`:
22/// `(1-t)*a + t*b`. This form returns `a` exactly at `t==0` and `b` exactly at
23/// `t==1` (the `a + (b-a)*t` form does not, which breaks contour-closure checks).
24#[inline]
25fn vmix(a: Vector2, b: Vector2, t: f64) -> Vector2 {
26    a * (1.0 - t) + b * t
27}
28
29/// An edge: linear (2 points), quadratic (3), or cubic (4 control points).
30#[derive(Debug, Clone, Copy, PartialEq)]
31pub enum EdgeSegment {
32    Line([Vector2; 2]),
33    Quadratic([Vector2; 3]),
34    Cubic([Vector2; 4]),
35}
36
37impl EdgeSegment {
38    /// Build a linear segment.
39    #[inline]
40    pub fn line(p0: Vector2, p1: Vector2) -> EdgeSegment {
41        EdgeSegment::Line([p0, p1])
42    }
43
44    /// Build a quadratic, collapsing to a line if the control point is colinear.
45    pub fn quadratic(p0: Vector2, p1: Vector2, p2: Vector2) -> EdgeSegment {
46        if cross(p1 - p0, p2 - p1) == 0.0 {
47            EdgeSegment::Line([p0, p2])
48        } else {
49            EdgeSegment::Quadratic([p0, p1, p2])
50        }
51    }
52
53    /// Build a cubic, collapsing to a line or quadratic when degenerate.
54    pub fn cubic(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2) -> EdgeSegment {
55        let p12 = p2 - p1;
56        if cross(p1 - p0, p12) == 0.0 && cross(p12, p3 - p2) == 0.0 {
57            return EdgeSegment::Line([p0, p3]);
58        }
59        let q = 1.5 * p1 - 0.5 * p0;
60        if q == 1.5 * p2 - 0.5 * p3 {
61            return EdgeSegment::Quadratic([p0, q, p3]);
62        }
63        EdgeSegment::Cubic([p0, p1, p2, p3])
64    }
65
66    /// Polynomial order: 1 = line, 2 = quadratic, 3 = cubic.
67    #[inline]
68    pub fn order(&self) -> usize {
69        match self {
70            EdgeSegment::Line(_) => 1,
71            EdgeSegment::Quadratic(_) => 2,
72            EdgeSegment::Cubic(_) => 3,
73        }
74    }
75
76    /// Control points `[0..=order]`, padded to a fixed array.
77    #[inline]
78    pub fn control_points(&self) -> ([Vector2; 4], usize) {
79        match self {
80            EdgeSegment::Line(p) => ([p[0], p[1], Vector2::ZERO, Vector2::ZERO], 1),
81            EdgeSegment::Quadratic(p) => ([p[0], p[1], p[2], Vector2::ZERO], 2),
82            EdgeSegment::Cubic(p) => (*p, 3),
83        }
84    }
85
86    /// Point on the curve at parameter `t in [0,1]`.
87    pub fn point(&self, t: f64) -> Vector2 {
88        match self {
89            EdgeSegment::Line(p) => vmix(p[0], p[1], t),
90            EdgeSegment::Quadratic(p) => vmix(vmix(p[0], p[1], t), vmix(p[1], p[2], t), t),
91            EdgeSegment::Cubic(p) => {
92                let p12 = vmix(p[1], p[2], t);
93                vmix(
94                    vmix(vmix(p[0], p[1], t), p12, t),
95                    vmix(p12, vmix(p[2], p[3], t), t),
96                    t,
97                )
98            }
99        }
100    }
101
102    /// Tangent (unnormalized direction) at parameter `t`.
103    pub fn direction(&self, t: f64) -> Vector2 {
104        match self {
105            EdgeSegment::Line(p) => p[1] - p[0],
106            EdgeSegment::Quadratic(p) => {
107                let tangent = vmix(p[1] - p[0], p[2] - p[1], t);
108                if !tangent.is_nonzero() {
109                    p[2] - p[0]
110                } else {
111                    tangent
112                }
113            }
114            EdgeSegment::Cubic(p) => {
115                let tangent = vmix(
116                    vmix(p[1] - p[0], p[2] - p[1], t),
117                    vmix(p[2] - p[1], p[3] - p[2], t),
118                    t,
119                );
120                if !tangent.is_nonzero() {
121                    if t == 0.0 {
122                        return p[2] - p[0];
123                    }
124                    if t == 1.0 {
125                        return p[3] - p[1];
126                    }
127                }
128                tangent
129            }
130        }
131    }
132
133    /// Second derivative direction (rate of tangent change) at parameter `t`.
134    pub fn direction_change(&self, t: f64) -> Vector2 {
135        match self {
136            EdgeSegment::Line(_) => Vector2::ZERO,
137            EdgeSegment::Quadratic(p) => (p[2] - p[1]) - (p[1] - p[0]),
138            EdgeSegment::Cubic(p) => vmix(
139                (p[2] - p[1]) - (p[1] - p[0]),
140                (p[3] - p[2]) - (p[2] - p[1]),
141                t,
142            ),
143        }
144    }
145
146    /// Arc length of the segment.
147    pub fn length(&self) -> f64 {
148        match self {
149            EdgeSegment::Line(p) => (p[1] - p[0]).length(),
150            EdgeSegment::Quadratic(p) => {
151                let ab = p[1] - p[0];
152                let br = p[2] - p[1] - ab;
153                let abab = dot(ab, ab);
154                let abbr = dot(ab, br);
155                let brbr = dot(br, br);
156                let ab_len = abab.sqrt();
157                let br_len = brbr.sqrt();
158                let crs = cross(ab, br);
159                let h = (abab + abbr + abbr + brbr).sqrt();
160                (br_len * ((abbr + brbr) * h - abbr * ab_len)
161                    + crs * crs * ((br_len * h + abbr + brbr) / (br_len * ab_len + abbr)).ln())
162                    / (brbr * br_len)
163            }
164            // Cubic length has no closed form; the original does not implement it.
165            EdgeSegment::Cubic(_) => f64::NAN,
166        }
167    }
168
169    /// Signed distance from `origin` to the segment, plus the parameter `t` of the
170    /// nearest point. Mirrors `signedDistance(origin, &param)`.
171    pub fn signed_distance(&self, origin: Vector2) -> (SignedDistance, f64) {
172        match self {
173            EdgeSegment::Line(p) => self.linear_signed_distance(p, origin),
174            EdgeSegment::Quadratic(p) => self.quadratic_signed_distance(p, origin),
175            EdgeSegment::Cubic(p) => self.cubic_signed_distance(p, origin),
176        }
177    }
178
179    fn linear_signed_distance(&self, p: &[Vector2; 2], origin: Vector2) -> (SignedDistance, f64) {
180        let aq = origin - p[0];
181        let ab = p[1] - p[0];
182        let param = dot(aq, ab) / dot(ab, ab);
183        let eq = (if param > 0.5 { p[1] } else { p[0] }) - origin;
184        let endpoint_distance = eq.length();
185        if param > 0.0 && param < 1.0 {
186            let ortho_distance = dot(ab.orthonormal(false, false), aq);
187            if ortho_distance.abs() < endpoint_distance {
188                return (SignedDistance::new(ortho_distance, 0.0), param);
189            }
190        }
191        let sd = SignedDistance::new(
192            non_zero_sign(cross(aq, ab)) as f64 * endpoint_distance,
193            dot(ab.normalize(false), eq.normalize(false)).abs(),
194        );
195        (sd, param)
196    }
197
198    fn quadratic_signed_distance(
199        &self,
200        p: &[Vector2; 3],
201        origin: Vector2,
202    ) -> (SignedDistance, f64) {
203        let qa = p[0] - origin;
204        let ab = p[1] - p[0];
205        let br = p[2] - p[1] - ab;
206        let a = dot(br, br);
207        let b = 3.0 * dot(ab, br);
208        let c = 2.0 * dot(ab, ab) + dot(qa, br);
209        let d = dot(qa, ab);
210        let solutions = solve_cubic(a, b, c, d);
211
212        let mut ep_dir = self.direction(0.0);
213        let mut min_distance = non_zero_sign(cross(ep_dir, qa)) as f64 * qa.length(); // from A
214        let mut param = -dot(qa, ep_dir) / dot(ep_dir, ep_dir);
215        {
216            let distance = (p[2] - origin).length(); // from B
217            if distance < min_distance.abs() {
218                ep_dir = self.direction(1.0);
219                min_distance = non_zero_sign(cross(ep_dir, p[2] - origin)) as f64 * distance;
220                param = dot(origin - p[1], ep_dir) / dot(ep_dir, ep_dir);
221            }
222        }
223        for &t in solutions.iter() {
224            if t > 0.0 && t < 1.0 {
225                let qe = qa + 2.0 * t * ab + (t * t) * br;
226                let distance = qe.length();
227                if distance <= min_distance.abs() {
228                    min_distance = non_zero_sign(cross(ab + t * br, qe)) as f64 * distance;
229                    param = t;
230                }
231            }
232        }
233
234        if (0.0..=1.0).contains(&param) {
235            (SignedDistance::new(min_distance, 0.0), param)
236        } else if param < 0.5 {
237            (
238                SignedDistance::new(
239                    min_distance,
240                    dot(self.direction(0.0).normalize(false), qa.normalize(false)).abs(),
241                ),
242                param,
243            )
244        } else {
245            (
246                SignedDistance::new(
247                    min_distance,
248                    dot(
249                        self.direction(1.0).normalize(false),
250                        (p[2] - origin).normalize(false),
251                    )
252                    .abs(),
253                ),
254                param,
255            )
256        }
257    }
258
259    fn cubic_signed_distance(&self, p: &[Vector2; 4], origin: Vector2) -> (SignedDistance, f64) {
260        let qa = p[0] - origin;
261        let ab = p[1] - p[0];
262        let br = p[2] - p[1] - ab;
263        let as_ = (p[3] - p[2]) - (p[2] - p[1]) - br;
264
265        let mut ep_dir = self.direction(0.0);
266        let mut min_distance = non_zero_sign(cross(ep_dir, qa)) as f64 * qa.length(); // from A
267        let mut param = -dot(qa, ep_dir) / dot(ep_dir, ep_dir);
268        {
269            let distance = (p[3] - origin).length(); // from B
270            if distance < min_distance.abs() {
271                ep_dir = self.direction(1.0);
272                min_distance = non_zero_sign(cross(ep_dir, p[3] - origin)) as f64 * distance;
273                param = dot(ep_dir - (p[3] - origin), ep_dir) / dot(ep_dir, ep_dir);
274            }
275        }
276        // Iterative minimum-distance search (Newton with curvature term).
277        for i in 0..=CUBIC_SEARCH_STARTS {
278            let mut t = (1.0 / CUBIC_SEARCH_STARTS as f64) * i as f64;
279            let mut qe = qa + 3.0 * t * ab + 3.0 * (t * t) * br + (t * t * t) * as_;
280            let mut d1 = 3.0 * ab + 6.0 * t * br + 3.0 * (t * t) * as_;
281            let mut d2 = 6.0 * br + 6.0 * t * as_;
282            let mut improved_t = t - dot(qe, d1) / (dot(d1, d1) + dot(qe, d2));
283            if improved_t > 0.0 && improved_t < 1.0 {
284                let mut remaining_steps = CUBIC_SEARCH_STEPS;
285                loop {
286                    t = improved_t;
287                    qe = qa + 3.0 * t * ab + 3.0 * (t * t) * br + (t * t * t) * as_;
288                    d1 = 3.0 * ab + 6.0 * t * br + 3.0 * (t * t) * as_;
289                    remaining_steps -= 1;
290                    if remaining_steps == 0 {
291                        break;
292                    }
293                    d2 = 6.0 * br + 6.0 * t * as_;
294                    improved_t = t - dot(qe, d1) / (dot(d1, d1) + dot(qe, d2));
295                    if !(improved_t > 0.0 && improved_t < 1.0) {
296                        break;
297                    }
298                }
299                let distance = qe.length();
300                if distance < min_distance.abs() {
301                    min_distance = non_zero_sign(cross(d1, qe)) as f64 * distance;
302                    param = t;
303                }
304            }
305        }
306
307        if (0.0..=1.0).contains(&param) {
308            (SignedDistance::new(min_distance, 0.0), param)
309        } else if param < 0.5 {
310            (
311                SignedDistance::new(
312                    min_distance,
313                    dot(self.direction(0.0).normalize(false), qa.normalize(false)).abs(),
314                ),
315                param,
316            )
317        } else {
318            (
319                SignedDistance::new(
320                    min_distance,
321                    dot(
322                        self.direction(1.0).normalize(false),
323                        (p[3] - origin).normalize(false),
324                    )
325                    .abs(),
326                ),
327                param,
328            )
329        }
330    }
331
332    /// Converts a true signed distance to a perpendicular one when the closest
333    /// point falls outside the `[0,1]` domain. Port of
334    /// `distanceToPerpendicularDistance`.
335    pub fn distance_to_perpendicular_distance(
336        &self,
337        distance: &mut SignedDistance,
338        origin: Vector2,
339        param: f64,
340    ) {
341        if param < 0.0 {
342            let dir = self.direction(0.0).normalize(false);
343            let aq = origin - self.point(0.0);
344            let ts = dot(aq, dir);
345            if ts < 0.0 {
346                let perpendicular_distance = cross(aq, dir);
347                if perpendicular_distance.abs() <= distance.distance.abs() {
348                    distance.distance = perpendicular_distance;
349                    distance.dot = 0.0;
350                }
351            }
352        } else if param > 1.0 {
353            let dir = self.direction(1.0).normalize(false);
354            let bq = origin - self.point(1.0);
355            let ts = dot(bq, dir);
356            if ts > 0.0 {
357                let perpendicular_distance = cross(bq, dir);
358                if perpendicular_distance.abs() <= distance.distance.abs() {
359                    distance.distance = perpendicular_distance;
360                    distance.dot = 0.0;
361                }
362            }
363        }
364    }
365
366    /// Horizontal scanline intersections at height `y`. Writes up to 3 `(x, dy)`
367    /// pairs into `out` and returns the count. `dy` is the crossing direction.
368    pub fn scanline_intersections(&self, y: f64, out: &mut [(f64, i32); 3]) -> usize {
369        match self {
370            EdgeSegment::Line(p) => Self::linear_scanline(p, y, out),
371            EdgeSegment::Quadratic(p) => Self::quadratic_scanline(p, y, out),
372            EdgeSegment::Cubic(p) => Self::cubic_scanline(p, y, out),
373        }
374    }
375
376    fn linear_scanline(p: &[Vector2; 2], y: f64, out: &mut [(f64, i32); 3]) -> usize {
377        if (y >= p[0].y && y < p[1].y) || (y >= p[1].y && y < p[0].y) {
378            let param = (y - p[0].y) / (p[1].y - p[0].y);
379            out[0] = (
380                crate::math::scalar::mix(p[0].x, p[1].x, param),
381                sign(p[1].y - p[0].y),
382            );
383            1
384        } else {
385            0
386        }
387    }
388
389    fn quadratic_scanline(p: &[Vector2; 3], y: f64, out: &mut [(f64, i32); 3]) -> usize {
390        let mut total = 0usize;
391        let mut next_dy = if y > p[0].y { 1 } else { -1 };
392        out[total].0 = p[0].x;
393        if p[0].y == y {
394            if p[0].y < p[1].y || (p[0].y == p[1].y && p[0].y < p[2].y) {
395                out[total].1 = 1;
396                total += 1;
397            } else {
398                next_dy = 1;
399            }
400        }
401        {
402            let ab = p[1] - p[0];
403            let br = p[2] - p[1] - ab;
404            let mut t = solve_quadratic(br.y, 2.0 * ab.y, p[0].y - y).roots();
405            if t.len() >= 2 && t[0] > t[1] {
406                t.swap(0, 1);
407            }
408            let mut i = 0;
409            while i < t.len() && total < 2 {
410                let ti = t[i];
411                if (0.0..=1.0).contains(&ti) {
412                    out[total].0 = p[0].x + 2.0 * ti * ab.x + ti * ti * br.x;
413                    if next_dy as f64 * (ab.y + ti * br.y) >= 0.0 {
414                        out[total].1 = next_dy;
415                        total += 1;
416                        next_dy = -next_dy;
417                    }
418                }
419                i += 1;
420            }
421        }
422        if p[2].y == y {
423            if next_dy > 0 && total > 0 {
424                total -= 1;
425                next_dy = -1;
426            }
427            if (p[2].y < p[1].y || (p[2].y == p[1].y && p[2].y < p[0].y)) && total < 2 {
428                out[total].0 = p[2].x;
429                if next_dy < 0 {
430                    out[total].1 = -1;
431                    total += 1;
432                    next_dy = 1;
433                }
434            }
435        }
436        if next_dy != (if y >= p[2].y { 1 } else { -1 }) {
437            if total > 0 {
438                total -= 1;
439            } else {
440                if (p[2].y - y).abs() < (p[0].y - y).abs() {
441                    out[total].0 = p[2].x;
442                }
443                out[total].1 = next_dy;
444                total += 1;
445            }
446        }
447        total
448    }
449
450    fn cubic_scanline(p: &[Vector2; 4], y: f64, out: &mut [(f64, i32); 3]) -> usize {
451        let mut total = 0usize;
452        let mut next_dy = if y > p[0].y { 1 } else { -1 };
453        out[total].0 = p[0].x;
454        if p[0].y == y {
455            if p[0].y < p[1].y
456                || (p[0].y == p[1].y && (p[0].y < p[2].y || (p[0].y == p[2].y && p[0].y < p[3].y)))
457            {
458                out[total].1 = 1;
459                total += 1;
460            } else {
461                next_dy = 1;
462            }
463        }
464        {
465            let ab = p[1] - p[0];
466            let br = p[2] - p[1] - ab;
467            let as_ = (p[3] - p[2]) - (p[2] - p[1]) - br;
468            let mut t = solve_cubic(as_.y, 3.0 * br.y, 3.0 * ab.y, p[0].y - y);
469            // sort up to 3 roots
470            if t.len() >= 2 {
471                if t[0] > t[1] {
472                    t.swap(0, 1);
473                }
474                if t.len() >= 3 && t[1] > t[2] {
475                    t.swap(1, 2);
476                    if t[0] > t[1] {
477                        t.swap(0, 1);
478                    }
479                }
480            }
481            let mut i = 0;
482            while i < t.len() && total < 3 {
483                let ti = t[i];
484                if (0.0..=1.0).contains(&ti) {
485                    out[total].0 =
486                        p[0].x + 3.0 * ti * ab.x + 3.0 * ti * ti * br.x + ti * ti * ti * as_.x;
487                    if next_dy as f64 * (ab.y + 2.0 * ti * br.y + ti * ti * as_.y) >= 0.0 {
488                        out[total].1 = next_dy;
489                        total += 1;
490                        next_dy = -next_dy;
491                    }
492                }
493                i += 1;
494            }
495        }
496        if p[3].y == y {
497            if next_dy > 0 && total > 0 {
498                total -= 1;
499                next_dy = -1;
500            }
501            if (p[3].y < p[2].y
502                || (p[3].y == p[2].y && (p[3].y < p[1].y || (p[3].y == p[1].y && p[3].y < p[0].y))))
503                && total < 3
504            {
505                out[total].0 = p[3].x;
506                if next_dy < 0 {
507                    out[total].1 = -1;
508                    total += 1;
509                    next_dy = 1;
510                }
511            }
512        }
513        if next_dy != (if y >= p[3].y { 1 } else { -1 }) {
514            if total > 0 {
515                total -= 1;
516            } else {
517                if (p[3].y - y).abs() < (p[0].y - y).abs() {
518                    out[total].0 = p[3].x;
519                }
520                out[total].1 = next_dy;
521                total += 1;
522            }
523        }
524        total
525    }
526
527    /// Expand `(min, max)` to include this segment's bounding box.
528    pub fn bound(&self, min: &mut Vector2, max: &mut Vector2) {
529        let mut pb = |p: Vector2| {
530            if p.x < min.x {
531                min.x = p.x;
532            }
533            if p.y < min.y {
534                min.y = p.y;
535            }
536            if p.x > max.x {
537                max.x = p.x;
538            }
539            if p.y > max.y {
540                max.y = p.y;
541            }
542        };
543        match self {
544            EdgeSegment::Line(p) => {
545                pb(p[0]);
546                pb(p[1]);
547            }
548            EdgeSegment::Quadratic(p) => {
549                pb(p[0]);
550                pb(p[2]);
551                let bot = (p[1] - p[0]) - (p[2] - p[1]);
552                if bot.x != 0.0 {
553                    let param = (p[1].x - p[0].x) / bot.x;
554                    if param > 0.0 && param < 1.0 {
555                        pb(self.point(param));
556                    }
557                }
558                if bot.y != 0.0 {
559                    let param = (p[1].y - p[0].y) / bot.y;
560                    if param > 0.0 && param < 1.0 {
561                        pb(self.point(param));
562                    }
563                }
564            }
565            EdgeSegment::Cubic(p) => {
566                pb(p[0]);
567                pb(p[3]);
568                let a0 = p[1] - p[0];
569                let a1 = 2.0 * (p[2] - p[1] - a0);
570                let a2 = p[3] - 3.0 * p[2] + 3.0 * p[1] - p[0];
571                for &param in solve_quadratic(a2.x, a1.x, a0.x).roots().iter() {
572                    if param > 0.0 && param < 1.0 {
573                        pb(self.point(param));
574                    }
575                }
576                for &param in solve_quadratic(a2.y, a1.y, a0.y).roots().iter() {
577                    if param > 0.0 && param < 1.0 {
578                        pb(self.point(param));
579                    }
580                }
581            }
582        }
583    }
584
585    /// Reverse the parameterization direction.
586    pub fn reverse(&mut self) {
587        match self {
588            EdgeSegment::Line(p) => p.swap(0, 1),
589            EdgeSegment::Quadratic(p) => p.swap(0, 2),
590            EdgeSegment::Cubic(p) => {
591                p.swap(0, 3);
592                p.swap(1, 2);
593            }
594        }
595    }
596
597    /// Move the start point, adjusting controls (port of `moveStartPoint`).
598    pub fn move_start_point(&mut self, to: Vector2) {
599        match self {
600            EdgeSegment::Line(p) => p[0] = to,
601            EdgeSegment::Quadratic(p) => {
602                let orig_s_dir = p[0] - p[1];
603                let orig_p1 = p[1];
604                p[1] = p[1]
605                    + cross(p[0] - p[1], to - p[0]) / cross(p[0] - p[1], p[2] - p[1])
606                        * (p[2] - p[1]);
607                p[0] = to;
608                if dot(orig_s_dir, p[0] - p[1]) < 0.0 {
609                    p[1] = orig_p1;
610                }
611            }
612            EdgeSegment::Cubic(p) => {
613                p[1] = p[1] + (to - p[0]);
614                p[0] = to;
615            }
616        }
617    }
618
619    /// Move the end point, adjusting controls (port of `moveEndPoint`).
620    pub fn move_end_point(&mut self, to: Vector2) {
621        match self {
622            EdgeSegment::Line(p) => p[1] = to,
623            EdgeSegment::Quadratic(p) => {
624                let orig_e_dir = p[2] - p[1];
625                let orig_p1 = p[1];
626                p[1] = p[1]
627                    + cross(p[2] - p[1], to - p[2]) / cross(p[2] - p[1], p[0] - p[1])
628                        * (p[0] - p[1]);
629                p[2] = to;
630                if dot(orig_e_dir, p[2] - p[1]) < 0.0 {
631                    p[1] = orig_p1;
632                }
633            }
634            EdgeSegment::Cubic(p) => {
635                p[2] = p[2] + (to - p[3]);
636                p[3] = to;
637            }
638        }
639    }
640
641    /// Split into three equal-parameter thirds (used by `Shape::normalize`).
642    pub fn split_in_thirds(&self) -> [EdgeSegment; 3] {
643        match self {
644            EdgeSegment::Line(_) => [
645                EdgeSegment::line(self.point(0.0), self.point(1.0 / 3.0)),
646                EdgeSegment::line(self.point(1.0 / 3.0), self.point(2.0 / 3.0)),
647                EdgeSegment::line(self.point(2.0 / 3.0), self.point(1.0)),
648            ],
649            EdgeSegment::Quadratic(p) => [
650                EdgeSegment::Quadratic([p[0], vmix(p[0], p[1], 1.0 / 3.0), self.point(1.0 / 3.0)]),
651                EdgeSegment::Quadratic([
652                    self.point(1.0 / 3.0),
653                    vmix(
654                        vmix(p[0], p[1], 5.0 / 9.0),
655                        vmix(p[1], p[2], 4.0 / 9.0),
656                        0.5,
657                    ),
658                    self.point(2.0 / 3.0),
659                ]),
660                EdgeSegment::Quadratic([self.point(2.0 / 3.0), vmix(p[1], p[2], 2.0 / 3.0), p[2]]),
661            ],
662            EdgeSegment::Cubic(p) => [
663                EdgeSegment::Cubic([
664                    p[0],
665                    if p[0] == p[1] {
666                        p[0]
667                    } else {
668                        vmix(p[0], p[1], 1.0 / 3.0)
669                    },
670                    vmix(
671                        vmix(p[0], p[1], 1.0 / 3.0),
672                        vmix(p[1], p[2], 1.0 / 3.0),
673                        1.0 / 3.0,
674                    ),
675                    self.point(1.0 / 3.0),
676                ]),
677                EdgeSegment::Cubic([
678                    self.point(1.0 / 3.0),
679                    vmix(
680                        vmix(
681                            vmix(p[0], p[1], 1.0 / 3.0),
682                            vmix(p[1], p[2], 1.0 / 3.0),
683                            1.0 / 3.0,
684                        ),
685                        vmix(
686                            vmix(p[1], p[2], 1.0 / 3.0),
687                            vmix(p[2], p[3], 1.0 / 3.0),
688                            1.0 / 3.0,
689                        ),
690                        2.0 / 3.0,
691                    ),
692                    vmix(
693                        vmix(
694                            vmix(p[0], p[1], 2.0 / 3.0),
695                            vmix(p[1], p[2], 2.0 / 3.0),
696                            2.0 / 3.0,
697                        ),
698                        vmix(
699                            vmix(p[1], p[2], 2.0 / 3.0),
700                            vmix(p[2], p[3], 2.0 / 3.0),
701                            2.0 / 3.0,
702                        ),
703                        1.0 / 3.0,
704                    ),
705                    self.point(2.0 / 3.0),
706                ]),
707                EdgeSegment::Cubic([
708                    self.point(2.0 / 3.0),
709                    vmix(
710                        vmix(p[1], p[2], 2.0 / 3.0),
711                        vmix(p[2], p[3], 2.0 / 3.0),
712                        2.0 / 3.0,
713                    ),
714                    if p[2] == p[3] {
715                        p[3]
716                    } else {
717                        vmix(p[2], p[3], 2.0 / 3.0)
718                    },
719                    p[3],
720                ]),
721            ],
722        }
723    }
724
725    /// Promote a quadratic to an equivalent cubic (port of `convertToCubic`).
726    pub fn convert_to_cubic(&self) -> EdgeSegment {
727        match self {
728            EdgeSegment::Quadratic(p) => EdgeSegment::Cubic([
729                p[0],
730                vmix(p[0], p[1], 2.0 / 3.0),
731                vmix(p[1], p[2], 1.0 / 3.0),
732                p[2],
733            ]),
734            other => *other,
735        }
736    }
737}
738
739#[cfg(test)]
740mod tests {
741    use super::*;
742
743    #[test]
744    fn line_distance_perpendicular() {
745        let e = EdgeSegment::line(Vector2::new(0.0, 0.0), Vector2::new(10.0, 0.0));
746        let (sd, t) = e.signed_distance(Vector2::new(5.0, 3.0));
747        assert!((sd.distance.abs() - 3.0).abs() < 1e-9);
748        assert!((t - 0.5).abs() < 1e-9);
749    }
750
751    #[test]
752    fn line_endpoints() {
753        let e = EdgeSegment::line(Vector2::new(0.0, 0.0), Vector2::new(10.0, 0.0));
754        assert_eq!(e.point(0.0), Vector2::new(0.0, 0.0));
755        assert_eq!(e.point(1.0), Vector2::new(10.0, 0.0));
756        assert_eq!(e.point(0.5), Vector2::new(5.0, 0.0));
757    }
758
759    #[test]
760    fn quadratic_collapses_to_line() {
761        // colinear control point
762        let e = EdgeSegment::quadratic(
763            Vector2::new(0.0, 0.0),
764            Vector2::new(1.0, 0.0),
765            Vector2::new(2.0, 0.0),
766        );
767        assert!(matches!(e, EdgeSegment::Line(_)));
768    }
769
770    #[test]
771    fn reverse_line() {
772        let mut e = EdgeSegment::line(Vector2::new(0.0, 0.0), Vector2::new(10.0, 0.0));
773        e.reverse();
774        assert_eq!(e.point(0.0), Vector2::new(10.0, 0.0));
775    }
776
777    #[test]
778    fn scanline_line_crossing() {
779        let e = EdgeSegment::line(Vector2::new(0.0, 0.0), Vector2::new(0.0, 10.0));
780        let mut out = [(0.0, 0); 3];
781        let n = e.scanline_intersections(5.0, &mut out);
782        assert_eq!(n, 1);
783        assert!((out[0].0 - 0.0).abs() < 1e-9);
784        assert_eq!(out[0].1, 1);
785    }
786}