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.
This commit is contained in:
ldm0
2026-10-08 21:36:39 +08:00
committed by Donough Liu
parent 10a8044788
commit 7026860073
6 changed files with 142 additions and 8 deletions
@@ -564,14 +564,13 @@ fn svg_computed_path_data(
handle: crate::document_runtime::DomHandle,
) -> Option<String> {
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(
@@ -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";
@@ -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<CoordPair>) -> [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}");
}
}
}
@@ -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,
@@ -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;
@@ -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/",
"<!doctype html><html><body><style id=geometry-style></style><svg xmlns='http://www.w3.org/2000/svg'><path d='M0 0L134217728 0'/></svg></body></html>",
);
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"
);
}