From d23f5f36bb9657159ba5468aa9c92d939786922b Mon Sep 17 00:00:00 2001 From: ".CooCoo" Date: Thu, 12 Nov 2026 11:45:57 -0800 Subject: [PATCH] craft-drawingml: outline digests print a near-zero coordinate without its sign (#1127) outlines_match_the_string_dispatch failed on macOS for curvedRightArrow while Linux and Windows passed. The digest prints each coordinate with {:.3}, which writes a residue such as -1.4e-14 from the sine or cosine of a right angle as "-0.000". The sign of that residue depends on the platform's libm, so the digest did too. digest_coord now maps every coordinate that prints as zero to 0.0 before it is printed. The digests of pieWedge, curvedRightArrow and curvedDownArrow, the only names whose outlines held such residues, are taken again; no outline changes. --- crates/drawingml/src/preset/extra.rs | 45 ++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/crates/drawingml/src/preset/extra.rs b/crates/drawingml/src/preset/extra.rs index ad9efb3bd..391295891 100644 --- a/crates/drawingml/src/preset/extra.rs +++ b/crates/drawingml/src/preset/extra.rs @@ -764,20 +764,32 @@ mod tests { /// Sizes the digests were taken at: wide, tall and square. const GOLD_SIZES: [(f64, f64); 4] = [(120.0, 80.0), (300.0, 50.0), (64.0, 64.0), (50.0, 300.0)]; + /// `v` as the digest prints it: a value that prints as zero at 3 decimal places becomes + /// `0.0`, so a residue such as `-1.4e-14` left by a sine or cosine of a right angle prints + /// `0.000` whatever its sign. Its sign depends on the platform's libm, and `{:.3}` prints a + /// negative residue as `-0.000`. + fn digest_coord(v: f64) -> f64 { + if v.abs() < 0.0005 { 0.0 } else { v } + } + /// FNV-1a over each outline's path elements, for every adjust set and size. Coordinates are - /// printed to 3 decimal places so last-bit libm differences between platforms do not change it. + /// printed to 3 decimal places through [`digest_coord`] so last-bit libm differences between + /// platforms do not change it. fn outline_digest(name: &str) -> u64 { use std::fmt::Write; + let c = digest_coord; let mut text = String::new(); for adj in GOLD_ADJ { for (w, h) in GOLD_SIZES { let g = build(name, w, h, adj).unwrap_or_else(|| panic!("{name} builds no outline")); for el in g.outline().elements() { let _ = match *el { - PathEl::MoveTo(p) => write!(text, "M{:.3},{:.3}", p.x, p.y), - PathEl::LineTo(p) => write!(text, "L{:.3},{:.3}", p.x, p.y), - PathEl::QuadTo(a, p) => write!(text, "Q{:.3},{:.3},{:.3},{:.3}", a.x, a.y, p.x, p.y), - PathEl::CurveTo(a, b, p) => write!(text, "C{:.3},{:.3},{:.3},{:.3},{:.3},{:.3}", a.x, a.y, b.x, b.y, p.x, p.y), + PathEl::MoveTo(p) => write!(text, "M{:.3},{:.3}", c(p.x), c(p.y)), + PathEl::LineTo(p) => write!(text, "L{:.3},{:.3}", c(p.x), c(p.y)), + PathEl::QuadTo(a, p) => write!(text, "Q{:.3},{:.3},{:.3},{:.3}", c(a.x), c(a.y), c(p.x), c(p.y)), + PathEl::CurveTo(a, b, p) => { + write!(text, "C{:.3},{:.3},{:.3},{:.3},{:.3},{:.3}", c(a.x), c(a.y), c(b.x), c(b.y), c(p.x), c(p.y)) + } PathEl::ClosePath => write!(text, "Z"), }; } @@ -789,21 +801,23 @@ mod tests { /// Digests of every name's outlines as the string dispatch built them (craft-libs 2b35d243; /// DeckCraft's fc35ad67 gave other digests for curvedRightArrow and curvedDownArrow), so - /// this refactor is proven output-identical: any changed outline fails here. + /// this refactor is proven output-identical: any changed outline fails here. pieWedge, + /// curvedRightArrow and curvedDownArrow were taken again when [`digest_coord`] began printing + /// their near-zero residues as `0.000`; their outlines are unchanged. const GOLDEN: [(&str, u64); 38] = [ ("lineInv", 0x4a58a16923e54a4f), ("nonIsoscelesTrapezoid", 0x79eca0b36812e282), - ("pieWedge", 0x2a0d3ffe5e9a481d), + ("pieWedge", 0x5fb58e0214989f4d), ("leftRightArrowCallout", 0x9cd88f0ab55f76e5), ("upDownArrowCallout", 0x568bc91708855007), ("quadArrowCallout", 0xca59015298b1584b), ("circularArrow", 0x4227aa468cfe990e), ("leftCircularArrow", 0x6fc3779ce4b08914), ("leftRightCircularArrow", 0x9dd43d9ba80e47df), - ("curvedRightArrow", 0x83e7dd251401715d), + ("curvedRightArrow", 0xf60d53e01a88930c), ("curvedLeftArrow", 0x82472848de4a9fec), ("curvedUpArrow", 0xcfbd88c1aea4bf9d), - ("curvedDownArrow", 0xa6f75eaf0ee71d49), + ("curvedDownArrow", 0x01b1ac2f13de11b9), ("swooshArrow", 0x63ff193fdf1d0427), ("callout1", 0xe7e0410f96282e8d), ("callout2", 0xcaa7730106a9ee1b), @@ -831,6 +845,19 @@ mod tests { ("chartPlus", 0x979031aae16250d7), ]; + /// A near-zero residue prints the same whichever sign the platform's libm left on it, and + /// values that round away from zero keep their sign. + #[test] + fn digest_prints_near_zero_residues_without_a_sign() { + let print = |v: f64| format!("{:.3}", digest_coord(v)); + for v in [-1.4210854715202004e-14, 1.4210854715202004e-14, -7.105427357601002e-15, -0.0, 0.0, -0.00049, 0.00049] { + assert_eq!(print(v), "0.000", "{v:e}"); + } + assert_eq!(print(-0.0006), "-0.001"); + assert_eq!(print(-12.25), "-12.250"); + assert_eq!(print(0.0006), "0.001"); + } + /// Every name builds the same outline as before the `Name` enum, and unknown names build nothing. #[test] fn outlines_match_the_string_dispatch() {