//! The one place this crate quantizes an `f64` staff-space value onto the //! `1/1024` grid (W3 §5 invariant 5). Every position this crate constructs //! — glyph offsets, caret stops, the run's `origin`, `bounds`, and //! `reserved_box` — goes through this function, so there is exactly one //! rounding rule to audit rather than one per call site. //! //! ## It is the *canonical* rounding rule, not a second one //! //! W3 §5 is explicit that this must not be a separate convention: //! //! > Positions are staff-space y-up, quantized on the same 1/1024 grid as //! > glyph positions (`resolved.rs:11-13`), **so text quantization is not a //! > second convention.** //! //! Revision 2 of this module implemented the grid arithmetic locally as //! `(v * 1024.0).round() / 1024.0` and *named* the divergence in a doc comment //! — `f64::round` is round-half-**away-from-zero**, while //! `epiphany_determinism::QuantizedCoord::from_staff_spaces` is round-half-to- //! **even** (Appendix D) — with the reasoning that this spike's values never //! land on a tie in practice. That reasoning was wrong twice over. It is not //! checkable (nothing asserted that no value ever ties, and a font metric or a //! padding constant could land on one at any time), and more importantly W3's //! requirement is about the *convention*, not about whether the two conventions //! happen to agree on today's inputs. Naming a divergence is not the same as //! being allowed to take it. //! //! So [`quantize_component`] now routes through `QuantizedCoord` itself. There //! is one quantizer in this project and this module calls it; the tie-break is //! whatever Appendix D says it is, today and after any future change to it. use epiphany_determinism::QuantizedCoord; /// Rounds `v` onto the canonical `1/1024` staff-space grid, using the /// project's own quantizer — **round-to-nearest, ties-to-even**, per /// Appendix D. /// /// # Panics /// /// If `v` is NaN, infinite, or so large that its scaled unit count leaves /// `i64` range. `QuantizedCoord::from_staff_spaces` returns `None` for those /// rather than saturating, and this crate deliberately does not paper over it /// with a default: a position that cannot be placed on the canonical grid is a /// bug in whatever computed it, and the fixture generator is the right place /// for it to stop. No such value occurs in this recipe — every input is a font /// metric or a stated constant — so this panic is a guard, not a code path. pub fn quantize_component(v: f64) -> f64 { QuantizedCoord::from_staff_spaces(v) .unwrap_or_else(|| { panic!( "{v} cannot be placed on the canonical 1/1024 staff-space grid (NaN, infinite, or \ outside i64 range) — see epiphany_determinism::QuantizedCoord::from_staff_spaces" ) }) .to_staff_spaces() } /// Whether `v` already sits **exactly** on the grid. /// /// Revision 2 tested `(scaled - scaled.round()).abs() < 1e-6`, which claimed /// exactness in its own doc comment while accepting anything within a /// tolerance — the precise defect it was written to prevent, since "within /// some tolerance of the grid" is what a fixed grid exists to rule out. /// /// The test is now a round-trip through the canonical quantizer: `v` is on the /// grid iff quantizing it changes nothing. That is exact by construction (the /// multiply and divide by `1024` are exact for every value this crate handles, /// `1024` being a power of two), and it rejects NaN, infinity and /// out-of-range values as off-grid rather than passing them to a subtraction /// that would produce `NaN < 1e-6 == false` by accident. pub fn is_on_grid(v: f64) -> bool { QuantizedCoord::from_staff_spaces(v).map(|q| q.to_staff_spaces()) == Some(v) } #[cfg(test)] mod tests { use super::*; /// The recipe's original nominal origin. Kept as a permanent record of the /// case even though §3 now states the quantized value: the property that /// matters is that an unrepresentable literal is *moved*, not that this /// particular one has since been fixed. #[test] fn quantizing_the_recipes_nominal_origin_lands_on_grid() { let q = quantize_component(1.6); assert!(is_on_grid(q)); assert_eq!(q, 1638.0 / 1024.0); } /// The constant the recipe now states must already be on the grid, so /// that quantizing it changes nothing. If someone re-edits /// `RUN_ORIGIN_STAFF` back to an unrepresentable literal, this fails /// rather than being repaired in silence. #[test] fn the_declared_run_origin_is_already_on_grid() { let (ox, oy) = crate::RUN_ORIGIN_STAFF; assert!( is_on_grid(ox as f64), "origin x {ox} is off the 1/1024 grid" ); assert!( is_on_grid(oy as f64), "origin y {oy} is off the 1/1024 grid" ); assert_eq!(quantize_component(ox as f64), ox as f64); assert_eq!(quantize_component(oy as f64), oy as f64); } #[test] fn the_unquantized_literal_is_not_on_grid() { // This is exactly the discrepancy the module doc comment names — // proven here so it cannot silently stop being true. assert!(!is_on_grid(1.6)); } #[test] fn is_on_grid_kills_a_value_one_unit_off() { let q = quantize_component(1.6); assert!(!is_on_grid(q + 1.0 / 2048.0)); } // ---- ties-to-even, the convention W3 requires ---- // // Each case below is a half-grid value where ties-to-even and // ties-away-from-zero DISAGREE, so reverting `quantize_component` to // `(v * 1024.0).round() / 1024.0` fails every one of them. A tie that both // rules resolve the same way (scaled = 1.5, where both give 2) would prove // nothing, and is checked separately below so this distinction is on the // record rather than implied. /// `0.5` grid units, positive: ties-to-even rounds **down to 0**; /// ties-away-from-zero would round up to 1 unit. #[test] fn positive_half_grid_ties_to_even_down() { let v = 1.0 / 2048.0; // scaled = 0.5 assert_eq!(quantize_component(v), 0.0); assert_ne!(quantize_component(v), 1.0 / 1024.0); } /// `2.5` grid units, positive: ties-to-even rounds **down to 2**; /// ties-away-from-zero would round up to 3. #[test] fn positive_half_grid_ties_to_even_at_two_and_a_half() { let v = 5.0 / 2048.0; // scaled = 2.5 assert_eq!(quantize_component(v), 2.0 / 1024.0); assert_ne!(quantize_component(v), 3.0 / 1024.0); } /// `-0.5` grid units: ties-to-even rounds **towards zero**; /// ties-away-from-zero would round to -1 unit. The sign matters because /// `f64::round`'s bias is away from zero in *both* directions, so a /// positive-only test would miss half of the divergence. #[test] fn negative_half_grid_ties_to_even_towards_zero() { let v = -1.0 / 2048.0; // scaled = -0.5 assert_eq!(quantize_component(v), 0.0); assert_ne!(quantize_component(v), -1.0 / 1024.0); } /// `-2.5` grid units: ties-to-even rounds **to -2**; /// ties-away-from-zero would round to -3. #[test] fn negative_half_grid_ties_to_even_at_minus_two_and_a_half() { let v = -5.0 / 2048.0; // scaled = -2.5 assert_eq!(quantize_component(v), -2.0 / 1024.0); assert_ne!(quantize_component(v), -3.0 / 1024.0); } /// A tie the two rules agree on, stated so the four tests above are /// understood as testing the *disagreement* and not merely "ties round /// somewhere". #[test] fn a_tie_the_two_conventions_agree_on_is_not_evidence() { let v = 3.0 / 2048.0; // scaled = 1.5; ties-even -> 2, ties-away -> 2 assert_eq!(quantize_component(v), 2.0 / 1024.0); assert_eq!((v * 1024.0).round() / 1024.0, 2.0 / 1024.0); } /// This module must not have its own arithmetic at all: quantizing agrees /// with the canonical type exactly, for ties and non-ties alike. #[test] fn quantizing_is_the_canonical_quantizer() { for v in [ 0.0, 1.6, -1.6, 1638.0 / 1024.0, 1.0 / 2048.0, -1.0 / 2048.0, 5.0 / 2048.0, -5.0 / 2048.0, 17.151_367_187_5, -0.362_304_687_5, ] { let canonical = QuantizedCoord::from_staff_spaces(v) .unwrap() .to_staff_spaces(); assert_eq!(quantize_component(v), canonical, "disagreement at {v}"); } } /// `is_on_grid` is exact, not tolerant. A value one part in `2^40` off the /// grid is off the grid; revision 2's `1e-6` tolerance accepted it. #[test] fn is_on_grid_is_exact_not_tolerant() { let on = 1638.0 / 1024.0; assert!(is_on_grid(on)); let barely_off = f64::from_bits(on.to_bits() + 1); assert_ne!( barely_off, on, "anchor: the perturbation must change the value" ); assert!( (barely_off * 1024.0 - (barely_off * 1024.0).round()).abs() < 1e-6, "anchor: revision 2's tolerant test would have ACCEPTED this value" ); assert!( !is_on_grid(barely_off), "one ULP off the grid is off the grid" ); } /// Non-finite and out-of-range values are off-grid, not accidentally /// on-grid. Revision 2's subtraction produced `NaN`, and `NaN < 1e-6` is /// `false`, so NaN came out off-grid by luck rather than by rule. #[test] fn non_finite_values_are_off_grid_by_rule() { assert!(!is_on_grid(f64::NAN)); assert!(!is_on_grid(f64::INFINITY)); assert!(!is_on_grid(f64::NEG_INFINITY)); assert!(!is_on_grid(1e30)); // scaled well past i64 range } }