From 9da253ebee1fc10a6f7cc6551119527cffe506fc Mon Sep 17 00:00:00 2001 From: KenM76 Date: Mon, 2 Nov 2026 17:24:42 -0500 Subject: [PATCH] fix(annot): a 3D poster fitted to its rectangle with rounding is taken A poster box fitted to the annotation rectangle in floating point can end a few ulps outside it (a 7x3 picture centred in 0 100 3.6 110.8 spans -2.2e-16 to 3.6000000000000005), and the exact containment check refused it. The check now allows a slack of 1e-9 of the rectangle's largest coordinate, clamps the box to the rectangle, and only then requires it to have width and height, so a box wholly within the slack outside the rectangle is still refused. Tests: a_3d_poster_fitted_to_its_rectangle_with_rounding_is_taken fails without the slack (Invalid); the new case in a_3d_poster_outside_its_rectangle_or_without_a_size_is_refused fails when the size check runs before the clamp. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9 --- crates/annot/src/tests.rs | 20 ++++++++++++++++++++ crates/annot/src/threed.rs | 7 +++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/crates/annot/src/tests.rs b/crates/annot/src/tests.rs index c31ef19b17..aae69ddb7a 100644 --- a/crates/annot/src/tests.rs +++ b/crates/annot/src/tests.rs @@ -2296,6 +2296,7 @@ fn a_3d_poster_outside_its_rectangle_or_without_a_size_is_refused() { (picture([50.0, 100.0, 250.0, 300.0], (10.0, 10.0)), "starts left of the rectangle"), (picture([100.0, 100.0, 300.0, 301.0], (10.0, 10.0)), "ends above it"), (picture([100.0, 100.0, 300.0, 300.0], (0.0, 10.0)), "has no width"), + (picture([100.0 - 2e-7, 150.0, 100.0 - 1e-7, 200.0], (10.0, 10.0)), "lies outside within the rounding slack"), ] { let mut doc = fixture(); let new = NewModel3d { @@ -2310,3 +2311,22 @@ fn a_3d_poster_outside_its_rectangle_or_without_a_size_is_refused() { assert_eq!(list(&doc, 0).len(), 1, "{why}: nothing added"); } } + +/// A poster box fitted to the rectangle in floating point ends an ulp outside it: a 7×3 picture +/// centred in [0 100 3.6 110.8] spans -2.2e-16 to 3.6000000000000005. It is taken, inside the +/// rectangle. +#[test] +fn a_3d_poster_fitted_to_its_rectangle_with_rounding_is_taken() { + let mut doc = fixture(); + let at = [-2.220446049250313e-16, 104.62857142857143, 3.6000000000000005, 106.17142857142858]; + let poster = PosterPicture { xobject: ObjRef { num: 6, generation: 0 }, image: false, size: (7.0, 3.0), at }; + let new = NewModel3d { + page: 0, + rect: [0.0, 100.0, 3.6, 110.8], + model: b"PRC rest", + activation: Activation3d::default(), + poster: Some(poster), + contents: "", + }; + assert_eq!(add_3d_model(&mut doc, &new, &meta("m")), Ok(1)); +} diff --git a/crates/annot/src/threed.rs b/crates/annot/src/threed.rs index be632337cb..cea6568bda 100644 --- a/crates/annot/src/threed.rs +++ b/crates/annot/src/threed.rs @@ -199,8 +199,11 @@ fn annotation_dict(new: &NewModel3d<'_>, rect: [f64; 4], page: ObjRef, stream: O /// The poster drawing `p` into its box, which must lie within `rect`. fn picture_poster(rect: [f64; 4], p: &PosterPicture) -> Result { let at = normalize(p.at); - let inside = finite(&p.at) && at[0] >= rect[0] && at[1] >= rect[1] && at[2] <= rect[2] && at[3] <= rect[3] && at[2] > at[0] && at[3] > at[1]; - if !inside { + // A box fitted to `rect` in floating point can end a few ulps outside it: allow that, then clamp. + let slack = rect.iter().fold(1.0_f64, |m, v| m.max(v.abs())) * 1e-9; + let near = finite(&p.at) && at[0] >= rect[0] - slack && at[1] >= rect[1] - slack && at[2] <= rect[2] + slack && at[3] <= rect[3] + slack; + let at = [at[0].max(rect[0]), at[1].max(rect[1]), at[2].min(rect[2]), at[3].min(rect[3])]; + if !(near && at[2] > at[0] && at[3] > at[1]) { return Err(AnnotError::Invalid("the poster must fit inside the 3D model's rectangle".into())); } let place = appearance::picture_place(at, p.image, Some(p.size)).ok_or_else(|| AnnotError::Invalid("the poster picture has no size".into()))?;