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() - 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 move_point_x() {
643        let mut mock = MockGraphicsState::new();
644        let mut gs = mock.graphics_state(100, 0);
645        let point_ix = 0;
646        let orig_x = gs.zones[1].point(point_ix).unwrap().x;
647        let dx = F26Dot6::from_bits(10);
648        // backward compatibility is on by default and we don't move x coord
649        gs.move_point(ZonePointer::Glyph, 0, dx).unwrap();
650        assert_eq!(orig_x, gs.zones[1].point(point_ix).unwrap().x);
651        // disable so we actually move
652        gs.backward_compatibility = false;
653        gs.move_point(ZonePointer::Glyph, 0, dx).unwrap();
654        let new_x = gs.zones[1].point(point_ix).unwrap().x;
655        assert_ne!(orig_x, new_x);
656        assert_eq!(new_x, orig_x + dx)
657    }
658
659    #[test]
660    fn move_point_y() {
661        let mut mock = MockGraphicsState::new();
662        let mut gs = mock.graphics_state(0, 100);
663        let point_ix = 0;
664        let orig_y = gs.zones[1].point(point_ix).unwrap().y;
665        let dy = F26Dot6::from_bits(10);
666        // movement in y is prevented post-iup when backward
667        // compatibility is enabled
668        gs.did_iup_x = true;
669        gs.did_iup_y = true;
670        gs.move_point(ZonePointer::Glyph, 0, dy).unwrap();
671        assert_eq!(orig_y, gs.zones[1].point(point_ix).unwrap().y);
672        // allow movement
673        gs.did_iup_x = false;
674        gs.did_iup_y = false;
675        gs.move_point(ZonePointer::Glyph, 0, dy).unwrap();
676        let new_y = gs.zones[1].point(point_ix).unwrap().y;
677        assert_ne!(orig_y, new_y);
678        assert_eq!(new_y, orig_y + dy)
679    }
680
681    #[test]
682    fn move_point_x_and_y() {
683        let mut mock = MockGraphicsState::new();
684        let mut gs = mock.graphics_state(100, 50);
685        let point_ix = 0;
686        let orig_point = gs.zones[1].point(point_ix).unwrap();
687        let dist = F26Dot6::from_bits(10);
688        // prevent movement in x and y
689        gs.did_iup_x = true;
690        gs.did_iup_y = true;
691        gs.move_point(ZonePointer::Glyph, 0, dist).unwrap();
692        assert_eq!(orig_point, gs.zones[1].point(point_ix).unwrap());
693        // allow movement
694        gs.backward_compatibility = false;
695        gs.did_iup_x = false;
696        gs.did_iup_y = false;
697        gs.move_point(ZonePointer::Glyph, 0, dist).unwrap();
698        let point = gs.zones[1].point(point_ix).unwrap();
699        assert_eq!(point.map(F26Dot6::to_bits), Point::new(4, -16));
700    }
701
702    #[test]
703    fn move_original_x() {
704        let mut mock = MockGraphicsState::new();
705        let mut gs = mock.graphics_state(100, 0);
706        let point_ix = 0;
707        let orig_x = gs.zones[1].original(point_ix).unwrap().x;
708        let dx = F26Dot6::from_bits(10);
709        gs.move_original(ZonePointer::Glyph, 0, dx).unwrap();
710        let new_x = gs.zones[1].original(point_ix).unwrap().x;
711        assert_eq!(new_x, orig_x + dx)
712    }
713
714    #[test]
715    fn move_original_y() {
716        let mut mock = MockGraphicsState::new();
717        let mut gs = mock.graphics_state(0, 100);
718        let point_ix = 0;
719        let orig_y = gs.zones[1].original(point_ix).unwrap().y;
720        let dy = F26Dot6::from_bits(10);
721        gs.move_original(ZonePointer::Glyph, 0, dy).unwrap();
722        let new_y = gs.zones[1].original(point_ix).unwrap().y;
723        assert_eq!(new_y, orig_y + dy)
724    }
725
726    #[test]
727    fn move_original_x_and_y() {
728        let mut mock = MockGraphicsState::new();
729        let mut gs = mock.graphics_state(100, 50);
730        let point_ix = 0;
731        let dist = F26Dot6::from_bits(10);
732        gs.move_original(ZonePointer::Glyph, 0, dist).unwrap();
733        let point = gs.zones[1].original(point_ix).unwrap();
734        assert_eq!(point.map(F26Dot6::to_bits), Point::new(9, 4));
735    }
736
737    #[test]
738    fn move_zp2_point() {
739        let mut mock = MockGraphicsState::new();
740        let mut gs = mock.graphics_state(100, 50);
741        gs.zp2 = ZonePointer::Glyph;
742        let point_ix = 0;
743        let orig_point = gs.zones[1].point(point_ix).unwrap();
744        let dx = F26Dot6::from_bits(10);
745        let dy = F26Dot6::from_bits(-10);
746        // prevent movement in x and y
747        gs.did_iup_x = true;
748        gs.did_iup_y = true;
749        gs.move_zp2_point(point_ix, dx, dy, false).unwrap();
750        assert_eq!(orig_point, gs.zones[1].point(point_ix).unwrap());
751        // allow movement
752        gs.backward_compatibility = false;
753        gs.did_iup_x = false;
754        gs.did_iup_y = false;
755        gs.move_zp2_point(point_ix, dx, dy, false).unwrap();
756        let point = gs.zones[1].point(point_ix).unwrap();
757        assert_eq!(point, orig_point + Point::new(dx, dy));
758    }
759
760    #[test]
761    fn point_displacement() {
762        let mut mock = MockGraphicsState::new();
763        let mut gs = mock.graphics_state(100, 50);
764        gs.zp0 = ZonePointer::Glyph;
765        gs.rp1 = 0;
766        assert_eq!(
767            gs.point_displacement(1).unwrap(),
768            PointDisplacement {
769                zone: ZonePointer::Glyph,
770                point_ix: 0,
771                dx: F26Dot6::from_f64(-0.1875),
772                dy: F26Dot6::from_f64(-0.09375),
773            }
774        );
775        gs.rp2 = 2;
776        assert_eq!(
777            gs.point_displacement(0).unwrap(),
778            PointDisplacement {
779                zone: ZonePointer::Glyph,
780                point_ix: 2,
781                dx: F26Dot6::from_f64(0.390625),
782                dy: F26Dot6::from_f64(0.203125),
783            }
784        );
785    }
786
787    struct MockGraphicsState {
788        points: [Point<F26Dot6>; 3],
789        original: [Point<F26Dot6>; 3],
790        contours: [u16; 1],
791        flags: [PointFlags; 3],
792    }
793
794    impl MockGraphicsState {
795        fn new() -> Self {
796            Self {
797                points: f26dot6_points([(-5, -20), (10, 10), (20, 20)]),
798                original: f26dot6_points([(0, 0), (10, 10), (20, -42)]),
799                flags: [PointFlags::default(); 3],
800                contours: [3],
801            }
802        }
803
804        fn graphics_state(&mut self, fv_x: i32, fv_y: i32) -> GraphicsState {
805            let glyph = Zone {
806                unscaled: &mut [],
807                original: &mut self.original,
808                points: &mut self.points,
809                contours: &self.contours,
810                flags: &mut self.flags,
811            };
812            let v = math::normalize14(fv_x, fv_y);
813            let mut gs = GraphicsState {
814                zones: [Zone::default(), glyph],
815                freedom_vector: v,
816                proj_vector: v,
817                zp0: ZonePointer::Glyph,
818                ..Default::default()
819            };
820            gs.update_projection_state();
821            gs
822        }
823    }
824
825    fn point_markers() -> [PointFlags; 2] {
826        let untouched = PointFlags::default();
827        let mut touched = untouched;
828        touched.set_marker(PointMarker::TOUCHED);
829        [untouched, touched]
830    }
831
832    fn f26dot6_points<const N: usize>(points: [(i32, i32); N]) -> [Point<F26Dot6>; N] {
833        points.map(|point| Point::new(F26Dot6::from_bits(point.0), F26Dot6::from_bits(point.1)))
834    }
835}