write_fonts/tables/gvar/
iup.rs

1//! Interpolate Untouched Points
2//!
3//! This module contains code for optimizing variable glyph deltas, by removing
4//! deltas that can be interpolated.
5//!
6//! See [Inferred deltas for un-referenced point numbers][spec] for more information.
7//!
8//! [spec]: https://learn.microsoft.com/en-us/typography/opentype/spec/gvar#inferred-deltas-for-un-referenced-point-numbers
9
10use std::collections::HashSet;
11
12use super::GlyphDelta;
13use crate::{round::OtRound, util::WrappingGet};
14
15use kurbo::{Point, Vec2};
16
17const NUM_PHANTOM_POINTS: usize = 4;
18
19/// For the outline given in `coords`, with contour endpoints given
20/// `ends`, optimize a set of delta values `deltas` within error `tolerance`.
21///
22/// For each delta in the input, returns an [`GlyphDelta`] with a flag
23/// indicating whether or not it can be interpolated.
24///
25/// See:
26/// * <https://github.com/fonttools/fonttools/blob/6a13bdc2e668334b04466b288d31179df1cff7be/Lib/fontTools/varLib/iup.py#L470>
27/// * <https://learn.microsoft.com/en-us/typography/opentype/spec/gvar#inferred-deltas-for-un-referenced-point-numbers>
28pub fn iup_delta_optimize(
29    deltas: Vec<Vec2>,
30    coords: Vec<Point>,
31    tolerance: f64,
32    contour_ends: &[usize],
33) -> Result<Vec<GlyphDelta>, IupError> {
34    let num_coords = coords.len();
35    if num_coords < NUM_PHANTOM_POINTS {
36        return Err(IupError::NotEnoughCoords(num_coords));
37    }
38    if deltas.len() != coords.len() {
39        return Err(IupError::DeltaCoordLengthMismatch {
40            num_deltas: deltas.len(),
41            num_coords: coords.len(),
42        });
43    }
44
45    let mut contour_ends = contour_ends.to_vec();
46    contour_ends.sort();
47
48    let expected_num_coords = contour_ends
49        .last()
50        .copied()
51        .map(|v| v + 1)
52        .unwrap_or_default()
53        + NUM_PHANTOM_POINTS;
54    if num_coords != expected_num_coords {
55        return Err(IupError::CoordEndsMismatch {
56            num_coords,
57            expected_num_coords,
58        });
59    }
60
61    for offset in (1..=4).rev() {
62        contour_ends.push(num_coords.saturating_sub(offset));
63    }
64
65    let mut result = Vec::with_capacity(num_coords);
66    let mut start = 0;
67    let mut deltas = deltas;
68    let mut coords = coords;
69    for end in contour_ends {
70        let contour = iup_contour_optimize(
71            &mut deltas[start..=end],
72            &mut coords[start..=end],
73            tolerance,
74        )?;
75        result.extend_from_slice(&contour);
76        assert_eq!(contour.len() + start, end + 1);
77        start = end + 1;
78    }
79    Ok(result)
80}
81
82#[derive(Clone, Debug)]
83pub enum IupError {
84    DeltaCoordLengthMismatch {
85        num_deltas: usize,
86        num_coords: usize,
87    },
88    NotEnoughCoords(usize),
89    CoordEndsMismatch {
90        num_coords: usize,
91        expected_num_coords: usize,
92    },
93    AchievedInvalidState(String),
94}
95
96/// Check if IUP _might_ be possible. If not then we *must* encode the value at this index.
97///
98/// <https://github.com/fonttools/fonttools/blob/6a13bdc2e668334b04466b288d31179df1cff7be/Lib/fontTools/varLib/iup.py#L238-L290>
99fn must_encode_at(deltas: &[Vec2], coords: &[Point], tolerance: f64, at: usize) -> bool {
100    let ld = *deltas.wrapping_prev(at);
101    let d = deltas[at];
102    let nd = *deltas.wrapping_next(at);
103    let lc = *coords.wrapping_prev(at);
104    let c = coords[at];
105    let nc = *coords.wrapping_next(at);
106
107    for axis in [Axis2D::X, Axis2D::Y] {
108        let (ldj, lcj) = (ld.get(axis), lc.get(axis));
109        let (dj, cj) = (d.get(axis), c.get(axis));
110        let (ndj, ncj) = (nd.get(axis), nc.get(axis));
111        let (c1, c2, d1, d2) = if lcj <= ncj {
112            (lcj, ncj, ldj, ndj)
113        } else {
114            (ncj, lcj, ndj, ldj)
115        };
116        // If the two coordinates are the same, then the interpolation
117        // algorithm produces the same delta if both deltas are equal,
118        // and zero if they differ.
119        match (c1, c2) {
120            _ if c1 == c2 => {
121                if (d1 - d2).abs() > tolerance && dj.abs() > tolerance {
122                    return true;
123                }
124            }
125            _ if c1 <= cj && cj <= c2 => {
126                // and c1 != c2
127                // If coordinate for current point is between coordinate of adjacent
128                // points on the two sides, but the delta for current point is NOT
129                // between delta for those adjacent points (considering tolerance
130                // allowance), then there is no way that current point can be IUP-ed.
131                if !(d1.min(d2) - tolerance <= dj && dj <= d1.max(d2) + tolerance) {
132                    return true;
133                }
134            }
135            _ => {
136                // cj < c1 or c2 < cj
137                // Otherwise, the delta should either match the closest, or have the
138                // same sign as the interpolation of the two deltas.
139                if d1 != d2 && dj.abs() > tolerance {
140                    if cj < c1 {
141                        if ((dj - d1).abs() > tolerance) && (dj - tolerance < d1) != (d1 < d2) {
142                            return true;
143                        }
144                    } else if ((dj - d2).abs() > tolerance) && (d2 < dj + tolerance) != (d1 < d2) {
145                        return true;
146                    }
147                }
148            }
149        }
150    }
151    false
152}
153
154/// Indices of deltas that must be encoded explicitly because they can't be interpolated.
155///
156/// These deltas must be encoded explicitly. That allows us the dynamic
157/// programming solution clear stop points which speeds it up considerably.
158///
159/// Rust port of <https://github.com/fonttools/fonttools/blob/6a13bdc2e668334b04466b288d31179df1cff7be/Lib/fontTools/varLib/iup.py#L218>
160fn iup_must_encode(
161    deltas: &[Vec2],
162    coords: &[Point],
163    tolerance: f64,
164) -> Result<HashSet<usize>, IupError> {
165    Ok((0..deltas.len())
166        .rev()
167        .filter(|i| must_encode_at(deltas, coords, tolerance, *i))
168        .collect())
169}
170
171// TODO: use for might_iup? - take point and vec and coord
172#[derive(Copy, Clone, Debug)]
173enum Axis2D {
174    X,
175    Y,
176}
177
178/// Some of the fonttools code loops over 0,1 to access x/y.
179///
180/// Attempt to provide a nice way to do the same in Rust.
181trait Coord {
182    fn get(&self, axis: Axis2D) -> f64;
183    fn set(&mut self, coord: Axis2D, value: f64);
184}
185
186impl Coord for Point {
187    fn get(&self, axis: Axis2D) -> f64 {
188        match axis {
189            Axis2D::X => self.x,
190            Axis2D::Y => self.y,
191        }
192    }
193
194    fn set(&mut self, axis: Axis2D, value: f64) {
195        match axis {
196            Axis2D::X => self.x = value,
197            Axis2D::Y => self.y = value,
198        }
199    }
200}
201
202impl Coord for Vec2 {
203    fn get(&self, axis: Axis2D) -> f64 {
204        match axis {
205            Axis2D::X => self.x,
206            Axis2D::Y => self.y,
207        }
208    }
209
210    fn set(&mut self, axis: Axis2D, value: f64) {
211        match axis {
212            Axis2D::X => self.x = value,
213            Axis2D::Y => self.y = value,
214        }
215    }
216}
217
218/// Given two reference coordinates `rc1` & `rc2` and their respective
219/// delta vectors `rd1` & `rd2`, returns interpolated deltas for the set of
220/// coordinates `coords`.
221///
222/// <https://github.com/fonttools/fonttools/blob/6a13bdc2e668334b04466b288d31179df1cff7be/Lib/fontTools/varLib/iup.py#L53>
223fn iup_segment(coords: &[Point], rc1: Point, rd1: Vec2, rc2: Point, rd2: Vec2) -> Vec<Vec2> {
224    // rc1 = reference coord 1
225    // rd1 = reference delta 1
226
227    let n = coords.len();
228    let mut result = vec![Vec2::default(); n];
229    for axis in [Axis2D::X, Axis2D::Y] {
230        let c1 = rc1.get(axis);
231        let c2 = rc2.get(axis);
232        let d1 = rd1.get(axis);
233        let d2 = rd2.get(axis);
234
235        if c1 == c2 {
236            let value = if d1 == d2 { d1 } else { 0.0 };
237            for r in result.iter_mut() {
238                r.set(axis, value);
239            }
240            continue;
241        }
242
243        let (c1, c2, d1, d2) = if c1 > c2 {
244            (c2, c1, d2, d1) // flip
245        } else {
246            (c1, c2, d1, d2) // nop
247        };
248
249        // # c1 < c2
250        let scale = (d2 - d1) / (c2 - c1);
251        for (idx, point) in coords.iter().enumerate() {
252            let c = point.get(axis);
253            let d = if c <= c1 {
254                d1
255            } else if c >= c2 {
256                d2
257            } else {
258                // Interpolate
259                d1 + (c - c1) * scale
260            };
261            result[idx].set(axis, d);
262        }
263    }
264    result
265}
266
267/// Checks if the deltas for points at `i` and `j` (`i < j`) be
268/// used to interpolate deltas for points in between them within
269/// provided error tolerance
270///
271/// See [iup_contour_optimize_dp] comments for context on from/to range restrictions.
272///
273/// <https://github.com/fonttools/fonttools/blob/6a13bdc2e668334b04466b288d31179df1cff7be/Lib/fontTools/varLib/iup.py#L187>
274fn can_iup_in_between(
275    deltas: &[Vec2],
276    coords: &[Point],
277    tolerance: f64,
278    from: isize,
279    to: isize,
280) -> Result<bool, IupError> {
281    if from < -1 || to <= from || to - from < 2 {
282        return Err(IupError::AchievedInvalidState(format!(
283            "bad from/to: {from}..{to}"
284        )));
285    }
286    // from is >= -1 so from + 1 is a valid usize
287    // to > from so to is a valid usize
288    // from -1 is taken to mean the last entry
289    let to = to as usize;
290    let (rc1, rd1) = if from < 0 {
291        (*coords.last().unwrap(), *deltas.last().unwrap())
292    } else {
293        (coords[from as usize], deltas[from as usize])
294    };
295    let iup_values = iup_segment(
296        &coords[(from + 1) as usize..to],
297        rc1,
298        rd1,
299        coords[to],
300        deltas[to],
301    );
302
303    let real_values = &deltas[(from + 1) as usize..to];
304
305    // compute this once here instead of in the loop
306    let tolerance_sq = tolerance.powi(2);
307    Ok(real_values
308        .iter()
309        .zip(iup_values)
310        .all(|(d, i)| (*d - i).hypot2() <= tolerance_sq))
311}
312
313/// <https://github.com/fonttools/fonttools/blob/6a13bdc2e668334b04466b288d31179df1cff7be/Lib/fontTools/varLib/iup.py#L327>
314fn iup_initial_lookback(deltas: &[Vec2]) -> usize {
315    std::cmp::min(deltas.len(), 8usize) // no more than 8!
316}
317
318#[derive(Debug, PartialEq)]
319struct OptimizeDpResult {
320    costs: Vec<i32>,
321    chain: Vec<Option<usize>>,
322}
323
324/// Straightforward Dynamic-Programming.  For each index i, find least-costly encoding of
325/// points 0 to i where i is explicitly encoded.  We find this by considering all previous
326/// explicit points j and check whether interpolation can fill points between j and i.
327///
328/// Note that solution always encodes last point explicitly.  Higher-level is responsible
329/// for removing that restriction.
330///
331/// As major speedup, we stop looking further whenever we see a point we are certain requires explicit encoding.
332/// <https://github.com/fonttools/fonttools/blob/6a13bdc2e668334b04466b288d31179df1cff7be/Lib/fontTools/varLib/iup.py#L308>
333fn iup_contour_optimize_dp(
334    deltas: &[Vec2],
335    coords: &[Point],
336    tolerance: f64,
337    must_encode: &HashSet<usize>,
338    lookback: usize,
339) -> Result<OptimizeDpResult, IupError> {
340    let n = deltas.len();
341    let lookback = lookback as isize;
342    let mut costs = Vec::with_capacity(n);
343    let mut chain: Vec<_> = (0..n)
344        .map(|i| if i > 0 { Some(i - 1) } else { None })
345        .collect();
346
347    // n < 2 is degenerate
348    if n < 2 {
349        return Ok(OptimizeDpResult { costs, chain });
350    }
351
352    for i in 0..n {
353        let mut best_cost = if i > 0 { costs[i - 1] } else { 0 } + 1;
354
355        costs.push(best_cost);
356        if i > 0 && must_encode.contains(&(i - 1)) {
357            continue;
358        }
359
360        // python inner loop is j in range(i - 2, max(i - lookback, -2), -1)
361        //      start at no less than -2, step -1 toward no less than -2
362        //      lookback is either deltas.len() or deltas.len()/2 (because we tried repeating the contour)
363        //          deltas must have more than 1 point to even try, so lookback at least 2
364        // how does this play out? - slightly non-obviously one might argue
365        // costs starts as {-1:0}
366        // at i=0
367        //      best_cost is set to costs[-1] + 1 which is 1
368        //      costs[0] = best_cost, costs is {-1:0, 0:1}
369        //      j in range(-2, max(0 - at least 2, -2), -1), so range(-2, -2, -1), which is empty
370        // at i=1
371        //      best_cost is set to costs[-1] + 1 which is 2
372        //      costs[i] = best_cost, costs is {-1:0, 0:1, 1:2}
373        //      j in range(-1, max(1 - at least 2, -2), -1), so it must be one of:
374        //          range(-1, -2, -1), range(-1, -1, -1)
375        //          only range(-1, -2, -1) has any values, -1
376        //          when j = -1
377        //              cost = costs[-1] + 1 will set cost to 1, which is < best_cost
378        //              call can_iup_in_between for -1, 1
379        // from i=2 onward we walk from >=0 to >=-2 non-inclusive, so can_iup_in_between
380        // will only see indices >= -1. In Python -1 reads the last point, which must be encoded.
381
382        // Python loops from high (inclusive) to low (exclusive) stepping -1
383        // To match, we loop from low+1 to high+1 to exclude low and include high
384        // and reverse to mimic the step
385        let j_min = std::cmp::max(i as isize - lookback, -2);
386        let j_max = i as isize - 2;
387        for j in (j_min + 1..j_max + 1).rev() {
388            let (cost, must_encode) = if j >= 0 {
389                (costs[j as usize] + 1, must_encode.contains(&(j as usize)))
390            } else {
391                (1, false)
392            };
393            if cost < best_cost && can_iup_in_between(deltas, coords, tolerance, j, i as isize)? {
394                best_cost = cost;
395                costs[i] = best_cost;
396                chain[i] = if j >= 0 { Some(j as usize) } else { None };
397            }
398            if must_encode {
399                break;
400            }
401        }
402    }
403
404    Ok(OptimizeDpResult { costs, chain })
405}
406
407/// For contour with coordinates `coords`, optimize a set of delta values `deltas` within error `tolerance`.
408///
409/// Returns delta vector that has most number of None items instead of the input delta.
410/// Returns a vector of [`GlyphDelta`]s with the maximal number marked as
411/// 'optional'.
412///
413/// <https://github.com/fonttools/fonttools/blob/6a13bdc2e668334b04466b288d31179df1cff7be/Lib/fontTools/varLib/iup.py#L369>
414fn iup_contour_optimize(
415    deltas: &mut [Vec2],
416    coords: &mut [Point],
417    tolerance: f64,
418) -> Result<Vec<GlyphDelta>, IupError> {
419    if deltas.len() != coords.len() {
420        return Err(IupError::DeltaCoordLengthMismatch {
421            num_deltas: deltas.len(),
422            num_coords: coords.len(),
423        });
424    }
425
426    let n = deltas.len();
427
428    // Get the easy cases out of the way
429    // Easy: all points are the same or there are no points
430    // This covers the case when there is only one point
431    let Some(first_delta) = deltas.first() else {
432        return Ok(Vec::new());
433    };
434    if deltas.iter().all(|d| d == first_delta) {
435        if *first_delta == Vec2::ZERO {
436            return Ok(vec![GlyphDelta::optional(0, 0); n]);
437        }
438
439        let (x, y) = first_delta.to_point().ot_round();
440        // if all deltas are equal than the first is explicit and the rest
441        // are interpolatable
442        return Ok(std::iter::once(GlyphDelta::required(x, y))
443            .chain(std::iter::repeat(GlyphDelta::optional(x, y)))
444            .take(n)
445            .collect());
446    }
447
448    // Solve the general problem using Dynamic Programming
449    let must_encode = iup_must_encode(deltas, coords, tolerance)?;
450    // The iup_contour_optimize_dp() routine returns the optimal encoding
451    // solution given the constraint that the last point is always encoded.
452    // To remove this constraint, we use two different methods, depending on
453    // whether forced set is non-empty or not:
454
455    // Debugging: Make the next if always take the second branch and observe
456    // if the font size changes (reduced); that would mean the forced-set
457    // has members it should not have.
458    let encode = if !must_encode.is_empty() {
459        // Setup for iup_contour_optimize_dp
460        // We know at least one point *must* be encoded so rotate such that last point is encoded
461        // rot must be > 0 so this is rightwards
462        let mid = n - 1 - must_encode.iter().max().unwrap();
463        deltas.rotate_right(mid);
464        coords.rotate_right(mid);
465        let must_encode: HashSet<usize> = must_encode.iter().map(|idx| (idx + mid) % n).collect();
466        let dp_result = iup_contour_optimize_dp(
467            deltas,
468            coords,
469            tolerance,
470            &must_encode,
471            iup_initial_lookback(deltas),
472        )?;
473
474        // Assemble solution
475        let mut encode = HashSet::new();
476
477        let mut i = n - 1;
478        loop {
479            encode.insert(i);
480            i = match dp_result.chain[i] {
481                Some(v) => v,
482                None => break,
483            };
484        }
485
486        if !encode.is_superset(&must_encode) {
487            return Err(IupError::AchievedInvalidState(format!(
488                "{encode:?} should contain {must_encode:?}"
489            )));
490        }
491
492        deltas.rotate_left(mid);
493        // The original fonttools version applies the IUP solution to the deltas and
494        // *then* rotates the deltas list back, whereas here we have rotated the deltas *before*
495        // applying the solution at the end, so we must rotate the solution as well
496        // otherwise indices don't match as in https://github.com/googlefonts/fontations/issues/571
497        // Cf. https://github.com/fonttools/fonttools/blob/dd783ff/Lib/fontTools/varLib/iup.py#L435-L437
498        encode.iter().map(|idx| ((idx + n - mid) % n)).collect()
499    } else {
500        // Repeat the contour an extra time, solve the new case, then look for solutions of the
501        // circular n-length problem in the solution for new linear case.  I cannot prove that
502        // this always produces the optimal solution...
503        let mut deltas_twice = Vec::with_capacity(2 * n);
504        deltas_twice.extend_from_slice(deltas);
505        deltas_twice.extend_from_slice(deltas);
506        let mut coords_twice = Vec::with_capacity(2 * n);
507        coords_twice.extend_from_slice(coords);
508        coords_twice.extend_from_slice(coords);
509
510        let dp_result = iup_contour_optimize_dp(
511            &deltas_twice,
512            &coords_twice,
513            tolerance,
514            &must_encode,
515            iup_initial_lookback(deltas),
516        )?;
517
518        let mut best_sol = None;
519        let mut best_cost = (n + 1) as i32;
520
521        for start in n - 1..dp_result.costs.len() - 1 {
522            // Assemble solution
523            let mut solution = HashSet::new();
524            let mut i = Some(start);
525
526            // As in Python, this must be true when `i` is non-negative and `start - n` is negative
527            while i > start.checked_sub(n) {
528                let idx = i.unwrap();
529                solution.insert(idx % n);
530                i = dp_result.chain[idx];
531            }
532
533            // this should only be None for the first loop, when start is n - 1
534            if i == start.checked_sub(n) {
535                // Python reads [-1] to get 0, usize doesn't like that
536                let cost = dp_result.costs[start]
537                    - if n < start {
538                        dp_result.costs[start - n]
539                    } else {
540                        0
541                    };
542                if cost <= best_cost {
543                    best_sol = Some(solution);
544                    best_cost = cost;
545                }
546            }
547        }
548
549        let encode = best_sol.ok_or(IupError::AchievedInvalidState(
550            "No best solution identified".to_string(),
551        ))?;
552
553        if !encode.is_superset(&must_encode) {
554            return Err(IupError::AchievedInvalidState(format!(
555                "{encode:?} should contain {must_encode:?}"
556            )));
557        }
558
559        encode
560    };
561
562    Ok(deltas
563        .iter()
564        .enumerate()
565        .map(|(i, delta)| {
566            let (x, y) = delta.to_point().ot_round();
567            if encode.contains(&i) {
568                GlyphDelta::required(x, y)
569            } else {
570                GlyphDelta::optional(x, y)
571            }
572        })
573        .collect())
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579    use pretty_assertions::assert_eq;
580
581    struct IupScenario {
582        deltas: Vec<Vec2>,
583        coords: Vec<Point>,
584        expected_must_encode: HashSet<usize>,
585    }
586
587    impl IupScenario {
588        /// <https://github.com/fonttools/fonttools/blob/6a13bdc2e668334b04466b288d31179df1cff7be/Tests/varLib/iup_test.py#L113>
589        fn assert_must_encode(&self) {
590            assert_eq!(
591                self.expected_must_encode,
592                iup_must_encode(&self.deltas, &self.coords, f64::EPSILON).unwrap()
593            );
594        }
595
596        /// <https://github.com/fonttools/fonttools/blob/6a13bdc2e668334b04466b288d31179df1cff7be/Tests/varLib/iup_test.py#L116-L120>
597        fn assert_optimize_dp(&self) {
598            let must_encode = iup_must_encode(&self.deltas, &self.coords, f64::EPSILON).unwrap();
599            let lookback = iup_initial_lookback(&self.deltas);
600            let r1 = iup_contour_optimize_dp(
601                &self.deltas,
602                &self.coords,
603                f64::EPSILON,
604                &must_encode,
605                lookback,
606            )
607            .unwrap();
608            let must_encode = HashSet::new();
609            let r2 = iup_contour_optimize_dp(
610                &self.deltas,
611                &self.coords,
612                f64::EPSILON,
613                &must_encode,
614                lookback,
615            )
616            .unwrap();
617
618            assert_eq!(r1, r2);
619        }
620
621        /// No Python equivalent
622        fn assert_optimize_contour(&self) {
623            let mut deltas = self.deltas.clone();
624            let mut coords = self.coords.clone();
625            iup_contour_optimize(&mut deltas, &mut coords, f64::EPSILON).unwrap();
626        }
627    }
628
629    /// <https://github.com/fonttools/fonttools/blob/6a13bdc2e668334b04466b288d31179df1cff7be/Tests/varLib/iup_test.py#L15>
630    fn iup_scenario1() -> IupScenario {
631        IupScenario {
632            deltas: vec![(0.0, 0.0).into()],
633            coords: vec![(1.0, 2.0).into()],
634            expected_must_encode: HashSet::new(),
635        }
636    }
637
638    /// <https://github.com/fonttools/fonttools/blob/6a13bdc2e668334b04466b288d31179df1cff7be/Tests/varLib/iup_test.py#L16>
639    fn iup_scenario2() -> IupScenario {
640        IupScenario {
641            deltas: vec![(0.0, 0.0).into(), (0.0, 0.0).into(), (0.0, 0.0).into()],
642            coords: vec![(1.0, 2.0).into(), (3.0, 2.0).into(), (2.0, 3.0).into()],
643            expected_must_encode: HashSet::new(),
644        }
645    }
646
647    /// <https://github.com/fonttools/fonttools/blob/6a13bdc2e668334b04466b288d31179df1cff7be/Tests/varLib/iup_test.py#L17-L21>
648    fn iup_scenario3() -> IupScenario {
649        IupScenario {
650            deltas: vec![
651                (1.0, 1.0).into(),
652                (-1.0, 1.0).into(),
653                (-1.0, -1.0).into(),
654                (1.0, -1.0).into(),
655            ],
656            coords: vec![
657                (0.0, 0.0).into(),
658                (2.0, 0.0).into(),
659                (2.0, 2.0).into(),
660                (0.0, 2.0).into(),
661            ],
662            expected_must_encode: HashSet::new(),
663        }
664    }
665
666    /// <https://github.com/fonttools/fonttools/blob/6a13bdc2e668334b04466b288d31179df1cff7be/Tests/varLib/iup_test.py#L22-L52>
667    fn iup_scenario4() -> IupScenario {
668        IupScenario {
669            deltas: vec![
670                (-1.0, 0.0).into(),
671                (-1.0, 0.0).into(),
672                (-1.0, 0.0).into(),
673                (-1.0, 0.0).into(),
674                (-1.0, 0.0).into(),
675                (0.0, 0.0).into(),
676                (0.0, 0.0).into(),
677                (0.0, 0.0).into(),
678                (0.0, 0.0).into(),
679                (0.0, 0.0).into(),
680                (0.0, 0.0).into(),
681                (-1.0, 0.0).into(),
682            ],
683            coords: vec![
684                (-35.0, -152.0).into(),
685                (-86.0, -101.0).into(),
686                (-50.0, -65.0).into(),
687                (0.0, -116.0).into(),
688                (51.0, -65.0).into(),
689                (86.0, -99.0).into(),
690                (35.0, -151.0).into(),
691                (87.0, -202.0).into(),
692                (51.0, -238.0).into(),
693                (-1.0, -187.0).into(),
694                (-53.0, -239.0).into(),
695                (-88.0, -205.0).into(),
696            ],
697            expected_must_encode: HashSet::from([11]),
698        }
699    }
700
701    /// <https://github.com/fonttools/fonttools/blob/6a13bdc2e668334b04466b288d31179df1cff7be/Tests/varLib/iup_test.py#L53-L108>
702    fn iup_scenario5() -> IupScenario {
703        IupScenario {
704            deltas: vec![
705                (0.0, 0.0).into(),
706                (1.0, 0.0).into(),
707                (2.0, 0.0).into(),
708                (2.0, 0.0).into(),
709                (0.0, 0.0).into(),
710                (1.0, 0.0).into(),
711                (3.0, 0.0).into(),
712                (3.0, 0.0).into(),
713                (2.0, 0.0).into(),
714                (2.0, 0.0).into(),
715                (0.0, 0.0).into(),
716                (0.0, 0.0).into(),
717                (-1.0, 0.0).into(),
718                (-1.0, 0.0).into(),
719                (-1.0, 0.0).into(),
720                (-3.0, 0.0).into(),
721                (-1.0, 0.0).into(),
722                (0.0, 0.0).into(),
723                (0.0, 0.0).into(),
724                (-2.0, 0.0).into(),
725                (-2.0, 0.0).into(),
726                (-1.0, 0.0).into(),
727                (-1.0, 0.0).into(),
728                (-1.0, 0.0).into(),
729                (-4.0, 0.0).into(),
730            ],
731            coords: vec![
732                (330.0, 65.0).into(),
733                (401.0, 65.0).into(),
734                (499.0, 117.0).into(),
735                (549.0, 225.0).into(),
736                (549.0, 308.0).into(),
737                (549.0, 422.0).into(),
738                (549.0, 500.0).into(),
739                (497.0, 600.0).into(),
740                (397.0, 648.0).into(),
741                (324.0, 648.0).into(),
742                (271.0, 648.0).into(),
743                (200.0, 620.0).into(),
744                (165.0, 570.0).into(),
745                (165.0, 536.0).into(),
746                (165.0, 473.0).into(),
747                (252.0, 407.0).into(),
748                (355.0, 407.0).into(),
749                (396.0, 407.0).into(),
750                (396.0, 333.0).into(),
751                (354.0, 333.0).into(),
752                (249.0, 333.0).into(),
753                (141.0, 268.0).into(),
754                (141.0, 203.0).into(),
755                (141.0, 131.0).into(),
756                (247.0, 65.0).into(),
757            ],
758            expected_must_encode: HashSet::from([5, 15, 24]),
759        }
760    }
761
762    /// The Python tests do not take the must encode empty branch of iup_contour_optimize,
763    /// this test is meant to activate it by having enough points to be interesting and
764    /// none of them must_encode.
765    fn iup_scenario6() -> IupScenario {
766        IupScenario {
767            deltas: vec![
768                (0.0, 0.0).into(),
769                (1.0, 1.0).into(),
770                (2.0, 2.0).into(),
771                (3.0, 3.0).into(),
772                (4.0, 4.0).into(),
773                (5.0, 5.0).into(),
774                (6.0, 6.0).into(),
775                (7.0, 7.0).into(),
776            ],
777            coords: vec![
778                (0.0, 0.0).into(),
779                (10.0, 10.0).into(),
780                (20.0, 20.0).into(),
781                (30.0, 30.0).into(),
782                (40.0, 40.0).into(),
783                (50.0, 50.0).into(),
784                (60.0, 60.0).into(),
785                (70.0, 70.0).into(),
786            ],
787            expected_must_encode: HashSet::from([]),
788        }
789    }
790
791    /// Another case with no must-encode items, this time a real one from a fontmake-rs test
792    /// that was failing
793    fn iup_scenario7() -> IupScenario {
794        IupScenario {
795            coords: vec![
796                (242.0, 111.0),
797                (314.0, 111.0),
798                (314.0, 317.0),
799                (513.0, 317.0),
800                (513.0, 388.0),
801                (314.0, 388.0),
802                (314.0, 595.0),
803                (242.0, 595.0),
804                (242.0, 388.0),
805                (43.0, 388.0),
806                (43.0, 317.0),
807                (242.0, 317.0),
808                (0.0, 0.0),
809                (557.0, 0.0),
810                (0.0, 0.0),
811                (0.0, 0.0),
812            ]
813            .into_iter()
814            .map(|c| c.into())
815            .collect(),
816            deltas: vec![
817                (-10.0, 0.0),
818                (25.0, 0.0),
819                (25.0, -18.0),
820                (15.0, -18.0),
821                (15.0, 18.0),
822                (25.0, 18.0),
823                (25.0, 1.0),
824                (-10.0, 1.0),
825                (-10.0, 18.0),
826                (0.0, 18.0),
827                (0.0, -18.0),
828                (-10.0, -18.0),
829                (0.0, 0.0),
830                (15.0, 0.0),
831                (0.0, 0.0),
832                (0.0, 0.0),
833            ]
834            .into_iter()
835            .map(|c| c.into())
836            .collect(),
837            expected_must_encode: HashSet::from([0]),
838        }
839    }
840
841    /// From a fontmake-rs test that was failing (achieved invalid state)
842    fn iup_scenario8() -> IupScenario {
843        IupScenario {
844            coords: vec![
845                (131.0, 430.0),
846                (131.0, 350.0),
847                (470.0, 350.0),
848                (470.0, 430.0),
849                (131.0, 330.0),
850            ]
851            .into_iter()
852            .map(|c| c.into())
853            .collect(),
854            deltas: vec![
855                (-15.0, 115.0),
856                (-15.0, 30.0),
857                (124.0, 30.0),
858                (124.0, 115.0),
859                (-39.0, 26.0),
860            ]
861            .into_iter()
862            .map(|c| c.into())
863            .collect(),
864            expected_must_encode: HashSet::from([0, 4]),
865        }
866    }
867
868    #[test]
869    fn iup_test_scenario01_must_encode() {
870        iup_scenario1().assert_must_encode();
871    }
872
873    #[test]
874    fn iup_test_scenario02_must_encode() {
875        iup_scenario2().assert_must_encode();
876    }
877
878    #[test]
879    fn iup_test_scenario03_must_encode() {
880        iup_scenario3().assert_must_encode();
881    }
882
883    #[test]
884    fn iup_test_scenario04_must_encode() {
885        iup_scenario4().assert_must_encode();
886    }
887
888    #[test]
889    fn iup_test_scenario05_must_encode() {
890        iup_scenario5().assert_must_encode();
891    }
892
893    #[test]
894    fn iup_test_scenario06_must_encode() {
895        iup_scenario6().assert_must_encode();
896    }
897
898    #[test]
899    fn iup_test_scenario07_must_encode() {
900        iup_scenario7().assert_must_encode();
901    }
902
903    #[test]
904    fn iup_test_scenario08_must_encode() {
905        iup_scenario8().assert_must_encode();
906    }
907
908    #[test]
909    fn iup_test_scenario01_optimize() {
910        iup_scenario1().assert_optimize_dp();
911    }
912
913    #[test]
914    fn iup_test_scenario02_optimize() {
915        iup_scenario2().assert_optimize_dp();
916    }
917
918    #[test]
919    fn iup_test_scenario03_optimize() {
920        iup_scenario3().assert_optimize_dp();
921    }
922
923    #[test]
924    fn iup_test_scenario04_optimize() {
925        iup_scenario4().assert_optimize_dp();
926    }
927
928    #[test]
929    fn iup_test_scenario05_optimize() {
930        iup_scenario5().assert_optimize_dp();
931    }
932
933    #[test]
934    fn iup_test_scenario06_optimize() {
935        iup_scenario6().assert_optimize_dp();
936    }
937
938    #[test]
939    fn iup_test_scenario07_optimize() {
940        iup_scenario7().assert_optimize_dp();
941    }
942
943    #[test]
944    fn iup_test_scenario08_optimize() {
945        iup_scenario8().assert_optimize_dp();
946    }
947
948    #[test]
949    fn iup_test_scenario01_optimize_contour() {
950        iup_scenario1().assert_optimize_contour();
951    }
952
953    #[test]
954    fn iup_test_scenario02_optimize_contour() {
955        iup_scenario2().assert_optimize_contour();
956    }
957
958    #[test]
959    fn iup_test_scenario03_optimize_contour() {
960        iup_scenario3().assert_optimize_contour();
961    }
962
963    #[test]
964    fn iup_test_scenario04_optimize_contour() {
965        iup_scenario4().assert_optimize_contour();
966    }
967
968    #[test]
969    fn iup_test_scenario05_optimize_contour() {
970        iup_scenario5().assert_optimize_contour();
971    }
972
973    #[test]
974    fn iup_test_scenario06_optimize_contour() {
975        iup_scenario6().assert_optimize_contour();
976    }
977
978    #[test]
979    fn iup_test_scenario07_optimize_contour() {
980        iup_scenario7().assert_optimize_contour();
981    }
982
983    #[test]
984    fn iup_test_scenario08_optimize_contour() {
985        iup_scenario8().assert_optimize_contour();
986    }
987
988    // a helper to let us match the existing test format
989    fn make_vec_of_options(deltas: &[GlyphDelta]) -> Vec<(usize, Option<Vec2>)> {
990        deltas
991            .iter()
992            .enumerate()
993            .map(|(i, delta)| {
994                (
995                    i,
996                    delta
997                        .required
998                        .then_some(Vec2::new(delta.x as _, delta.y as _)),
999                )
1000            })
1001            .collect()
1002    }
1003
1004    #[test]
1005    fn iup_delta_optimize_oswald_glyph_two() {
1006        // https://github.com/googlefonts/fontations/issues/564
1007        let deltas: Vec<_> = vec![
1008            (0.0, 0.0),
1009            (41.0, 0.0),
1010            (41.0, 41.0),
1011            (60.0, 41.0),
1012            (22.0, -22.0),
1013            (27.0, -15.0),
1014            (38.0, -4.0),
1015            (44.0, 2.0),
1016            (44.0, -1.0),
1017            (44.0, 2.0),
1018            (29.0, 4.0),
1019            (18.0, 4.0),
1020            (9.0, 4.0),
1021            (-4.0, -4.0),
1022            (-11.0, -12.0),
1023            (-11.0, -10.0),
1024            (-11.0, -25.0),
1025            (44.0, -25.0),
1026            (44.0, -12.0),
1027            (44.0, -20.0),
1028            (39.0, -38.0),
1029            (26.0, -50.0),
1030            (16.0, -50.0),
1031            (-5.0, -50.0),
1032            (-13.0, -21.0),
1033            (-13.0, 1.0),
1034            (-13.0, 11.0),
1035            (-13.0, 16.0),
1036            (-13.0, 16.0),
1037            (-12.0, 19.0),
1038            (0.0, 42.0),
1039            (0.0, 0.0),
1040            (36.0, 0.0),
1041            (0.0, 0.0),
1042            (0.0, 0.0),
1043        ]
1044        .into_iter()
1045        .map(|c| c.into())
1046        .collect();
1047        let coords: Vec<_> = vec![
1048            (41.0, 0.0),
1049            (423.0, 0.0),
1050            (423.0, 90.0),
1051            (167.0, 90.0),
1052            (353.0, 374.0),
1053            (377.0, 410.0),
1054            (417.0, 478.0),
1055            (442.0, 556.0),
1056            (442.0, 608.0),
1057            (442.0, 706.0),
1058            (346.0, 817.0),
1059            (248.0, 817.0),
1060            (176.0, 817.0),
1061            (89.0, 759.0),
1062            (50.0, 654.0),
1063            (50.0, 581.0),
1064            (50.0, 553.0),
1065            (157.0, 553.0),
1066            (157.0, 580.0),
1067            (157.0, 619.0),
1068            (173.0, 687.0),
1069            (215.0, 729.0),
1070            (253.0, 729.0),
1071            (298.0, 729.0),
1072            (334.0, 665.0),
1073            (334.0, 609.0),
1074            (334.0, 564.0),
1075            (309.0, 495.0),
1076            (270.0, 433.0),
1077            (247.0, 397.0),
1078            (41.0, 76.0),
1079            (0.0, 0.0),
1080            (478.0, 0.0),
1081            (0.0, 0.0),
1082            (0.0, 0.0),
1083        ]
1084        .into_iter()
1085        .map(|c| c.into())
1086        .collect();
1087
1088        // using fonttools varLib's default tolerance
1089        let tolerance = 0.5;
1090        // a single contour, minus the phantom points
1091        let contour_ends = vec![coords.len() - 1 - 4];
1092
1093        let result = iup_delta_optimize(deltas.clone(), coords, tolerance, &contour_ends).unwrap();
1094        assert_eq!(result.len(), deltas.len());
1095        let result = make_vec_of_options(&result);
1096
1097        assert_eq!(
1098            result,
1099            // this is what fonttools iup_delta_optimize returns and what we want to match
1100            vec![
1101                (0, None),
1102                (1, Some(Vec2 { x: 41.0, y: 0.0 })),
1103                (2, None),
1104                (3, Some(Vec2 { x: 60.0, y: 41.0 })),
1105                (4, Some(Vec2 { x: 22.0, y: -22.0 })),
1106                (5, Some(Vec2 { x: 27.0, y: -15.0 })),
1107                (6, Some(Vec2 { x: 38.0, y: -4.0 })),
1108                (7, Some(Vec2 { x: 44.0, y: 2.0 })),
1109                (8, Some(Vec2 { x: 44.0, y: -1.0 })),
1110                (9, Some(Vec2 { x: 44.0, y: 2.0 })),
1111                (10, Some(Vec2 { x: 29.0, y: 4.0 })),
1112                (11, Some(Vec2 { x: 18.0, y: 4.0 })),
1113                (12, Some(Vec2 { x: 9.0, y: 4.0 })),
1114                (13, Some(Vec2 { x: -4.0, y: -4.0 })),
1115                (14, Some(Vec2 { x: -11.0, y: -12.0 })),
1116                (15, Some(Vec2 { x: -11.0, y: -10.0 })),
1117                (16, None),
1118                (17, Some(Vec2 { x: 44.0, y: -25.0 })),
1119                (18, Some(Vec2 { x: 44.0, y: -12.0 })),
1120                (19, Some(Vec2 { x: 44.0, y: -20.0 })),
1121                (20, Some(Vec2 { x: 39.0, y: -38.0 })),
1122                (21, Some(Vec2 { x: 26.0, y: -50.0 })),
1123                (22, Some(Vec2 { x: 16.0, y: -50.0 })),
1124                (23, Some(Vec2 { x: -5.0, y: -50.0 })),
1125                (24, Some(Vec2 { x: -13.0, y: -21.0 })),
1126                (25, Some(Vec2 { x: -13.0, y: 1.0 })),
1127                (26, Some(Vec2 { x: -13.0, y: 11.0 })),
1128                (27, Some(Vec2 { x: -13.0, y: 16.0 })),
1129                (28, Some(Vec2 { x: -13.0, y: 16.0 })),
1130                (29, None),
1131                (30, Some(Vec2 { x: 0.0, y: 42.0 })),
1132                (31, None),
1133                (32, Some(Vec2 { x: 36.0, y: 0.0 })),
1134                (33, None),
1135                (34, None),
1136            ]
1137        )
1138    }
1139
1140    #[test]
1141    fn iup_delta_optimize_gs_glyph_uppercase_c() {
1142        // https://github.com/googlefonts/fontations/issues/571
1143        let deltas: Vec<_> = vec![
1144            (2.0, 0.0),
1145            (4.0, 0.0),
1146            (8.0, -1.0),
1147            (10.0, -1.0),
1148            (10.0, 0.0),
1149            (-14.0, 25.0),
1150            (-8.0, 34.0),
1151            (-3.0, 38.0),
1152            (-5.0, 35.0),
1153            (-7.0, 35.0),
1154            (6.0, 35.0),
1155            (22.0, 27.0),
1156            (29.0, 11.0),
1157            (29.0, -1.0),
1158            (29.0, -13.0),
1159            (22.0, -29.0),
1160            (8.0, -37.0),
1161            (-3.0, -37.0),
1162            (0.0, -37.0),
1163            (1.0, -43.0),
1164            (-7.0, -41.0),
1165            (-19.0, -28.0),
1166            (8.0, 0.0),
1167            (8.0, 0.0),
1168            (6.0, 0.0),
1169            (4.0, 0.0),
1170            (2.0, 0.0),
1171            (0.0, 0.0),
1172            (-5.0, 0.0),
1173            (-8.0, 0.0),
1174            (-10.0, 0.0),
1175            (-10.0, 0.0),
1176            (-8.0, 0.0),
1177            (-5.0, 0.0),
1178            (-1.0, 0.0),
1179            (0.0, 0.0),
1180            (0.0, 0.0),
1181            (0.0, 0.0),
1182            (0.0, 0.0),
1183        ]
1184        .into_iter()
1185        .map(|c| c.into())
1186        .collect();
1187
1188        let coords: Vec<_> = vec![
1189            (416.0, -16.0),
1190            (476.0, -16.0),
1191            (581.0, 17.0),
1192            (668.0, 75.0),
1193            (699.0, 112.0),
1194            (637.0, 172.0),
1195            (609.0, 139.0),
1196            (542.0, 91.0),
1197            (463.0, 65.0),
1198            (416.0, 65.0),
1199            (339.0, 65.0),
1200            (209.0, 137.0),
1201            (131.0, 269.0),
1202            (131.0, 358.0),
1203            (131.0, 448.0),
1204            (209.0, 579.0),
1205            (339.0, 651.0),
1206            (416.0, 651.0),
1207            (458.0, 651.0),
1208            (529.0, 631.0),
1209            (590.0, 589.0),
1210            (617.0, 556.0),
1211            (678.0, 615.0),
1212            (646.0, 652.0),
1213            (566.0, 704.0),
1214            (471.0, 732.0),
1215            (416.0, 732.0),
1216            (337.0, 732.0),
1217            (202.0, 675.0),
1218            (101.0, 574.0),
1219            (45.0, 438.0),
1220            (45.0, 279.0),
1221            (101.0, 142.0),
1222            (202.0, 41.0),
1223            (337.0, -16.0),
1224            (0.0, 0.0),
1225            (741.0, 0.0),
1226            (0.0, 0.0),
1227            (0.0, 0.0),
1228        ]
1229        .into_iter()
1230        .map(|c| c.into())
1231        .collect();
1232
1233        // using fonttools varLib's default tolerance
1234        let tolerance = 0.5;
1235        // a single contour, minus the phantom points
1236        let contour_ends = vec![coords.len() - 1 - 4];
1237
1238        let result = iup_delta_optimize(deltas.clone(), coords, tolerance, &contour_ends).unwrap();
1239        let result = make_vec_of_options(&result);
1240
1241        assert_eq!(
1242            result,
1243            vec![
1244                (0, None),
1245                (1, Some(Vec2 { x: 4.0, y: 0.0 })),
1246                (2, Some(Vec2 { x: 8.0, y: -1.0 })),
1247                (3, Some(Vec2 { x: 10.0, y: -1.0 })),
1248                (4, Some(Vec2 { x: 10.0, y: 0.0 })),
1249                (5, Some(Vec2 { x: -14.0, y: 25.0 })),
1250                (6, Some(Vec2 { x: -8.0, y: 34.0 })),
1251                (7, Some(Vec2 { x: -3.0, y: 38.0 })),
1252                (8, Some(Vec2 { x: -5.0, y: 35.0 })),
1253                (9, Some(Vec2 { x: -7.0, y: 35.0 })),
1254                (10, Some(Vec2 { x: 6.0, y: 35.0 })),
1255                (11, Some(Vec2 { x: 22.0, y: 27.0 })),
1256                (12, Some(Vec2 { x: 29.0, y: 11.0 })),
1257                (13, None),
1258                (14, Some(Vec2 { x: 29.0, y: -13.0 })),
1259                (15, Some(Vec2 { x: 22.0, y: -29.0 })),
1260                (16, Some(Vec2 { x: 8.0, y: -37.0 })),
1261                (17, Some(Vec2 { x: -3.0, y: -37.0 })),
1262                (18, Some(Vec2 { x: 0.0, y: -37.0 })),
1263                (19, Some(Vec2 { x: 1.0, y: -43.0 })),
1264                (20, Some(Vec2 { x: -7.0, y: -41.0 })),
1265                (21, Some(Vec2 { x: -19.0, y: -28.0 })),
1266                (22, Some(Vec2 { x: 8.0, y: 0.0 })),
1267                (23, Some(Vec2 { x: 8.0, y: 0.0 })),
1268                (24, None),
1269                (25, Some(Vec2 { x: 4.0, y: 0.0 })),
1270                (26, None),
1271                (27, None),
1272                (28, None),
1273                (29, None),
1274                (30, None),
1275                (31, Some(Vec2 { x: -10.0, y: 0.0 })),
1276                (32, None),
1277                (33, None),
1278                (34, None),
1279                (35, None),
1280                (36, None),
1281                (37, None),
1282                (38, None),
1283            ]
1284        )
1285    }
1286
1287    // https://github.com/googlefonts/fontations/issues/662
1288    // this bug seems to be triggered when deltas and coords are equal?
1289    // test case based on s.cp in googlesans
1290    #[test]
1291    fn bug_662() {
1292        let deltas = vec![
1293            Vec2 { x: 73.0, y: 0.0 },
1294            Vec2 { x: 158.0, y: 448.0 },
1295            Vec2 { x: 154.0, y: 448.0 },
1296            Vec2 { x: 0.0, y: 0.0 },
1297            Vec2 { x: 765.0, y: 0.0 },
1298            Vec2 { x: 0.0, y: 0.0 },
1299            Vec2 { x: 0.0, y: 0.0 },
1300        ];
1301
1302        let coords = [
1303            (73.0, 0.0),
1304            (158.0, 448.0),
1305            (154.0, 448.0),
1306            (0.0, 0.0),
1307            (765.0, 0.0),
1308            (0.0, 0.0),
1309            (0.0, 0.0),
1310        ]
1311        .into_iter()
1312        .map(|(x, y)| Point::new(x, y))
1313        .collect::<Vec<_>>();
1314
1315        let contour_ends = vec![2];
1316
1317        iup_delta_optimize(deltas, coords, 0.5, &contour_ends).unwrap();
1318    }
1319
1320    // https://github.com/googlefonts/fontc/issues/1116
1321    // This test case triggered an error caused by a slight semantic difference
1322    // that was introduced from us transitioning to unsigned values in porting
1323    // the Python original.
1324    #[test]
1325    fn bug_fontc_1116() {
1326        let mut deltas = vec![
1327            Vec2 { x: 0.0, y: 0.0 },
1328            Vec2 { x: 0.0, y: 3.0 },
1329            Vec2 { x: 0.0, y: 0.0 },
1330            Vec2 { x: 0.0, y: 0.0 },
1331            Vec2 { x: 2.0, y: 3.0 },
1332            Vec2 { x: 2.0, y: 0.0 },
1333            Vec2 { x: 1.0, y: 3.0 },
1334            Vec2 { x: 1.0, y: 0.0 },
1335            Vec2 { x: 0.0, y: 0.0 },
1336            Vec2 { x: 0.0, y: 1.0 },
1337            Vec2 { x: 1.0, y: 0.0 },
1338            Vec2 { x: 1.0, y: 0.0 },
1339        ];
1340        let mut coords = [
1341            (0.0, 0.0),
1342            (0.0, 3.0),
1343            (0.0, 0.0),
1344            (0.0, 0.0),
1345            (2.0, 3.0),
1346            (2.0, 0.0),
1347            (1.0, 3.0),
1348            (1.0, 0.0),
1349            (0.0, 0.0),
1350            (0.0, 1.0),
1351            (1.0, 0.0),
1352            (1.0, 0.0),
1353        ]
1354        .into_iter()
1355        .map(|(x, y)| Point::new(x, y))
1356        .collect::<Vec<_>>();
1357
1358        iup_contour_optimize(&mut deltas, &mut coords, 0.5).unwrap();
1359    }
1360}