From 7026860073aabde9b6d3b521c19f3547a8a28cce Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 7 Oct 2026 08:26:46 +0800 Subject: [PATCH] fix(svg): retain computed path coordinates in native geometry Read the cascaded native path representation instead of re-parsing display-oriented CSSOM serialization. Preserve binary32 coordinates exactly when passing them to the geometry parser, including large endpoints, curves and arcs. Regression coverage exercises all command families and verifies style override/removal on a live SVG path. The geometry fixture edits a stylesheet directly to isolate native computed-path precision. Main's pre-existing inline CSSOM write path serializes declarations before style resolution and has a separate write-side precision limitation. --- .../context_bootstrap/svg_runtime/builders.rs | 13 +-- .../src/context_bootstrap/svg_runtime/mod.rs | 1 + .../svg_runtime/path_data.rs | 104 ++++++++++++++++++ moli-renderer-v8/src/native_bridge/element.rs | 2 +- moli-renderer-v8/src/script_vm/tests/mod.rs | 1 + .../tests/svg_computed_path_precision.rs | 29 +++++ 6 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 moli-renderer-v8/src/context_bootstrap/svg_runtime/path_data.rs create mode 100644 moli-renderer-v8/src/script_vm/tests/svg_computed_path_precision.rs diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs index cc25df86ff..1b8a74a5f1 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs @@ -564,14 +564,13 @@ fn svg_computed_path_data( handle: crate::document_runtime::DomHandle, ) -> Option { let computed = - crate::native_bridge::element::computed_style_property_for_handle(runtime, handle, "d"); - let value = computed.trim(); - let inner = value.strip_prefix("path(")?.strip_suffix(')')?.trim(); - let quote = inner.chars().next()?; - if !matches!(quote, '\'' | '"') || !inner.ends_with(quote) { - return None; + crate::native_bridge::element::ComputedStyleRead::new(runtime, handle).computed_values()?; + match computed.clone_d() { + style::values::computed::DProperty::Path(path) => { + Some(super::path_data::geometry_path_data(&path)) + } + style::values::computed::DProperty::None => None, } - Some(inner[quote.len_utf8()..inner.len() - quote.len_utf8()].to_owned()) } fn svg_geometry_length_attribute_for_handle( diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs index 63d786c49f..956e15bc13 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs @@ -16,6 +16,7 @@ use moli_svg::{ mod bindings; mod builders; mod callbacks; +mod path_data; mod rect; const SVG_GRAPHICS_TRANSFORM_SLOT: &str = "__moliSvgGraphicsTransform"; diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/path_data.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/path_data.rs new file mode 100644 index 0000000000..b14d48b907 --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/path_data.rs @@ -0,0 +1,104 @@ +use std::fmt::Write; +use style::values::specified::svg_path::{CoordPair, PathCommand, SVGPathData}; + +/// Keep native computed coordinates when passing a cascaded path to geometry. +/// CSSOM number serialization rounds values for display and is not a geometry +/// representation. Promote each binary32 coordinate before writing it so the +/// double-precision geometry parser receives that coordinate's exact value. +pub(super) fn geometry_path_data(path: &SVGPathData) -> String { + let path = path.normalize(false); + let mut output = String::new(); + for command in path.commands() { + let result = match *command { + PathCommand::Move { point } => { + let [x, y] = coordinates(point); + write!(output, "M{x} {y} ") + } + PathCommand::Line { point } => { + let [x, y] = coordinates(point); + write!(output, "L{x} {y} ") + } + PathCommand::HLine { x } => write!(output, "H{} ", f64::from(f32::from(x))), + PathCommand::VLine { y } => write!(output, "V{} ", f64::from(f32::from(y))), + PathCommand::CubicCurve { + point, + control1, + control2, + } => { + let [x, y] = coordinates(point); + let [x1, y1] = coordinates(control1); + let [x2, y2] = coordinates(control2); + write!(output, "C{x1} {y1} {x2} {y2} {x} {y} ") + } + PathCommand::QuadCurve { point, control1 } => { + let [x, y] = coordinates(point); + let [x1, y1] = coordinates(control1); + write!(output, "Q{x1} {y1} {x} {y} ") + } + PathCommand::SmoothCubic { point, control2 } => { + let [x, y] = coordinates(point); + let [x2, y2] = coordinates(control2); + write!(output, "S{x2} {y2} {x} {y} ") + } + PathCommand::SmoothQuad { point } => { + let [x, y] = coordinates(point); + write!(output, "T{x} {y} ") + } + PathCommand::Arc { + point, + radii, + arc_sweep, + arc_size, + rotate, + } => { + let [x, y] = coordinates(point); + let rx = f64::from(radii.rx); + let ry = f64::from(radii.ry.as_ref().copied().unwrap_or(radii.rx)); + let angle = f64::from(rotate); + write!( + output, + "A{rx} {ry} {angle} {} {} {x} {y} ", + arc_size as u8, arc_sweep as u8 + ) + } + PathCommand::Close => output.write_str("Z "), + }; + result.expect("writing SVG geometry path data to a String is infallible"); + } + output +} + +fn coordinates(point: impl Into) -> [f64; 2] { + let point = point.into(); + [f64::from(point.x), f64::from(point.y)] +} + +#[cfg(test)] +mod tests { + use super::*; + use moli_svg::{SvgGeometryElement, point_at_length, segments_for_element}; + + #[test] + fn computed_path_coordinates_retain_precision_across_svg_commands() { + for raw in [ + "M0 0L134217728 0", + "M0 0h134217728v1", + "M1 2c0 1 1 1 2 0s1 -1 2 0", + "M1 2q1 1 2 0t2 0", + "M0 0a3 2 45 1 1 5 2", + "M1 2L4 2L4 6z", + ] { + let (native, valid) = SVGPathData::parse_bytes(raw.as_bytes()); + assert!(valid); + let serialized = geometry_path_data(&native); + let (reparsed, valid) = SVGPathData::parse_bytes(serialized.as_bytes()); + assert!(valid); + assert_eq!(reparsed.normalize(false), native.normalize(false), "{raw}"); + let segments = segments_for_element(SvgGeometryElement::Path { d: serialized }); + let point = point_at_length(&segments, f64::MAX); + let direct = segments_for_element(SvgGeometryElement::Path { d: raw.to_owned() }); + let expected = point_at_length(&direct, f64::MAX); + assert_eq!([point.x, point.y], [expected.x, expected.y], "{raw}"); + } + } +} diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs index 4da669b41b..d28cbc9463 100644 --- a/moli-renderer-v8/src/native_bridge/element.rs +++ b/moli-renderer-v8/src/native_bridge/element.rs @@ -165,7 +165,7 @@ pub(crate) use shadow_dom::{ ensure_shadow_root_adopted_style_sheets_initialized, }; pub(crate) use styles::{ - StyleMode, active_css_animation_transform_value, + ComputedStyleRead, StyleMode, active_css_animation_transform_value, computed_style_properties_for_inspector_handle, computed_style_property_values_for_document_snapshot, css_animation_start_applies, cssom_style_entry_requires_structured_parser, diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 30086da630..e48295209c 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2150,6 +2150,7 @@ mod http_fixture; mod media_owner_playback_interfaces; mod media_recorder_shell; mod svg_animation_interfaces; +mod svg_computed_path_precision; mod svg_transform_sync_consolidation; mod audio_event_interfaces; diff --git a/moli-renderer-v8/src/script_vm/tests/svg_computed_path_precision.rs b/moli-renderer-v8/src/script_vm/tests/svg_computed_path_precision.rs new file mode 100644 index 0000000000..93907ed835 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/svg_computed_path_precision.rs @@ -0,0 +1,29 @@ +use super::*; + +#[test] +fn svg_computed_path_precision_survives_native_geometry_and_style_cascades() { + let mut vm = new_parsed_test_vm( + "https://svg-path-precision.test/", + "", + ); + assert_eq!( + vm.eval( + r#"(() => { + const path = document.querySelector('path'); + const check = (condition, label) => { if (!condition) throw Error(label); }; + check(path.getTotalLength() === 134217728, 'native computed attribute coordinates'); + const end = path.getPointAtLength(134217728); + check(end.x === 134217728 && end.y === 0, 'native computed endpoint'); + const sheet = document.getElementById('geometry-style'); + sheet.textContent = 'path { d: path("M0 0L67108864 0"); }'; + check(path.getTotalLength() === 67108864, 'cascaded path overrides attribute'); + check(path.getAttribute('d') === 'M0 0L134217728 0', 'attribute unchanged'); + sheet.textContent = ''; + check(path.getTotalLength() === 134217728, 'attribute restored after mutation'); + return true; + })()"# + ) + .unwrap(), + "true" + ); +}