Skip to main content

skrifa/outline/glyf/hint/
zone.rs

1//! Glyph zones.
2
3use read_fonts::{
4    tables::glyf::{PointFlags, PointMarker},
5    types::{F26Dot6, Point},
6};
7
8use super::{
9    error::HintErrorKind,
10    graphics::{CoordAxis, GraphicsState},
11    math,
12};
13
14use HintErrorKind::{InvalidPointIndex, InvalidPointRange};
15
16/// Reference to either the twilight or glyph zone.
17///
18/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructing_glyphs#zones>
19#[derive(Copy, Clone, PartialEq, Default, Debug)]
20#[repr(u8)]
21pub enum ZonePointer {
22    Twilight = 0,
23    #[default]
24    Glyph = 1,
25}
26
27impl ZonePointer {
28    pub fn is_twilight(self) -> bool {
29        self == Self::Twilight
30    }
31}
32
33impl TryFrom<i32> for ZonePointer {
34    type Error = HintErrorKind;
35
36    fn try_from(value: i32) -> Result<Self, Self::Error> {
37        match value {
38            0 => Ok(Self::Twilight),
39            1 => Ok(Self::Glyph),
40            _ => Err(HintErrorKind::InvalidZoneIndex(value)),
41        }
42    }
43}
44
45/// Glyph zone for TrueType hinting.
46///
47/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructing_glyphs#zones>
48#[derive(Default, Debug)]
49pub struct Zone<'a> {
50    /// Outline points prior to applying scale.
51    pub unscaled: &'a [Point<i32>],
52    /// Copy of the outline points after applying scale.
53    pub original: &'a mut [Point<F26Dot6>],
54    /// Scaled outline points.
55    pub points: &'a mut [Point<F26Dot6>],
56    pub flags: &'a mut [PointFlags],
57    pub contours: &'a [u16],
58}
59
60impl<'a> Zone<'a> {
61    /// Creates a new hinting zone.
62    pub fn new(
63        unscaled: &'a [Point<i32>],
64        original: &'a mut [Point<F26Dot6>],
65        points: &'a mut [Point<F26Dot6>],
66        flags: &'a mut [PointFlags],
67        contours: &'a [u16],
68    ) -> Self {
69        Self {
70            unscaled,
71            original,
72            points,
73            flags,
74            contours,
75        }
76    }
77
78    pub fn point(&self, index: usize) -> Result<Point<F26Dot6>, HintErrorKind> {
79        self.points
80            .get(index)
81            .copied()
82            .ok_or(InvalidPointIndex(index))
83    }
84
85    pub fn point_mut(&mut self, index: usize) -> Result<&mut Point<F26Dot6>, HintErrorKind> {
86        self.points.get_mut(index).ok_or(InvalidPointIndex(index))
87    }
88
89    pub fn original(&self, index: usize) -> Result<Point<F26Dot6>, HintErrorKind> {
90        self.original
91            .get(index)
92            .copied()
93            .ok_or(InvalidPointIndex(index))
94    }
95
96    pub fn original_mut(&mut self, index: usize) -> Result<&mut Point<F26Dot6>, HintErrorKind> {
97        self.original.get_mut(index).ok_or(InvalidPointIndex(index))
98    }
99
100    pub fn unscaled(&self, index: usize) -> Point<i32> {
101        // Unscaled points in the twilight zone are always (0, 0). This allows
102        // us to avoid the allocation for that zone and back it with an empty
103        // slice.
104        self.unscaled.get(index).copied().unwrap_or_default()
105    }
106
107    pub fn contour(&self, index: usize) -> Result<u16, HintErrorKind> {
108        self.contours
109            .get(index)
110            .copied()
111            .ok_or(HintErrorKind::InvalidContourIndex(index))
112    }
113
114    pub fn touch(&mut self, index: usize, axis: CoordAxis) -> Result<(), HintErrorKind> {
115        let flag = self.flags.get_mut(index).ok_or(InvalidPointIndex(index))?;
116        flag.set_marker(axis.touched_marker());
117        Ok(())
118    }
119
120    pub fn untouch(&mut self, index: usize, axis: CoordAxis) -> Result<(), HintErrorKind> {
121        let flag = self.flags.get_mut(index).ok_or(InvalidPointIndex(index))?;
122        flag.clear_marker(axis.touched_marker());
123        Ok(())
124    }
125
126    pub fn is_touched(&self, index: usize, axis: CoordAxis) -> Result<bool, HintErrorKind> {
127        let flag = self.flags.get(index).ok_or(InvalidPointIndex(index))?;
128        Ok(flag.has_marker(axis.touched_marker()))
129    }
130
131    pub fn flip_on_curve(&mut self, index: usize) -> Result<(), HintErrorKind> {
132        let flag = self.flags.get_mut(index).ok_or(InvalidPointIndex(index))?;
133        flag.flip_on_curve();
134        Ok(())
135    }
136
137    pub fn set_on_curve(
138        &mut self,
139        start: usize,
140        end: usize,
141        on: bool,
142    ) -> Result<(), HintErrorKind> {
143        let flags = self
144            .flags
145            .get_mut(start..end)
146            .ok_or(InvalidPointRange(start, end))?;
147        if on {
148            for flag in flags {
149                flag.set_on_curve();
150            }
151        } else {
152            for flag in flags {
153                flag.clear_on_curve();
154            }
155        }
156        Ok(())
157    }
158
159    /// Interpolate untouched points.
160    ///
161    /// Based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6391>
162    pub fn iup(&mut self, axis: CoordAxis) -> Result<(), HintErrorKind> {
163        let mut point = 0;
164        for i in 0..self.contours.len() {
165            let mut end_point = self.contour(i)? as usize;
166            let first_point = point;
167            if end_point >= self.points.len() {
168                end_point = self.points.len().saturating_sub(1);
169            }
170            while point <= end_point && !self.is_touched(point, axis)? {
171                point += 1;
172            }
173            if point <= end_point {
174                let first_touched = point;
175                let mut cur_touched = point;
176                point += 1;
177                while point <= end_point {
178                    if self.is_touched(point, axis)? {
179                        self.iup_interpolate(axis, cur_touched + 1, point - 1, cur_touched, point)?;
180                        cur_touched = point;
181                    }
182                    point += 1;
183                }
184                if cur_touched == first_touched {
185                    self.iup_shift(axis, first_point, end_point, cur_touched)?;
186                } else {
187                    self.iup_interpolate(
188                        axis,
189                        cur_touched + 1,
190                        end_point,
191                        cur_touched,
192                        first_touched,
193                    )?;
194                    if first_touched > 0 {
195                        self.iup_interpolate(
196                            axis,
197                            first_point,
198                            first_touched - 1,
199                            cur_touched,
200                            first_touched,
201                        )?;
202                    }
203                }
204            }
205        }
206        Ok(())
207    }
208
209    /// Shift the range of points p1..=p2 based on the delta given by the
210    /// reference point p.
211    ///
212    /// Based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6262>
213    fn iup_shift(
214        &mut self,
215        axis: CoordAxis,
216        p1: usize,
217        p2: usize,
218        p: usize,
219    ) -> Result<(), HintErrorKind> {
220        if p1 > p2 || p1 > p || p > p2 {
221            return Ok(());
222        }
223        macro_rules! shift_coord {
224            ($coord:ident) => {
225                let delta = self.point(p)?.$coord - self.original(p)?.$coord;
226                if delta != F26Dot6::ZERO {
227                    let (first, second) = self
228                        .points
229                        .get_mut(p1..=p2)
230                        .ok_or(InvalidPointRange(p1, p2 + 1))?
231                        .split_at_mut(p - p1);
232                    for point in first
233                        .iter_mut()
234                        .chain(second.get_mut(1..).ok_or(InvalidPointIndex(p - p1))?)
235                    {
236                        point.$coord += delta;
237                    }
238                }
239            };
240        }
241        if axis == CoordAxis::X {
242            shift_coord!(x);
243        } else {
244            shift_coord!(y);
245        }
246        Ok(())
247    }
248
249    /// Interpolate the range of points p1..=p2 based on the deltas
250    /// given by the two reference points.
251    ///
252    /// Based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6284>
253    fn iup_interpolate(
254        &mut self,
255        axis: CoordAxis,
256        p1: usize,
257        p2: usize,
258        mut ref1: usize,
259        mut ref2: usize,
260    ) -> Result<(), HintErrorKind> {
261        if p1 > p2 {
262            return Ok(());
263        }
264        let max_points = self.points.len();
265        if ref1 >= max_points || ref2 >= max_points {
266            return Ok(());
267        }
268        macro_rules! interpolate_coord {
269            ($coord:ident) => {
270                let mut orus1 = self.unscaled(ref1).$coord;
271                let mut orus2 = self.unscaled(ref2).$coord;
272                if orus1 > orus2 {
273                    use core::mem::swap;
274                    swap(&mut orus1, &mut orus2);
275                    swap(&mut ref1, &mut ref2);
276                }
277                let org1 = self.original(ref1)?.$coord;
278                let org2 = self.original(ref2)?.$coord;
279                let cur1 = self.point(ref1)?.$coord;
280                let cur2 = self.point(ref2)?.$coord;
281                let delta1 = cur1 - org1;
282                let delta2 = cur2 - org2;
283                let iter = self
284                    .original
285                    .get(p1..=p2)
286                    .ok_or(InvalidPointRange(p1, p2 + 1))?
287                    .iter()
288                    .zip(
289                        self.unscaled
290                            .get(p1..=p2)
291                            .ok_or(InvalidPointRange(p1, p2 + 1))?,
292                    )
293                    .zip(
294                        self.points
295                            .get_mut(p1..=p2)
296                            .ok_or(InvalidPointRange(p1, p2 + 1))?,
297                    );
298                if cur1 == cur2 || orus1 == orus2 {
299                    for ((orig, _unscaled), point) in iter {
300                        let a = orig.$coord;
301                        point.$coord = if a <= org1 {
302                            a + delta1
303                        } else if a >= org2 {
304                            a + delta2
305                        } else {
306                            cur1
307                        };
308                    }
309                } else {
310                    let scale = math::div((cur2 - cur1).to_bits(), orus2 - orus1);
311                    for ((orig, unscaled), point) in iter {
312                        let a = orig.$coord;
313                        point.$coord = if a <= org1 {
314                            a + delta1
315                        } else if a >= org2 {
316                            a + delta2
317                        } else {
318                            cur1 + F26Dot6::from_bits(math::mul(unscaled.$coord - orus1, scale))
319                        };
320                    }
321                }
322            };
323        }
324        if axis == CoordAxis::X {
325            interpolate_coord!(x);
326        } else {
327            interpolate_coord!(y);
328        }
329        Ok(())
330    }
331}
332
333impl<'a> GraphicsState<'a> {
334    /// Takes an array of (zone pointer, point index) pairs and returns true if
335    /// all accesses would be valid.
336    pub fn in_bounds<const N: usize>(&self, pairs: [(ZonePointer, usize); N]) -> bool {
337        for (zp, index) in pairs {
338            if index >= self.zone(zp).points.len() {
339                return false;
340            }
341        }
342        true
343    }
344
345    #[inline(always)]
346    pub fn zone(&self, pointer: ZonePointer) -> &Zone<'a> {
347        &self.zones[pointer as usize]
348    }
349
350    #[inline(always)]
351    pub fn zone_mut(&mut self, pointer: ZonePointer) -> &mut Zone<'a> {
352        &mut self.zones[pointer as usize]
353    }
354
355    #[inline(always)]
356    pub fn zp0(&self) -> &Zone<'a> {
357        self.zone(self.zp0)
358    }
359
360    #[inline(always)]
361    pub fn zp0_mut(&mut self) -> &mut Zone<'a> {
362        self.zone_mut(self.zp0)
363    }
364
365    #[inline(always)]
366    pub fn zp1(&self) -> &Zone<'_> {
367        self.zone(self.zp1)
368    }
369
370    #[inline(always)]
371    pub fn zp1_mut(&mut self) -> &mut Zone<'a> {
372        self.zone_mut(self.zp1)
373    }
374
375    #[inline(always)]
376    pub fn zp2(&self) -> &Zone<'_> {
377        self.zone(self.zp2)
378    }
379
380    #[inline(always)]
381    pub fn zp2_mut(&mut self) -> &mut Zone<'a> {
382        self.zone_mut(self.zp2)
383    }
384}
385
386impl GraphicsState<'_> {
387    /// Moves the requested original point by the given distance.
388    // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L1743>
389    pub(crate) fn move_original(
390        &mut self,
391        zone: ZonePointer,
392        point_ix: usize,
393        distance: F26Dot6,
394    ) -> Result<(), HintErrorKind> {
395        let fv = self.freedom_vector;
396        let fdotp = self.fdotp;
397        let axis = self.freedom_axis;
398        let point = self.zone_mut(zone).original_mut(point_ix)?;
399        match axis {
400            CoordAxis::X => point.x += distance,
401            CoordAxis::Y => point.y += distance,
402            CoordAxis::Both => {
403                let distance = distance.to_bits();
404                if fv.x != 0 {
405                    point.x += F26Dot6::from_bits(math::mul_div(distance, fv.x, fdotp));
406                }
407                if fv.y != 0 {
408                    point.y += F26Dot6::from_bits(math::mul_div(distance, fv.y, fdotp));
409                }
410            }
411        }
412        Ok(())
413    }
414
415    /// Moves the requested scaled point by the given distance.
416    /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L1771>
417    pub(crate) fn move_point(
418        &mut self,
419        zone: ZonePointer,
420        point_ix: usize,
421        distance: F26Dot6,
422    ) -> Result<(), HintErrorKind> {
423        // Note: we never adjust x in backward compatibility mode and we never
424        // adjust y in backward compatibility mode after IUP has been done in
425        // both directions.
426        //
427        // The primary motivation is to avoid horizontal adjustments in cases
428        // where subpixel rendering provides better fidelity.
429        //
430        // For more detail, see <https://learn.microsoft.com/en-us/typography/cleartype/truetypecleartype>
431        let back_compat = self.backward_compatibility;
432        let back_compat_and_did_iup = back_compat && self.did_iup_x && self.did_iup_y;
433        let zone = &mut self.zones[zone as usize];
434        let point = zone.point_mut(point_ix)?;
435        match self.freedom_axis {
436            CoordAxis::X => {
437                if !back_compat {
438                    point.x += distance;
439                }
440                zone.touch(point_ix, CoordAxis::X)?;
441            }
442            CoordAxis::Y => {
443                if !back_compat_and_did_iup {
444                    point.y += distance;
445                }
446                zone.touch(point_ix, CoordAxis::Y)?;
447            }
448            CoordAxis::Both => {
449                // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L1669>
450                let fv = self.freedom_vector;
451                let distance = distance.to_bits();
452                if fv.x != 0 {
453                    if !back_compat {
454                        point.x += F26Dot6::from_bits(math::mul_div(distance, fv.x, self.fdotp));
455                    }
456                    zone.touch(point_ix, CoordAxis::X)?;
457                }
458                if fv.y != 0 {
459                    if !back_compat_and_did_iup {
460                        zone.point_mut(point_ix)?.y +=
461                            F26Dot6::from_bits(math::mul_div(distance, fv.y, self.fdotp));
462                    }
463                    zone.touch(point_ix, CoordAxis::Y)?;
464                }
465            }
466        }
467        Ok(())
468    }
469
470    /// Moves the requested scaled point in the zone referenced by zp2 by the
471    /// given delta.
472    ///
473    /// This is a helper function for SHP, SHC, SHZ, and SHPIX instructions.
474    ///
475    /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5170>
476    pub(crate) fn move_zp2_point(
477        &mut self,
478        point_ix: usize,
479        dx: F26Dot6,
480        dy: F26Dot6,
481        do_touch: bool,
482    ) -> Result<(), HintErrorKind> {
483        // See notes above in move_point() about how this is used.
484        let back_compat = self.backward_compatibility;
485        let back_compat_and_did_iup = back_compat && self.did_iup_x && self.did_iup_y;
486        let fv = self.freedom_vector;
487        let zone = self.zp2_mut();
488        if fv.x != 0 {
489            if !back_compat {
490                zone.point_mut(point_ix)?.x += dx;
491            }
492            if do_touch {
493                zone.touch(point_ix, CoordAxis::X)?;
494            }
495        }
496        if fv.y != 0 {
497            if !back_compat_and_did_iup {
498                zone.point_mut(point_ix)?.y += dy;
499            }
500            if do_touch {
501                zone.touch(point_ix, CoordAxis::Y)?;
502            }
503        }
504        Ok(())
505    }
506
507    /// Computes the adjustment made to a point along the current freedom vector.
508    /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5126>
509    pub(crate) fn point_displacement(
510        &mut self,
511        opcode: u8,
512    ) -> Result<PointDisplacement, HintErrorKind> {
513        let (zone, point_ix) = if (opcode & 1) != 0 {
514            (self.zp0, self.rp1)
515        } else {
516            (self.zp1, self.rp2)
517        };
518        let zone_data = self.zone(zone);
519        let point = zone_data.point(point_ix)?;
520        let original_point = zone_data.original(point_ix)?;
521        let distance = self.project(point, original_point);
522        let fv = self.freedom_vector;
523        let dx = F26Dot6::from_bits(math::mul_div(distance.to_bits(), fv.x, self.fdotp));
524        let dy = F26Dot6::from_bits(math::mul_div(distance.to_bits(), fv.y, self.fdotp));
525        Ok(PointDisplacement {
526            zone,
527            point_ix,
528            dx,
529            dy,
530        })
531    }
532}
533
534#[derive(PartialEq, Debug)]
535pub(crate) struct PointDisplacement {
536    pub zone: ZonePointer,
537    pub point_ix: usize,
538    pub dx: F26Dot6,
539    pub dy: F26Dot6,
540}
541
542impl CoordAxis {
543    fn touched_marker(self) -> PointMarker {
544        match self {
545            CoordAxis::Both => PointMarker::TOUCHED,
546            CoordAxis::X => PointMarker::TOUCHED_X,
547            CoordAxis::Y => PointMarker::TOUCHED_Y,
548        }
549    }
550}
551
552#[cfg(test)]
553mod tests {
554    use super::{math, CoordAxis, GraphicsState, PointDisplacement, Zone, ZonePointer};
555    use raw::{
556        tables::glyf::{PointFlags, PointMarker},
557        types::{F26Dot6, Point},
558    };
559
560    #[test]
561    fn flip_on_curve_point() {
562        let on_curve = PointFlags::on_curve();
563        let off_curve = PointFlags::off_curve_quad();
564        let mut zone = Zone {
565            unscaled: &mut [],
566            original: &mut [],
567            points: &mut [],
568            contours: &[],
569            flags: &mut [on_curve, off_curve, off_curve, on_curve],
570        };
571        for i in 0..4 {
572            zone.flip_on_curve(i).unwrap();
573        }
574        assert_eq!(zone.flags, &[off_curve, on_curve, on_curve, off_curve]);
575    }
576
577    #[test]
578    fn set_on_curve_regions() {
579        let on_curve = PointFlags::on_curve();
580        let off_curve = PointFlags::off_curve_quad();
581        let mut zone = Zone {
582            unscaled: &mut [],
583            original: &mut [],
584            points: &mut [],
585            contours: &[],
586            flags: &mut [on_curve, off_curve, off_curve, on_curve],
587        };
588        zone.set_on_curve(0, 2, true).unwrap();
589        zone.set_on_curve(2, 4, false).unwrap();
590        assert_eq!(zone.flags, &[on_curve, on_curve, off_curve, off_curve]);
591    }
592
593    #[test]
594    fn iup_shift() {
595        let [untouched, touched] = point_markers();
596        // A single touched point shifts the whole contour
597        let mut original = f26dot6_points([(0, 0), (10, 10), (20, 20)]);
598        let mut points = f26dot6_points([(-5, -20), (10, 10), (20, 20)]);
599        let mut zone = Zone {
600            unscaled: &mut [],
601            original: &mut original,
602            points: &mut points,
603            contours: &[3],
604            flags: &mut [touched, untouched, untouched],
605        };
606        zone.iup(CoordAxis::X).unwrap();
607        assert_eq!(zone.points, &f26dot6_points([(-5, -20), (5, 10), (15, 20)]),);
608        zone.iup(CoordAxis::Y).unwrap();
609        assert_eq!(zone.points, &f26dot6_points([(-5, -20), (5, -10), (15, 0)]),);
610    }
611
612    #[test]
613    fn iup_interpolate() {
614        let [untouched, touched] = point_markers();
615        // Two touched points interpolates the intermediate point(s)
616        let mut original = f26dot6_points([(0, 0), (10, 10), (20, 20)]);
617        let mut points = f26dot6_points([(-5, -20), (10, 10), (27, 56)]);
618        let mut zone = Zone {
619            unscaled: &mut [
620                Point::new(0, 0),
621                Point::new(500, 500),
622                Point::new(1000, 1000),
623            ],
624            original: &mut original,
625            points: &mut points,
626            contours: &[3],
627            flags: &mut [touched, untouched, touched],
628        };
629        zone.iup(CoordAxis::X).unwrap();
630        assert_eq!(
631            zone.points,
632            &f26dot6_points([(-5, -20), (11, 10), (27, 56)]),
633        );
634        zone.iup(CoordAxis::Y).unwrap();
635        assert_eq!(
636            zone.points,
637            &f26dot6_points([(-5, -20), (11, 18), (27, 56)]),
638        );
639    }
640
641    #[test]
642    fn iup_does_not_panic_with_empty_points() {
643        let mut zone = Zone {
644            unscaled: &[],
645            original: &mut [],
646            points: &mut [],
647            contours: &[0],
648            flags: &mut [],
649        };
650        let _ = zone.iup(CoordAxis::X);
651        let _ = zone.iup(CoordAxis::Y);
652    }
653
654    #[test]
655    fn move_point_x() {
656        let mut mock = MockGraphicsState::new();
657        let mut gs = mock.graphics_state(100, 0);
658        let point_ix = 0;
659        let orig_x = gs.zones[1].point(point_ix).unwrap().x;
660        let dx = F26Dot6::from_bits(10);
661        // backward compatibility is on by default and we don't move x coord
662        gs.move_point(ZonePointer::Glyph, 0, dx).unwrap();
663        assert_eq!(orig_x, gs.zones[1].point(point_ix).unwrap().x);
664        // disable so we actually move
665        gs.backward_compatibility = false;
666        gs.move_point(ZonePointer::Glyph, 0, dx).unwrap();
667        let new_x = gs.zones[1].point(point_ix).unwrap().x;
668        assert_ne!(orig_x, new_x);
669        assert_eq!(new_x, orig_x + dx)
670    }
671
672    #[test]
673    fn move_point_y() {
674        let mut mock = MockGraphicsState::new();
675        let mut gs = mock.graphics_state(0, 100);
676        let point_ix = 0;
677        let orig_y = gs.zones[1].point(point_ix).unwrap().y;
678        let dy = F26Dot6::from_bits(10);
679        // movement in y is prevented post-iup when backward
680        // compatibility is enabled
681        gs.did_iup_x = true;
682        gs.did_iup_y = true;
683        gs.move_point(ZonePointer::Glyph, 0, dy).unwrap();
684        assert_eq!(orig_y, gs.zones[1].point(point_ix).unwrap().y);
685        // allow movement
686        gs.did_iup_x = false;
687        gs.did_iup_y = false;
688        gs.move_point(ZonePointer::Glyph, 0, dy).unwrap();
689        let new_y = gs.zones[1].point(point_ix).unwrap().y;
690        assert_ne!(orig_y, new_y);
691        assert_eq!(new_y, orig_y + dy)
692    }
693
694    #[test]
695    fn move_point_x_and_y() {
696        let mut mock = MockGraphicsState::new();
697        let mut gs = mock.graphics_state(100, 50);
698        let point_ix = 0;
699        let orig_point = gs.zones[1].point(point_ix).unwrap();
700        let dist = F26Dot6::from_bits(10);
701        // prevent movement in x and y
702        gs.did_iup_x = true;
703        gs.did_iup_y = true;
704        gs.move_point(ZonePointer::Glyph, 0, dist).unwrap();
705        assert_eq!(orig_point, gs.zones[1].point(point_ix).unwrap());
706        // allow movement
707        gs.backward_compatibility = false;
708        gs.did_iup_x = false;
709        gs.did_iup_y = false;
710        gs.move_point(ZonePointer::Glyph, 0, dist).unwrap();
711        let point = gs.zones[1].point(point_ix).unwrap();
712        assert_eq!(point.map(F26Dot6::to_bits), Point::new(4, -16));
713    }
714
715    #[test]
716    fn move_original_x() {
717        let mut mock = MockGraphicsState::new();
718        let mut gs = mock.graphics_state(100, 0);
719        let point_ix = 0;
720        let orig_x = gs.zones[1].original(point_ix).unwrap().x;
721        let dx = F26Dot6::from_bits(10);
722        gs.move_original(ZonePointer::Glyph, 0, dx).unwrap();
723        let new_x = gs.zones[1].original(point_ix).unwrap().x;
724        assert_eq!(new_x, orig_x + dx)
725    }
726
727    #[test]
728    fn move_original_y() {
729        let mut mock = MockGraphicsState::new();
730        let mut gs = mock.graphics_state(0, 100);
731        let point_ix = 0;
732        let orig_y = gs.zones[1].original(point_ix).unwrap().y;
733        let dy = F26Dot6::from_bits(10);
734        gs.move_original(ZonePointer::Glyph, 0, dy).unwrap();
735        let new_y = gs.zones[1].original(point_ix).unwrap().y;
736        assert_eq!(new_y, orig_y + dy)
737    }
738
739    #[test]
740    fn move_original_x_and_y() {
741        let mut mock = MockGraphicsState::new();
742        let mut gs = mock.graphics_state(100, 50);
743        let point_ix = 0;
744        let dist = F26Dot6::from_bits(10);
745        gs.move_original(ZonePointer::Glyph, 0, dist).unwrap();
746        let point = gs.zones[1].original(point_ix).unwrap();
747        assert_eq!(point.map(F26Dot6::to_bits), Point::new(9, 4));
748    }
749
750    #[test]
751    fn move_zp2_point() {
752        let mut mock = MockGraphicsState::new();
753        let mut gs = mock.graphics_state(100, 50);
754        gs.zp2 = ZonePointer::Glyph;
755        let point_ix = 0;
756        let orig_point = gs.zones[1].point(point_ix).unwrap();
757        let dx = F26Dot6::from_bits(10);
758        let dy = F26Dot6::from_bits(-10);
759        // prevent movement in x and y
760        gs.did_iup_x = true;
761        gs.did_iup_y = true;
762        gs.move_zp2_point(point_ix, dx, dy, false).unwrap();
763        assert_eq!(orig_point, gs.zones[1].point(point_ix).unwrap());
764        // allow movement
765        gs.backward_compatibility = false;
766        gs.did_iup_x = false;
767        gs.did_iup_y = false;
768        gs.move_zp2_point(point_ix, dx, dy, false).unwrap();
769        let point = gs.zones[1].point(point_ix).unwrap();
770        assert_eq!(point, orig_point + Point::new(dx, dy));
771    }
772
773    #[test]
774    fn point_displacement() {
775        let mut mock = MockGraphicsState::new();
776        let mut gs = mock.graphics_state(100, 50);
777        gs.zp0 = ZonePointer::Glyph;
778        gs.rp1 = 0;
779        assert_eq!(
780            gs.point_displacement(1).unwrap(),
781            PointDisplacement {
782                zone: ZonePointer::Glyph,
783                point_ix: 0,
784                dx: F26Dot6::from_f64(-0.1875),
785                dy: F26Dot6::from_f64(-0.09375),
786            }
787        );
788        gs.rp2 = 2;
789        assert_eq!(
790            gs.point_displacement(0).unwrap(),
791            PointDisplacement {
792                zone: ZonePointer::Glyph,
793                point_ix: 2,
794                dx: F26Dot6::from_f64(0.390625),
795                dy: F26Dot6::from_f64(0.203125),
796            }
797        );
798    }
799
800    #[test]
801    fn in_bounds_rejects_index_equal_to_len() {
802        let mut mock = MockGraphicsState::new();
803        let gs = mock.graphics_state(100, 50);
804        let len = gs.zones[ZonePointer::Glyph as usize].points.len();
805        assert!(!gs.in_bounds([(ZonePointer::Glyph, len)]));
806        assert!(gs.in_bounds([(ZonePointer::Glyph, len.saturating_sub(1))]));
807    }
808
809    struct MockGraphicsState {
810        points: [Point<F26Dot6>; 3],
811        original: [Point<F26Dot6>; 3],
812        contours: [u16; 1],
813        flags: [PointFlags; 3],
814    }
815
816    impl MockGraphicsState {
817        fn new() -> Self {
818            Self {
819                points: f26dot6_points([(-5, -20), (10, 10), (20, 20)]),
820                original: f26dot6_points([(0, 0), (10, 10), (20, -42)]),
821                flags: [PointFlags::default(); 3],
822                contours: [3],
823            }
824        }
825
826        fn graphics_state(&mut self, fv_x: i32, fv_y: i32) -> GraphicsState<'_> {
827            let glyph = Zone {
828                unscaled: &mut [],
829                original: &mut self.original,
830                points: &mut self.points,
831                contours: &self.contours,
832                flags: &mut self.flags,
833            };
834            let v = math::normalize14(fv_x, fv_y);
835            let mut gs = GraphicsState {
836                zones: [Zone::default(), glyph],
837                freedom_vector: v,
838                proj_vector: v,
839                zp0: ZonePointer::Glyph,
840                ..Default::default()
841            };
842            gs.update_projection_state();
843            gs
844        }
845    }
846
847    fn point_markers() -> [PointFlags; 2] {
848        let untouched = PointFlags::default();
849        let mut touched = untouched;
850        touched.set_marker(PointMarker::TOUCHED);
851        [untouched, touched]
852    }
853
854    fn f26dot6_points<const N: usize>(points: [(i32, i32); N]) -> [Point<F26Dot6>; N] {
855        points.map(|point| Point::new(F26Dot6::from_bits(point.0), F26Dot6::from_bits(point.1)))
856    }
857}