mirror of
https://github.com/lexmount/moli.git
synced 2026-10-16 00:01:20 +00:00
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:
@@ -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"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user