diff --git a/moli-layout/src/style.rs b/moli-layout/src/style.rs index 4cc8ab0d6..115ff2432 100644 --- a/moli-layout/src/style.rs +++ b/moli-layout/src/style.rs @@ -2375,19 +2375,17 @@ fn resolve_stylo_2d_transform( } else { match box_styles .transform - .to_transform_3d_matrix(Some(&reference_box)) + // Keep the computed matrix in double precision until mapping the + // final points. Casting coefficients to float here loses geometry + // precision even if they are widened again for LayoutTransform2D. + .to_transform_3d_matrix_f64(Some(&reference_box)) { Ok((_matrix, true)) => { has_unsupported_3d = true; None } Ok((matrix, false)) => Some(LayoutTransform2D::new([ - f64::from(matrix.m11), - f64::from(matrix.m12), - f64::from(matrix.m21), - f64::from(matrix.m22), - f64::from(matrix.m41), - f64::from(matrix.m42), + matrix.m11, matrix.m12, matrix.m21, matrix.m22, matrix.m41, matrix.m42, ])), Err(_) => { has_unsupported_3d = true; diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs index 32090e210..ecf340a22 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs @@ -1,5 +1,7 @@ use super::*; +mod transform_precision; + use base64::Engine as _; use super::super::main_document_lifecycle_completion::execute_main_document_lifecycle_on_owner_local_task; diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/transform_precision.rs b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/transform_precision.rs new file mode 100644 index 000000000..f2f0cae4d --- /dev/null +++ b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/transform_precision.rs @@ -0,0 +1,69 @@ +use super::*; + +#[tokio::test(flavor = "current_thread")] +async fn rotated_client_rects_retain_matrix_precision_until_point_mapping() { + run_page_vm_async_test(async move { + let loader = + crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); + let mut page_vm = test_page_vm_with_loader_and_document_url( + &loader, + Vec::new(), + Url::parse("https://example.com/transform-precision.html")?, + ); + page_vm.vm_mut().eval( + r#" +document.head.innerHTML = ``; +for (const angle of [0,15,30,45,135,-45]) { + for (const property of ['transform', 'rotate']) { + const element = document.createElement('div'); + element.id = property + angle; + element.style[property] = property === 'transform' ? `rotate(${angle}deg)` : `${angle}deg`; + document.body.appendChild(element); + } +} +'installed' +"#, + )?; + page_vm.vm_mut().sync_live_document_style_sources(); + page_vm + .vm_mut() + .screenshot_layout_snapshot(moli_layout::PaintViewport::new(200, 200, 1.0))? + .expect("transform fixture must retain a layout root"); + + // Chromium maps points with a double-precision transform, then rounds + // the resulting geometry to float. Rounding the matrix first changes + // x/y even when the final bounding width and height happen to match. + // Non-right angles exercise matrix coefficient precision. Exact + // quarter-turn snapping is a separate transform contract. + assert_eq!( + page_vm.vm_mut().eval( + r#"(() => { +const r = document.getElementById('transform45').getClientRects()[0]; +return [r.x, r.y, r.width, r.height, r.right, r.bottom].join('|'); +})()"#, + )?, + "-20.710678100585938|-20.710678100585938|141.42135620117188|141.42135620117188|120.71067810058594|120.71067810058594", + ); + assert_eq!( + page_vm.vm_mut().eval( + r#"(() => { +const geometry = id => { + const r = document.getElementById(id).getBoundingClientRect(); + return [r.x, r.y, r.width, r.height].join('|'); +}; +return [0,15,30,45,135,-45].filter(angle => + geometry('transform' + angle) !== geometry('rotate' + angle) +).join(','); +})()"#, + )?, + "", + "transform:rotate() and the individual rotate property must map identical points", + ); + Ok::<_, anyhow::Error>(()) + }) + .await + .expect("transform precision fixture should run"); +}