Skip to content

Add SVG 2 arcs joins with optional rounded miter clipping - #965

Closed
SonyStone wants to merge 2 commits into
nical:mainfrom
SonyStone:feature/868-add-arcs-line-join-type
Closed

SonyStone wants to merge 2 commits into
nical:mainfrom
SonyStone:feature/868-add-arcs-line-join-type

Conversation

@SonyStone

Copy link
Copy Markdown

The issue: #868

Summary

Adds LineJoin::Arcs to lyon_path and implements it in lyon_tessellation.
Original segment tangents and curvature are retained before flattening. Private
modules construct support geometry and triangulate the join, reusing scratch
allocations.

The construction follows the SVG 2 Candidate Recommendation, 4 October 2018:
support-circle intersection and radius adjustment, the excessive-curvature guard,
opposite-parallel rectangle handling, and arc-length miter clipping. Nonnegative
miter limits are accepted, including zero and sub-unit limits.

A second, separable commit adds the nonstandard ArcsClip::Round extension.
It fits tangent-continuous biarcs to actual miter-cut directions, without moving
the existing stroke mesh. It is not necessarily a semicircle and may extend past
the miter limit. Invalid or non-convex fits retain the flat cut. SVG behavior
remains the default.

No editor, canvas persistence, support-preview API, historical join modes, or
public profiling switches are included.

Commit sequence

  1. 455ff35 — SVG 2 construction, regression tests, SVG example, and benchmarks.
  2. 1692323 — optional rounded clipping, metadata/winding tests, and serialization migration test.

The first commit builds and passes the workspace tests independently. The second
can be considered separately if the extension is outside Lyon's desired scope.

API

use lyon_tessellation::{ArcsClip, LineJoin, StrokeOptions};

let svg2 = StrokeOptions::default()
    .with_line_join(LineJoin::Arcs)
    .with_line_width(8.0)
    .with_miter_limit(4.0);

let rounded = svg2.with_arcs_clip(ArcsClip::Round);

Compatibility points requiring maintainer review:

  • Adding a variant to the existing exhaustive LineJoin enum breaks downstream
    exhaustive matches. Release/version policy must be decided before merging.
    The new variant is appended so existing serialized variant indices are not shifted.
  • StrokeOptions is already non-exhaustive. The second commit adds a field and
    builder method. Missing fields in old JSON default to flat clipping; binary
    serialization compatibility is not claimed.
  • MINIMUM_MITER_LIMIT changes from 1 to 0 for all joins, not only arcs.
  • There are no new production dependencies. serde_json is a dev dependency for
    the migration test.

Validation

On Windows x86_64, stable Rust 1.90.0:

  • cargo test --workspace --all-features passes.
  • cargo check -p lyon_tessellation --no-default-features passes.
  • cargo check --workspace --target wasm32-unknown-unknown passes.
  • cargo +nightly test -p lyon_tessellation --all-features passes.
  • Tessellation coverage: 281 unit tests, 9 integration tests, 9 doctests.
  • git diff --check passes.
  • Ordinary Clippy is blocked by the pre-existing overly_complex_bool_expr
    diagnostic in crates/tessellation/src/fill.rs:772, reproduced on the unchanged
    base. With that lint allowed, the check succeeds; existing test warnings remain.
    No warnings were reported in the newly added arcs modules, integration test, or example.

Linux CI and any maintainer-selected MSRV still need validation. Existing unused
num_traits::Float import warnings remain in the no-std build.

Generate a comparison rendered from the actual triangle meshes:

cargo run -p lyon_tessellation --example arcs -- joins.svg

The example compares Round, MiterClip, SVG 2 Arcs, and SVG 2 with rounded cuts,
without relying on browser support for arcs joins.

Performance: open merge blocker

Do not merge on the assumption that existing joins are unaffected.

The unchanged upstream logo benchmarks were built separately at base
994526f2766c7e9bc5bb16687232fd7e8999dbdf and at this candidate. Four complete
alternating pairs were measured with default release settings on an Intel
Core i9-13900H, Windows x86_64, Rust 1.90.0. Medians of reported ns/iteration:

Existing join Upstream Candidate Change
Miter 43,633 47,754 +9.4%
Bevel 46,261.5 51,593.5 +11.5%
Round 56,916 66,268 +16.4%

This is a local microbenchmark result, not a cross-platform estimate. The
standard-only commit also regressed these cases in an earlier five-pair run.
Profiling and reducing this cost is required before calling the implementation
merge-ready.

Reproduce the existing tests in separate checkouts:

cargo run -p tess_bench --release -- stroke_

The new same-input, reused-buffer benchmarks can be selected with arcs,
round_curved, and miter_clip_lines. Initial smoke measurements were noisy;
they are not sufficient to quantify the overhead of rounded clipping.

Remaining correctness/scope questions

  • The SVG comparison example currently combines triangles into one nonzero-filled
    path without normalizing their winding. Overlapping triangles with opposite
    winding cancel, producing apparent holes. Coordinate checks confirm coverage
    in those regions. The exporter needs correction, and mixed mesh winding needs
    a separate review for back-face culling. This has not been fixed in these commits.
  • Degenerate or numerically unmeshable joins currently use a round safety
    fallback. SVG 2 describes a line construction for degenerate Bezier handles;
    that behavior still needs reconciliation and dedicated conformance tests.
    This PR does not claim complete SVG conformance.
  • Variable-width supports use width at the join, not derivatives of the width
    profile.
  • Pre-flattened paths have no curvature and use the straight-segment behavior.
    Elliptical arcs converted to Beziers use those Beziers' curvature.
  • Existing stroke self-overlap/alpha-compositing limitations remain.

Please review the public API and desired scope of the rounded-cut extension while
the performance and degenerate-handle questions are addressed.

Replaces #964 after renaming the source branch. The implementation commits are unchanged.

Preserve endpoint differentials before flattening, adjust disjoint supports, and apply arc-length miter clipping. Include geometry regressions, a mesh-rendered SVG example, and same-input benchmarks. Permit nonnegative SVG 2 miter limits.
Expose ArcsClip::Butt (SVG 2 default) and ArcsClip::Round (nonstandard extension). Preserve base geometry and vertex metadata; add regression tests and JSON migration coverage.
@SonyStone

Copy link
Copy Markdown
Author

Closing this upstream draft while the implementation is prepared and reviewed in the fork: SonyStone#1. The API, correctness and performance work is still in progress.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant