mirror of
https://github.com/penpot/penpot.git
synced 2026-09-08 13:09:22 +00:00
✨ Export solid and patterned strokes to WASM SVG (#11513)
* ✨ Export solid inner and outer strokes to WASM SVG SkSVGDevice drops save_layer+Clear used for path Outer strokes, so solid Inner/Outer strokes on closed shapes are expanded to filled outlines via stroke_to_path. Center and open paths stay on the shared stroke path. Closes #11381 * ✨ Export dotted, dashed, and mixed strokes to WASM SVG PathEffects do not survive SkSVGDevice, so dotted/dashed/mixed strokes expand via stroke_to_path with the effect kept, producing filled outline geometry for all alignments including Center. Closes #11382 * 🐛 Fix WASM SVG stroke export edge cases Keep expanded stroke outlines in local space under the leaf CTM so rotated rects/paths are not double-transformed; overlay open-path caps after dotted expansion; draw frame strokes outside the content clip; and route per-side rect/frame strokes through the evenodd band path.
This commit is contained in:
parent
1f12561427
commit
269aa36f82
@ -5,8 +5,9 @@ use skia_safe as skia;
|
|||||||
use crate::globals::TestRenderResourcesGuard;
|
use crate::globals::TestRenderResourcesGuard;
|
||||||
use crate::render::{FontStore, RenderResources};
|
use crate::render::{FontStore, RenderResources};
|
||||||
use crate::shapes::{
|
use crate::shapes::{
|
||||||
Fill, FontFamily, FontStyle, Frame, Group, GrowType, Paragraph, Rect, SolidColor, TextAlign,
|
Fill, FontFamily, FontStyle, Frame, Group, GrowType, Paragraph, Path, Rect, Segment,
|
||||||
TextContent, TextDirection, TextSpan, Type,
|
SolidColor, Stroke, StrokeKind, StrokeStyle, TextAlign, TextContent, TextDirection, TextSpan,
|
||||||
|
Type,
|
||||||
};
|
};
|
||||||
use crate::state::ShapesPool;
|
use crate::state::ShapesPool;
|
||||||
use crate::utils::uuid_from_u32_quartet;
|
use crate::utils::uuid_from_u32_quartet;
|
||||||
@ -153,6 +154,101 @@ pub(super) fn add_text_with_fills(
|
|||||||
shape.set_shape_type(Type::Text(content));
|
shape.set_shape_type(Type::Text(content));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds a rectangle with a single solid stroke (no fill).
|
||||||
|
pub(super) fn add_stroked_rect(
|
||||||
|
pool: &mut ShapesPool,
|
||||||
|
id: Uuid,
|
||||||
|
parent: Uuid,
|
||||||
|
(l, t, r, b): (f32, f32, f32, f32),
|
||||||
|
stroke: Stroke,
|
||||||
|
) {
|
||||||
|
let shape = pool.add_shape(id);
|
||||||
|
shape.set_parent(parent);
|
||||||
|
shape.set_shape_type(Type::Rect(Rect::default()));
|
||||||
|
shape.set_selrect(l, t, r, b);
|
||||||
|
shape.set_fills(vec![]);
|
||||||
|
shape.add_stroke(stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a closed rectangular path with a single solid stroke (no fill).
|
||||||
|
pub(super) fn add_stroked_closed_path(
|
||||||
|
pool: &mut ShapesPool,
|
||||||
|
id: Uuid,
|
||||||
|
parent: Uuid,
|
||||||
|
bounds: (f32, f32, f32, f32),
|
||||||
|
stroke: Stroke,
|
||||||
|
) {
|
||||||
|
add_stroked_path(pool, id, parent, bounds, stroke, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an open polyline path with a single solid stroke (no fill).
|
||||||
|
pub(super) fn add_stroked_open_path(
|
||||||
|
pool: &mut ShapesPool,
|
||||||
|
id: Uuid,
|
||||||
|
parent: Uuid,
|
||||||
|
bounds: (f32, f32, f32, f32),
|
||||||
|
stroke: Stroke,
|
||||||
|
) {
|
||||||
|
add_stroked_path(pool, id, parent, bounds, stroke, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_stroked_path(
|
||||||
|
pool: &mut ShapesPool,
|
||||||
|
id: Uuid,
|
||||||
|
parent: Uuid,
|
||||||
|
(l, t, r, b): (f32, f32, f32, f32),
|
||||||
|
stroke: Stroke,
|
||||||
|
closed: bool,
|
||||||
|
) {
|
||||||
|
let mut segments = vec![
|
||||||
|
Segment::MoveTo((l, t)),
|
||||||
|
Segment::LineTo((r, t)),
|
||||||
|
Segment::LineTo((r, b)),
|
||||||
|
Segment::LineTo((l, b)),
|
||||||
|
];
|
||||||
|
if closed {
|
||||||
|
segments.push(Segment::Close);
|
||||||
|
}
|
||||||
|
let path = Path::new(segments);
|
||||||
|
let shape = pool.add_shape(id);
|
||||||
|
shape.set_parent(parent);
|
||||||
|
shape.set_shape_type(Type::Path(path));
|
||||||
|
shape.set_selrect(l, t, r, b);
|
||||||
|
shape.set_fills(vec![]);
|
||||||
|
shape.add_stroke(stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn solid_stroke(kind: StrokeKind, width: f32, color: skia::Color) -> Stroke {
|
||||||
|
stroke_with_style(kind, StrokeStyle::Solid, width, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn dotted_stroke(kind: StrokeKind, width: f32, color: skia::Color) -> Stroke {
|
||||||
|
stroke_with_style(kind, StrokeStyle::Dotted, width, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn dashed_stroke(kind: StrokeKind, width: f32, color: skia::Color) -> Stroke {
|
||||||
|
stroke_with_style(kind, StrokeStyle::Dashed, width, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn mixed_stroke(kind: StrokeKind, width: f32, color: skia::Color) -> Stroke {
|
||||||
|
stroke_with_style(kind, StrokeStyle::Mixed, width, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stroke_with_style(
|
||||||
|
kind: StrokeKind,
|
||||||
|
style: StrokeStyle,
|
||||||
|
width: f32,
|
||||||
|
color: skia::Color,
|
||||||
|
) -> Stroke {
|
||||||
|
let mut stroke = match kind {
|
||||||
|
StrokeKind::Inner => Stroke::new_inner_stroke(width, style, None, None, None, None),
|
||||||
|
StrokeKind::Outer => Stroke::new_outer_stroke(width, style, None, None, None, None),
|
||||||
|
StrokeKind::Center => Stroke::new_center_stroke(width, style, None, None, None, None),
|
||||||
|
};
|
||||||
|
stroke.fill = Fill::Solid(SolidColor(color));
|
||||||
|
stroke
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn render(pool: &ShapesPool, root: Uuid) -> String {
|
pub(super) fn render(pool: &ShapesPool, root: Uuid) -> String {
|
||||||
let mut resources = RenderResources::try_new_headless().expect("headless resources");
|
let mut resources = RenderResources::try_new_headless().expect("headless resources");
|
||||||
register_test_font_urls(&mut resources.fonts);
|
register_test_font_urls(&mut resources.fonts);
|
||||||
|
|||||||
@ -45,7 +45,14 @@ pub(super) fn render_frame(
|
|||||||
render_tree(builder, shared, child_id, tree, scale)?;
|
render_tree(builder, shared, child_id, tree, scale)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strokes over children (frame space).
|
// Close content clip before strokes. Outer (and half of center) strokes
|
||||||
|
// extend past the frame bounds; keeping them under clip-path hides them.
|
||||||
|
// Matches GPU: clipped-frame strokes render in exit without the frame clip.
|
||||||
|
if clipped {
|
||||||
|
builder.close_group();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strokes over children (frame space), outside the content clip.
|
||||||
let visible_strokes: Vec<&Stroke> = element.visible_strokes().collect();
|
let visible_strokes: Vec<&Stroke> = element.visible_strokes().collect();
|
||||||
if !visible_strokes.is_empty() {
|
if !visible_strokes.is_empty() {
|
||||||
let canvas = builder.canvas();
|
let canvas = builder.canvas();
|
||||||
@ -56,9 +63,6 @@ pub(super) fn render_frame(
|
|||||||
canvas.restore();
|
canvas.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
if clipped {
|
|
||||||
builder.close_group();
|
|
||||||
}
|
|
||||||
if effects.is_some() {
|
if effects.is_some() {
|
||||||
builder.close_group();
|
builder.close_group();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,7 +60,8 @@ fn svg_page_bounds(shape: &Shape, tree: ShapesPoolRef, scale: f32) -> skia::Rect
|
|||||||
/// `<clipPath>`.
|
/// `<clipPath>`.
|
||||||
///
|
///
|
||||||
/// Special-case re-emission for shadows, layer blur, masks, text strokes, and
|
/// Special-case re-emission for shadows, layer blur, masks, text strokes, and
|
||||||
/// deferred strokes is intentionally out of scope for this cut.
|
/// image strokes is intentionally out of scope for this cut. Solid Inner/Outer
|
||||||
|
/// and dotted/dashed strokes are emitted as filled outlines.
|
||||||
pub fn render_to_svg(
|
pub fn render_to_svg(
|
||||||
shared: &mut RenderResources,
|
shared: &mut RenderResources,
|
||||||
id: &Uuid,
|
id: &Uuid,
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="164" height="124" viewBox="0 0 164 124"><defs><clipPath id="clip0" clipPathUnits="userSpaceOnUse">
|
||||||
|
<rect transform="translate(12 12)" width="140" height="100"/>
|
||||||
|
</clipPath></defs><g clip-path="url(#clip0)">
|
||||||
|
<rect fill="#EEE" transform="translate(12 12)" width="140" height="100"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#1040FF" transform="translate(12 12)" d="M-12 -12L152 -12L152 112L-12 112L-12 -12ZM140 0L0 0L0 100L140 100L140 0Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="80" viewBox="0 0 100 80">
|
||||||
|
<path fill="green" d="M18 0L0 0L0 8L18 8L18 0ZM0 36L0 18L8 18L8 36L0 36ZM0 72L0 54L8 54L8 72L0 72ZM28 80L10 80L10 72L28 72L28 80ZM64 80L45.999996 80L45.999996 72L64 72L64 80ZM100 80L100 72L82 72L82 80L100 80ZM100 44L100 62L92 62L92 44L100 44ZM100 8L100 26L92 26L92 8L100 8ZM72 0L90 0L90 8L72 8L72 0ZM36 0L54.000004 0L54.000004 8L36 8L36 0Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="96" viewBox="0 0 116 96">
|
||||||
|
<path fill="green" transform="translate(8 8)" d="M0 4C2.6666667 4 4 2.6666667 4 0C4 -2.6666667 2.6666667 -4 0 -4C-2.6666667 -4 -4 -2.6666667 -4 0C-4 2.6666667 -2.6666667 4 0 4ZM13 4C15.666667 4 17 2.6666667 17 0C17 -2.6666667 15.666667 -4 13 -4C10.333333 -4 9 -2.6666667 9 0C9 2.6666667 10.333333 4 13 4ZM26 4C28.666666 4 30 2.6666667 30 0C30 -2.6666667 28.666666 -4 26 -4C23.333334 -4 22 -2.6666667 22 0C22 2.6666667 23.333334 4 26 4ZM39 4C41.666668 4 43 2.6666667 43 0C43 -2.6666667 41.666668 -4 39 -4C36.333332 -4 35 -2.6666667 35 0C35 2.6666667 36.333332 4 39 4ZM52 4C54.666668 4 56 2.6666667 56 0C56 -2.6666667 54.666668 -4 52 -4C49.333332 -4 48 -2.6666667 48 0C48 2.6666667 49.333332 4 52 4ZM65 4C67.666664 4 69 2.6666667 69 0C69 -2.6666667 67.666664 -4 65 -4C62.333332 -4 61 -2.6666667 61 0C61 2.6666667 62.333332 4 65 4ZM78 4C80.666664 4 82 2.6666667 82 0C82 -2.6666667 80.666664 -4 78 -4C75.333336 -4 74 -2.6666667 74 0C74 2.6666667 75.333336 4 78 4ZM91 4C93.666664 4 95 2.6666667 95 0C95 -2.6666667 93.666664 -4 91 -4C88.333336 -4 87 -2.6666667 87 0C87 2.6666667 88.333336 4 91 4ZM100 8C102.66666 8 104 6.666667 104 4C104 1.3333333 102.66666 0 100 0C97.333336 0 96 1.3333333 96 4C96 6.666667 97.333336 8 100 8ZM0 13C2.6666667 13 4 11.666667 4 9C4 6.333333 2.6666667 5 0 5C-2.6666667 5 -4 6.333333 -4 9C-4 11.666667 -2.6666667 13 0 13ZM100 21C102.66666 21 104 19.666666 104 17C104 14.333333 102.66666 13 100 13C97.333336 13 96 14.333333 96 17C96 19.666666 97.333336 21 100 21ZM0 26C2.6666667 26 4 24.666666 4 22C4 19.333334 2.6666667 18 0 18C-2.6666667 18 -4 19.333334 -4 22C-4 24.666666 -2.6666667 26 0 26ZM100 34C102.66666 34 104 32.666668 104 30C104 27.333334 102.66666 26 100 26C97.333336 26 96 27.333334 96 30C96 32.666668 97.333336 34 100 34ZM0 39C2.6666667 39 4 37.666668 4 35C4 32.333332 2.6666667 31 0 31C-2.6666667 31 -4 32.333332 -4 35C-4 37.666668 -2.6666667 39 0 39ZM100 47C102.66666 47 104 45.666668 104 43C104 40.333332 102.66666 39 100 39C97.333336 39 96 40.333332 96 43C96 45.666668 97.333336 47 100 47ZM0 52C2.6666667 52 4 50.666668 4 48C4 45.333332 2.6666667 44 0 44C-2.6666667 44 -4 45.333332 -4 48C-4 50.666668 -2.6666667 52 0 52ZM100 60C102.66666 60 104 58.666668 104 56C104 53.333332 102.66666 52 100 52C97.333336 52 96 53.333332 96 56C96 58.666668 97.333336 60 100 60ZM0 65C2.6666667 65 4 63.666668 4 61C4 58.333332 2.6666667 57 0 57C-2.6666667 57 -4 58.333332 -4 61C-4 63.666668 -2.6666667 65 0 65ZM100 73C102.66666 73 104 71.666664 104 69C104 66.333336 102.66666 65 100 65C97.333336 65 96 66.333336 96 69C96 71.666664 97.333336 73 100 73ZM0 78C2.6666667 78 4 76.666664 4 74C4 71.333336 2.6666667 70 0 70C-2.6666667 70 -4 71.333336 -4 74C-4 76.666664 -2.6666667 78 0 78ZM7 84C9.666667 84 11 82.666664 11 80C11 77.333336 9.666667 76 7 76C4.333333 76 3 77.333336 3 80C3 82.666664 4.333333 84 7 84ZM20 84C22.666666 84 24 82.666664 24 80C24 77.333336 22.666666 76 20 76C17.333334 76 16 77.333336 16 80C16 82.666664 17.333334 84 20 84ZM33 84C35.666668 84 37 82.666664 37 80C37 77.333336 35.666668 76 33 76C30.333334 76 29 77.333336 29 80C29 82.666664 30.333334 84 33 84ZM45.999996 84C48.666664 84 49.999996 82.666664 49.999996 80C49.999996 77.333336 48.666664 76 45.999996 76C43.333328 76 41.999996 77.333336 41.999996 80C41.999996 82.666664 43.333328 84 45.999996 84ZM59 84C61.666668 84 63 82.666664 63 80C63 77.333336 61.666668 76 59 76C56.333332 76 55 77.333336 55 80C55 82.666664 56.333332 84 59 84ZM72 84C74.666664 84 76 82.666664 76 80C76 77.333336 74.666664 76 72 76C69.333336 76 68 77.333336 68 80C68 82.666664 69.333336 84 72 84ZM85 84C87.666664 84 89 82.666664 89 80C89 77.333336 87.666664 76 85 76C82.333336 76 81 77.333336 81 80C81 82.666664 82.333336 84 85 84ZM98 84C100.66666 84 102 82.666664 102 80C102 77.333336 100.66666 76 98 76C95.333336 76 94 77.333336 94 80C94 82.666664 95.333336 84 98 84Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="80" viewBox="0 0 100 80">
|
||||||
|
<path fill="blue" d="M100 0L0 0L0 80L100 80L100 0ZM8 9C8 7.7741446 7.7323785 6.6098022 7.197135 5.5069714C8.7744055 7.1689906 10.708694 8 13 8C15.737486 8 17.904152 6.8878965 19.5 4.6636891C21.095848 6.8878965 23.262514 8 26 8C28.737486 8 30.904152 6.8878965 32.5 4.6636891C34.095848 6.8878965 36.262516 8 39 8C41.737484 8 43.904152 6.8878965 45.5 4.6636891C47.095848 6.8878965 49.262516 8 52 8C54.737484 8 56.904152 6.8878965 58.5 4.6636891C60.095848 6.8878965 62.262516 8 65 8C67.737488 8 69.904152 6.8878965 71.5 4.6636891C73.095848 6.8878965 75.262512 8 78 8C80.737488 8 82.904152 6.8878965 84.5 4.6636891C86.095848 6.8878965 88.262512 8 91 8C91.656258 8 92.302719 7.9204535 92.939392 7.7613606C93.525131 8.8608704 94.324104 9.7737494 95.336311 10.5C93.112106 12.095847 92 14.262514 92 17C92 19.737486 93.112106 21.904152 95.336311 23.5C93.112106 25.095848 92 27.262514 92 30C92 32.737484 93.112106 34.904152 95.336311 36.5C93.112106 38.095848 92 40.262516 92 43C92 45.737484 93.112106 47.904152 95.336311 49.5C93.112106 51.095848 92 53.262516 92 56C92 58.737484 93.112106 60.904152 95.336311 62.5C93.112106 64.095848 92 66.262512 92 69C92 70.631767 92.456512 72.123863 93.369545 73.47628C92.642754 73.992149 92.019569 74.61216 91.5 75.336311C89.904152 73.112106 87.737488 72 85 72C82.262512 72 80.095848 73.112106 78.5 75.336311C76.904152 73.112106 74.737488 72 72 72C69.262512 72 67.095848 73.112106 65.5 75.336311C63.904152 73.112106 61.737484 72 59 72C56.262512 72 54.095844 73.112106 52.5 75.336311C50.904152 73.112106 48.737484 72 45.999996 72C43.262512 72 41.095844 73.112106 39.5 75.336311C37.904152 73.112106 35.737484 72 33 72C30.262514 72 28.095848 73.112106 26.5 75.336311C24.904152 73.112106 22.737486 72 20 72C17.262514 72 15.095847 73.112106 13.5 75.336311C12.085551 73.364929 10.170585 72.264732 7.7551012 72.035721C7.276824 70.147453 6.2463531 68.635551 4.6636891 67.5C6.8878965 65.904152 8 63.737488 8 61C8 58.262516 6.8878965 56.095848 4.6636891 54.5C6.8878965 52.904152 8 50.737484 8 48C8 45.262516 6.8878965 43.095848 4.6636891 41.5C6.8878965 39.904152 8 37.737484 8 35C8 32.262516 6.8878965 30.095848 4.6636891 28.5C6.8878965 26.904152 8 24.737486 8 22C8 19.262514 6.8878965 17.095848 4.6636891 15.5C6.8878965 13.904153 8 11.737486 8 9Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="132" height="112" viewBox="0 0 132 112">
|
||||||
|
<path fill="green" transform="translate(16 16)" d="M-8 0C-8 1.6422184 -7.538126 3.1422181 -6.6143785 4.5C-7.538126 5.8577819 -8 7.3577814 -8 9C-8 11.737486 -6.8878965 13.904153 -4.6636891 15.5C-6.8878965 17.095848 -8 19.262514 -8 22C-8 24.737486 -6.8878965 26.904152 -4.6636891 28.5C-6.8878965 30.095848 -8 32.262516 -8 35C-8 37.737484 -6.8878965 39.904152 -4.6636891 41.5C-6.8878965 43.095848 -8 45.262516 -8 48C-8 50.737484 -6.8878965 52.904152 -4.6636891 54.5C-6.8878965 56.095848 -8 58.262516 -8 61C-8 63.737488 -6.8878965 65.904152 -4.6636891 67.5C-6.8878965 69.095848 -8 71.262512 -8 74C-8 78.851593 -5.5850339 81.506355 -0.75510108 81.964279C0.26408672 85.988091 2.8491201 88 7 88C9.7374859 88 11.904153 86.887894 13.5 84.663689C15.095847 86.887894 17.262514 88 20 88C22.737486 88 24.904152 86.887894 26.5 84.663689C28.095848 86.887894 30.262514 88 33 88C35.737484 88 37.904152 86.887894 39.499996 84.663689C41.095844 86.887894 43.262512 88 45.999996 88C48.737484 88 50.904152 86.887894 52.499996 84.663689C54.095844 86.887894 56.262512 88 59 88C61.737484 88 63.904152 86.887894 65.5 84.663689C67.095848 86.887894 69.262512 88 72 88C74.737488 88 76.904152 86.887894 78.5 84.663689C80.095848 86.887894 82.262512 88 85 88C87.737488 88 89.904152 86.887894 91.5 84.663689C93.095848 86.887894 95.262512 88 98 88C103.33334 88 106 85.333336 106 80C106 78.368233 105.54349 76.876137 104.63046 75.52372C106.87682 73.929276 108 71.754707 108 69C108 66.262512 106.88789 64.095848 104.66369 62.5C106.88789 60.904152 108 58.737484 108 56C108 53.262516 106.88789 51.095848 104.66369 49.5C106.88789 47.904152 108 45.737484 108 43C108 40.262516 106.88789 38.095848 104.66369 36.5C106.88789 34.904152 108 32.737484 108 30C108 27.262514 106.88789 25.095848 104.66369 23.5C106.88789 21.904152 108 19.737486 108 17C108 14.262514 106.88789 12.095847 104.66369 10.5C106.88789 8.9041529 108 6.7374859 108 4C108 -1.3333335 105.33334 -4 100 -4C99.343742 -4 98.697281 -3.9204535 98.060608 -3.7613606C96.55526 -6.5871201 94.201721 -8 91 -8C88.262512 -8 86.095848 -6.8878965 84.5 -4.6636891C82.904152 -6.8878965 80.737488 -8 78 -8C75.262512 -8 73.095848 -6.8878965 71.5 -4.6636891C69.904152 -6.8878965 67.737488 -8 65 -8C62.262516 -8 60.095848 -6.8878965 58.5 -4.6636891C56.904152 -6.8878965 54.737484 -8 52 -8C49.262516 -8 47.095848 -6.8878965 45.5 -4.6636891C43.904152 -6.8878965 41.737484 -8 39 -8C36.262516 -8 34.095848 -6.8878965 32.5 -4.6636891C30.904152 -6.8878965 28.737486 -8 26 -8C23.262514 -8 21.095848 -6.8878965 19.5 -4.6636891C17.904152 -6.8878965 15.737486 -8 13 -8C10.262514 -8 8.0958471 -6.8878965 6.5 -4.6636891C4.9041519 -6.8878965 2.7374856 -8 0 -8C-5.3333335 -8 -8 -5.3333335 -8 0ZM100 0L0 0L0 80L100 80L100 0Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="132" height="112" viewBox="0 0 132 112">
|
||||||
|
<path fill="red" transform="translate(16 16)" d="M0 -8L13 -8L13 0L0 0L0 -8ZM35 0L26 0L26 -8L35 -8L35 0ZM61 0L48 0L48 -8L61 -8L61 0ZM83 0L74 0L74 -8L83 -8L83 0ZM100 31L100 22L108 22L108 31L100 31ZM100 57L100 44L108 44L108 57L100 57ZM100 79L100 70L108 70L108 79L100 79ZM75 80L88 80L88 88L75 88L75 80ZM53 80L62 80L62 88L53 88L53 80ZM27 80L39.999996 80L39.999996 88L27 88L27 80ZM0 37L0 46L-8 46L-8 37L0 37ZM0 11L0 24L-8 24L-8 11L0 11ZM0 59L0 72L-8 72L-8 59L0 59ZM5 80L14 80L14 88L5 88L5 80ZM100 9L100 0L96 0L96 -8L108 -8L108 9L100 9Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="96" viewBox="0 0 116 96">
|
||||||
|
<path fill="none" stroke="green" stroke-width="8" stroke-miterlimit="4" transform="translate(8 8)" d="M0 0L100 0L100 80L0 80L0 0Z"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="80" viewBox="0 0 100 80">
|
||||||
|
<path fill="blue" d="M100 0L0 0L0 80L100 80L100 0ZM8 8L8 72L92 72L92 8L8 8Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="132" height="112" viewBox="0 0 132 112">
|
||||||
|
<path fill="green" transform="translate(16 16)" d="M-8 -8L108 -8L108 88L-8 88L-8 -8ZM100 0L0 0L0 80L100 80L100 0Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="96" viewBox="0 0 116 96">
|
||||||
|
<path fill="red" transform="translate(8 8)" d="M0 4C2.6666667 4 4 2.6666667 4 0C4 -2.6666667 2.6666667 -4 0 -4C-2.6666667 -4 -4 -2.6666667 -4 0C-4 2.6666667 -2.6666667 4 0 4ZM13 4C15.666667 4 17 2.6666667 17 0C17 -2.6666667 15.666667 -4 13 -4C10.333333 -4 9 -2.6666667 9 0C9 2.6666667 10.333333 4 13 4ZM26 4C28.666666 4 30 2.6666667 30 0C30 -2.6666667 28.666666 -4 26 -4C23.333334 -4 22 -2.6666667 22 0C22 2.6666667 23.333334 4 26 4ZM39 4C41.666668 4 43 2.6666667 43 0C43 -2.6666667 41.666668 -4 39 -4C36.333332 -4 35 -2.6666667 35 0C35 2.6666667 36.333332 4 39 4ZM52 4C54.666668 4 56 2.6666667 56 0C56 -2.6666667 54.666668 -4 52 -4C49.333332 -4 48 -2.6666667 48 0C48 2.6666667 49.333332 4 52 4ZM65 4C67.666664 4 69 2.6666667 69 0C69 -2.6666667 67.666664 -4 65 -4C62.333332 -4 61 -2.6666667 61 0C61 2.6666667 62.333332 4 65 4ZM78 4C80.666664 4 82 2.6666667 82 0C82 -2.6666667 80.666664 -4 78 -4C75.333336 -4 74 -2.6666667 74 0C74 2.6666667 75.333336 4 78 4ZM91 4C93.666664 4 95 2.6666667 95 0C95 -2.6666667 93.666664 -4 91 -4C88.333336 -4 87 -2.6666667 87 0C87 2.6666667 88.333336 4 91 4ZM100 8C102.66666 8 104 6.666667 104 4C104 1.3333333 102.66666 0 100 0C97.333336 0 96 1.3333333 96 4C96 6.666667 97.333336 8 100 8ZM100 21C102.66666 21 104 19.666666 104 17C104 14.333333 102.66666 13 100 13C97.333336 13 96 14.333333 96 17C96 19.666666 97.333336 21 100 21ZM100 34C102.66666 34 104 32.666668 104 30C104 27.333334 102.66666 26 100 26C97.333336 26 96 27.333334 96 30C96 32.666668 97.333336 34 100 34ZM100 47C102.66666 47 104 45.666668 104 43C104 40.333332 102.66666 39 100 39C97.333336 39 96 40.333332 96 43C96 45.666668 97.333336 47 100 47ZM100 60C102.66666 60 104 58.666668 104 56C104 53.333332 102.66666 52 100 52C97.333336 52 96 53.333332 96 56C96 58.666668 97.333336 60 100 60ZM100 73C102.66666 73 104 71.666664 104 69C104 66.333336 102.66666 65 100 65C97.333336 65 96 66.333336 96 69C96 71.666664 97.333336 73 100 73ZM7 84C9.666667 84 11 82.666664 11 80C11 77.333336 9.666667 76 7 76C4.333333 76 3 77.333336 3 80C3 82.666664 4.333333 84 7 84ZM20 84C22.666666 84 24 82.666664 24 80C24 77.333336 22.666666 76 20 76C17.333334 76 16 77.333336 16 80C16 82.666664 17.333334 84 20 84ZM33 84C35.666668 84 37 82.666664 37 80C37 77.333336 35.666668 76 33 76C30.333334 76 29 77.333336 29 80C29 82.666664 30.333334 84 33 84ZM45.999996 84C48.666664 84 49.999996 82.666664 49.999996 80C49.999996 77.333336 48.666664 76 45.999996 76C43.333328 76 41.999996 77.333336 41.999996 80C41.999996 82.666664 43.333328 84 45.999996 84ZM59 84C61.666668 84 63 82.666664 63 80C63 77.333336 61.666668 76 59 76C56.333332 76 55 77.333336 55 80C55 82.666664 56.333332 84 59 84ZM72 84C74.666664 84 76 82.666664 76 80C76 77.333336 74.666664 76 72 76C69.333336 76 68 77.333336 68 80C68 82.666664 69.333336 84 72 84ZM85 84C87.666664 84 89 82.666664 89 80C89 77.333336 87.666664 76 85 76C82.333336 76 81 77.333336 81 80C81 82.666664 82.333336 84 85 84ZM98 84C100.66666 84 102 82.666664 102 80C102 77.333336 100.66666 76 98 76C95.333336 76 94 77.333336 94 80C94 82.666664 95.333336 84 98 84Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="96" viewBox="0 0 116 96">
|
||||||
|
<path fill="none" stroke="red" stroke-width="8" stroke-miterlimit="4" transform="translate(8 8)" d="M0 0L100 0L100 80L0 80"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="96" viewBox="0 0 116 96">
|
||||||
|
<path fill="none" stroke="blue" stroke-width="8" stroke-miterlimit="4" transform="translate(8 8)" d="M0 0L100 0L100 80L0 80"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="96" viewBox="0 0 116 96">
|
||||||
|
<path fill="none" stroke="green" stroke-width="8" stroke-miterlimit="4" transform="translate(8 8)" d="M0 0L100 0L100 80L0 80"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="108" height="88" viewBox="0 0 108 88">
|
||||||
|
<path fill="blue" transform="translate(-6 -6)" d="M28 6L10 6L10 14L28 14L28 6ZM64 6L46 6L46 14L64 14L64 6ZM100 6L82 6L82 14L100 14L100 6ZM114 36L114 18L106 18L106 36L114 36ZM6 28L6 46L14 46L14 28L6 28ZM114 72L114 54L106 54L106 72L114 72ZM6 64L6 82L14 82L14 64L6 64ZM20 94L38 94L38 86L20 86L20 94ZM55.999996 94L74 94L74 86L55.999996 86L55.999996 94ZM92 94L110 94L110 86L92 86L92 94Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="120" height="100" viewBox="0 0 120 100">
|
||||||
|
<path fill="red" transform="translate(-10 -10)" d="M20 10L40 10L40 20L20 20L20 10ZM80 20L60 20L60 10L80 10L80 20ZM100 20L100 10L120 10L120 20L100 20ZM120 60L120 40L130 40L130 60L120 60ZM80 100L100 100L100 110L80 110L80 100ZM40 100L59.999996 100L59.999996 110L40 110L40 100ZM20 40L20 60L10 60L10 40L20 40ZM20 80L20 100L10 100L10 80L20 80ZM120 100L120 80L130 80L130 100L120 100Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="108" height="88" viewBox="0 0 108 88">
|
||||||
|
<path fill="blue" transform="translate(-6 -6)" d="M10 14C12.666667 14 14 12.666667 14 10C14 7.333333 12.666667 6 10 6C7.333333 6 6 7.333333 6 10C6 12.666667 7.333333 14 10 14ZM23 14C25.666666 14 27 12.666667 27 10C27 7.333333 25.666666 6 23 6C20.333334 6 19 7.333333 19 10C19 12.666667 20.333334 14 23 14ZM36 14C38.666668 14 40 12.666667 40 10C40 7.333333 38.666668 6 36 6C33.333332 6 32 7.333333 32 10C32 12.666667 33.333332 14 36 14ZM49 14C51.666668 14 53 12.666667 53 10C53 7.333333 51.666668 6 49 6C46.333332 6 45 7.333333 45 10C45 12.666667 46.333332 14 49 14ZM62 14C64.666664 14 66 12.666667 66 10C66 7.333333 64.666664 6 62 6C59.333332 6 58 7.333333 58 10C58 12.666667 59.333332 14 62 14ZM75 14C77.666664 14 79 12.666667 79 10C79 7.333333 77.666664 6 75 6C72.333336 6 71 7.333333 71 10C71 12.666667 72.333336 14 75 14ZM88 14C90.666664 14 92 12.666667 92 10C92 7.333333 90.666664 6 88 6C85.333336 6 84 7.333333 84 10C84 12.666667 85.333336 14 88 14ZM101 14C103.66666 14 105 12.666667 105 10C105 7.333333 103.66666 6 101 6C98.333336 6 97 7.333333 97 10C97 12.666667 98.333336 14 101 14ZM110 18C112.66666 18 114 16.666666 114 14C114 11.333333 112.66666 10 110 10C107.33334 10 106 11.333333 106 14C106 16.666666 107.33334 18 110 18ZM10 23C12.666667 23 14 21.666666 14 19C14 16.333334 12.666667 15 10 15C7.333333 15 6 16.333334 6 19C6 21.666666 7.333333 23 10 23ZM110 31C112.66666 31 114 29.666666 114 27C114 24.333334 112.66666 23 110 23C107.33334 23 106 24.333334 106 27C106 29.666666 107.33334 31 110 31ZM10 36C12.666667 36 14 34.666668 14 32C14 29.333334 12.666667 28 10 28C7.333333 28 6 29.333334 6 32C6 34.666668 7.333333 36 10 36ZM110 44C112.66666 44 114 42.666668 114 40C114 37.333332 112.66666 36 110 36C107.33334 36 106 37.333332 106 40C106 42.666668 107.33334 44 110 44ZM10 49C12.666667 49 14 47.666668 14 45C14 42.333332 12.666667 41 10 41C7.333333 41 6 42.333332 6 45C6 47.666668 7.333333 49 10 49ZM110 57C112.66666 57 114 55.666668 114 53C114 50.333332 112.66666 49 110 49C107.33334 49 106 50.333332 106 53C106 55.666668 107.33334 57 110 57ZM10 62C12.666667 62 14 60.666668 14 58C14 55.333332 12.666667 54 10 54C7.333333 54 6 55.333332 6 58C6 60.666668 7.333333 62 10 62ZM110 70C112.66666 70 114 68.666664 114 66C114 63.333332 112.66666 62 110 62C107.33334 62 106 63.333332 106 66C106 68.666664 107.33334 70 110 70ZM10 75C12.666667 75 14 73.666664 14 71C14 68.333336 12.666667 67 10 67C7.333333 67 6 68.333336 6 71C6 73.666664 7.333333 75 10 75ZM110 83C112.66666 83 114 81.666664 114 79C114 76.333336 112.66666 75 110 75C107.33334 75 106 76.333336 106 79C106 81.666664 107.33334 83 110 83ZM10 88C12.666667 88 14 86.666664 14 84C14 81.333336 12.666667 80 10 80C7.333333 80 6 81.333336 6 84C6 86.666664 7.333333 88 10 88ZM17 94C19.666666 94 21 92.666664 21 90C21 87.333336 19.666666 86 17 86C14.333333 86 13 87.333336 13 90C13 92.666664 14.333333 94 17 94ZM30 94C32.666668 94 34 92.666664 34 90C34 87.333336 32.666668 86 30 86C27.333334 86 26 87.333336 26 90C26 92.666664 27.333334 94 30 94ZM43 94C45.666668 94 47 92.666664 47 90C47 87.333336 45.666668 86 43 86C40.333332 86 39 87.333336 39 90C39 92.666664 40.333332 94 43 94ZM55.999996 94C58.666664 94 59.999996 92.666664 59.999996 90C59.999996 87.333336 58.666664 86 55.999996 86C53.333328 86 51.999996 87.333336 51.999996 90C51.999996 92.666664 53.333328 94 55.999996 94ZM69 94C71.666664 94 73 92.666664 73 90C73 87.333336 71.666664 86 69 86C66.333336 86 65 87.333336 65 90C65 92.666664 66.333336 94 69 94ZM82 94C84.666664 94 86 92.666664 86 90C86 87.333336 84.666664 86 82 86C79.333336 86 78 87.333336 78 90C78 92.666664 79.333336 94 82 94ZM95 94C97.666664 94 99 92.666664 99 90C99 87.333336 97.666664 86 95 86C92.333336 86 91 87.333336 91 90C91 92.666664 92.333336 94 95 94ZM108 94C110.66666 94 112 92.666664 112 90C112 87.333336 110.66666 86 108 86C105.33334 86 104 87.333336 104 90C104 92.666664 105.33334 94 108 94Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="80" viewBox="0 0 100 80">
|
||||||
|
<path fill="blue" transform="translate(-10 -10)" d="M110 10L10 10L10 90L110 90L110 10ZM16.614376 17.500002C16.928108 17.223316 17.223316 16.928108 17.500002 16.614376C19.490559 18.871458 21.990557 20 25 20C28.009443 20 30.509443 18.871458 32.5 16.614376C34.490559 18.871458 36.990559 20 40 20C43.009441 20 45.509441 18.871458 47.5 16.614376C49.490559 18.871458 51.990559 20 55 20C58.009441 20 60.509441 18.871458 62.5 16.614376C64.490562 18.871458 66.990562 20 70 20C73.009438 20 75.509438 18.871458 77.5 16.614376C79.490562 18.871458 81.990562 20 85 20C88.009438 20 90.509438 18.871458 92.5 16.614376C94.490562 18.871458 96.990562 20 100 20C100.43244 20 100.86308 19.972065 101.2919 19.916197C101.84457 20.895153 102.54248 21.75642 103.38562 22.499998C101.12854 24.490557 100 26.990557 100 30C100 33.009441 101.12854 35.509441 103.38562 37.5C101.12854 39.490559 100 41.990559 100 45C100 48.009441 101.12854 50.509441 103.38562 52.5C101.12854 54.490559 100 56.990559 100 60C100 63.009441 101.12854 65.509438 103.38562 67.5C101.12854 69.490562 100 71.990562 100 75C100 78.009438 101.12854 80.509438 103.38562 82.5C103.07189 82.77668 102.77668 83.071892 102.5 83.38562C100.50944 81.12854 98.009438 80 95 80C91.990562 80 89.490562 81.12854 87.5 83.38562C85.509438 81.12854 83.009438 80 80 80C76.990562 80 74.490562 81.12854 72.5 83.38562C70.509438 81.12854 68.009438 80 65 80C61.990559 80 59.490559 81.12854 57.5 83.385628C55.509441 81.12854 53.009438 80 49.999996 80C46.990555 80 44.490559 81.12854 42.5 83.38562C40.509441 81.12854 38.009441 80 35 80C31.990557 80 29.490559 81.12854 27.500002 83.38562C25.509443 81.12854 23.009443 80 20 80C19.567553 80 19.136921 80.027931 18.708101 80.083801C18.155426 79.104851 17.457518 78.243584 16.614376 77.5C18.871458 75.509438 20 73.009438 20 70C20 66.990562 18.871458 64.490562 16.614376 62.5C18.871458 60.509441 20 58.009441 20 55C20 51.990559 18.871458 49.490559 16.614376 47.5C18.871458 45.509441 20 43.009441 20 40C20 36.990559 18.871458 34.490559 16.614376 32.5C18.871458 30.509443 20 28.009443 20 25C20 21.990557 18.871458 19.490559 16.614376 17.500002Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="120" height="100" viewBox="0 0 120 100">
|
||||||
|
<path fill="red" transform="translate(-10 -10)" d="M50 10C46.990559 10 44.490559 11.128541 42.5 13.385624C40.509441 11.128541 38.009441 10 35 10C31.990557 10 29.490559 11.128541 27.500002 13.385624C25.509443 11.128541 23.009443 10 20 10C13.333333 10 10 13.333333 10 20C10 23.009443 11.128541 25.509441 13.385624 27.499998C11.128541 29.490557 10 31.990557 10 35C10 38.009441 11.128541 40.509441 13.385624 42.5C11.128541 44.490559 10 46.990559 10 50C10 53.009441 11.128541 55.509441 13.385624 57.5C11.128541 59.490559 10 61.990559 10 65C10 68.009438 11.128541 70.509438 13.385624 72.5C11.128541 74.490562 10 76.990562 10 80C10 83.009438 11.128541 85.509438 13.385624 87.5C11.128541 89.490562 10 91.990562 10 95C10 101.66666 13.333333 105 20 105C20.432447 105 20.863079 104.97207 21.291899 104.9162C23.205288 108.3054 26.107988 110 30 110C33.009441 110 35.509441 108.87146 37.5 106.61438C39.490559 108.87146 41.990559 110 45 110C48.009441 110 50.509438 108.87146 52.499996 106.61438C54.490555 108.87146 56.990555 110 59.999996 110C63.009438 110 65.509438 108.87146 67.5 106.61437C69.490555 108.87146 71.990555 110 75 110C78.009438 110 80.509438 108.87146 82.5 106.61438C84.490562 108.87146 86.990562 110 90 110C93.009438 110 95.509438 108.87146 97.5 106.61438C99.490562 108.87146 101.99056 110 105 110C108.00944 110 110.50944 108.87146 112.5 106.61438C114.49056 108.87146 116.99056 110 120 110C126.66666 110 130 106.66666 130 100C130 96.990562 128.87146 94.490562 126.61438 92.5C128.87146 90.509438 130 88.009438 130 85C130 81.990562 128.87146 79.490562 126.61438 77.5C128.87146 75.509438 130 73.009438 130 70C130 66.990562 128.87146 64.490562 126.61438 62.5C128.87146 60.509441 130 58.009441 130 55C130 51.990559 128.87146 49.490559 126.61438 47.5C128.87146 45.509441 130 43.009441 130 40C130 36.990559 128.87146 34.490559 126.61438 32.5C128.87146 30.509443 130 28.009443 130 25C130 18.333332 126.66666 15 120 15C119.56756 15 119.13692 15.027935 118.7081 15.083804C116.79472 11.694601 113.89201 10 110 10C106.99056 10 104.49056 11.128541 102.5 13.385624C100.50944 11.128541 98.009438 10 95 10C91.990562 10 89.490562 11.128541 87.5 13.385624C85.509438 11.128541 83.009438 10 80 10C76.990562 10 74.490562 11.128541 72.5 13.385624C70.509438 11.128541 68.009438 10 65 10C61.990559 10 59.490559 11.128541 57.5 13.385624C55.509441 11.128541 53.009441 10 50 10ZM120 20L20 20L20 100L120 100L120 20Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="108" height="88" viewBox="0 0 108 88">
|
||||||
|
<path fill="blue" transform="translate(-6 -6)" d="M23 6L10 6L10 14L23 14L23 6ZM45 6L36 6L36 14L45 14L45 6ZM71 6L58 6L58 14L71 14L71 6ZM93 6L84 6L84 14L93 14L93 6ZM114 6L106 6L106 19L114 19L114 6ZM6 21L6 34L14 34L14 21L6 21ZM114 41L114 32L106 32L106 41L114 41ZM6 47L6 56L14 56L14 47L6 47ZM114 67L114 54L106 54L106 67L114 67ZM6 69L6 82L14 82L14 69L6 69ZM114 89L114 80L106 80L106 89L114 89ZM15 94L24 94L24 86L15 86L15 94ZM37 94L49.999996 94L49.999996 86L37 86L37 94ZM63 94L72 94L72 86L63 86L63 94ZM85 94L98 94L98 86L85 86L85 94Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="140" height="100" viewBox="0 0 140 100">
|
||||||
|
<rect fill="#FFD400" width="140" height="100"/>
|
||||||
|
<path fill="#1040FF" d="M0 0L140 0L140 100L0 100L0 0ZM40 4L128 4L128 76L40 76L40 4Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="108" height="88" viewBox="0 0 108 88">
|
||||||
|
<rect fill="none" stroke="blue" stroke-width="8" stroke-miterlimit="4" transform="translate(-6 -6)" x="10" y="10" width="100" height="80"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="80" viewBox="0 0 100 80">
|
||||||
|
<path fill="blue" transform="translate(-10 -10)" d="M110 10L10 10L10 90L110 90L110 10ZM20 20L20 80L100 80L100 20L20 20Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="120" height="100" viewBox="0 0 120 100">
|
||||||
|
<path fill="red" transform="translate(-10 -10)" d="M10 10L130 10L130 110L10 110L10 10ZM120 20L20 20L20 100L120 100L120 20Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
assertion_line: 454
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="171.24356" height="156.60254" viewBox="0 0 171.24356 156.60254">
|
||||||
|
<path fill="#FFD400" transform="matrix(0.866025 0.5 -0.5 0.866025 50 0)" d="M0 0L140 0L140 100L-9.5367432e-07 100L0 0Z"/>
|
||||||
|
<path fill="#1040FF" transform="matrix(0.866025 0.5 -0.5 0.866025 50 0)" d="M140 0L0 0L0 100L140 100L140 0ZM11.999999 88L12 12L128 12L128 88L11.999999 88Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
assertion_line: 367
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="204.02817" height="189.38715" viewBox="0 0 204.02817 189.38715">
|
||||||
|
<rect fill="#FFD400" transform="matrix(0.866025 0.5 -0.5 0.866025 66.3923 16.3923)" width="140" height="100"/>
|
||||||
|
<path fill="#1040FF" transform="matrix(0.866025 0.5 -0.5 0.866025 66.3923 16.3923)" d="M-12 -12L152 -12L152 112L-12 112L-12 -12ZM140 0L0 0L0 100L140 100L140 0Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
@ -1,6 +1,6 @@
|
|||||||
use super::fixtures::*;
|
use super::fixtures::*;
|
||||||
|
|
||||||
use crate::shapes::{BlendMode, Fill, SolidColor};
|
use crate::shapes::{BlendMode, Fill, SolidColor, StrokeCap, StrokeKind};
|
||||||
use crate::state::ShapesPool;
|
use crate::state::ShapesPool;
|
||||||
use crate::uuid::Uuid;
|
use crate::uuid::Uuid;
|
||||||
|
|
||||||
@ -208,6 +208,47 @@ fn exports_an_unclipped_frame_with_overflowing_child() {
|
|||||||
insta::assert_snapshot!(svg);
|
insta::assert_snapshot!(svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_clipped_frame_with_solid_outer_stroke() {
|
||||||
|
// Regression: frame content clip must not wrap strokes — outer strokes
|
||||||
|
// sit outside the selrect and would be fully clipped away.
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_frame(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 140.0, 100.0),
|
||||||
|
skia::Color::from_rgb(0xee, 0xee, 0xee),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let shape = pool.get_mut(&id).unwrap();
|
||||||
|
shape.add_stroke(solid_stroke(
|
||||||
|
StrokeKind::Outer,
|
||||||
|
12.0,
|
||||||
|
skia::Color::from_rgb(0x10, 0x40, 0xff),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("clip-path=\"url(#"),
|
||||||
|
"clipped frame must keep content clip: {svg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill-rule=\"evenodd\""),
|
||||||
|
"outer stroke must emit an evenodd outline: {svg}"
|
||||||
|
);
|
||||||
|
let stroke_pos = svg.find("fill-rule=\"evenodd\"").expect("stroke outline");
|
||||||
|
let clip_close = svg.find("</g>").expect("clip group close");
|
||||||
|
assert!(
|
||||||
|
stroke_pos > clip_close,
|
||||||
|
"outer stroke must be outside the content clip group: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn exports_text_with_multiple_solid_fills() {
|
fn exports_text_with_multiple_solid_fills() {
|
||||||
let mut pool = ShapesPool::new();
|
let mut pool = ShapesPool::new();
|
||||||
@ -240,6 +281,661 @@ fn exports_text_with_multiple_solid_fills() {
|
|||||||
insta::assert_snapshot!(svg);
|
insta::assert_snapshot!(svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_rect_with_solid_inner_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_rect(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(10.0, 10.0, 110.0, 90.0),
|
||||||
|
solid_stroke(StrokeKind::Inner, 10.0, skia::Color::from_rgb(0, 0, 255)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill=\"blue\"") || svg.to_ascii_lowercase().contains("fill=\"#0000ff\""),
|
||||||
|
"inner stroke must emit a filled outline: {svg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill-rule=\"evenodd\""),
|
||||||
|
"aligned stroke outline should use evenodd: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_rect_with_per_side_solid_inner_stroke() {
|
||||||
|
// Regression: solid Inner/Outer SVG expansion used stroke_to_path with a
|
||||||
|
// uniform width and ignored stroke.widths. Per-side must use the GPU
|
||||||
|
// evenodd band (top/right/bottom/left).
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
let mut stroke = solid_stroke(
|
||||||
|
StrokeKind::Inner,
|
||||||
|
20.0,
|
||||||
|
skia::Color::from_rgb(0x10, 0x40, 0xff),
|
||||||
|
);
|
||||||
|
stroke.widths = Some([4.0, 12.0, 24.0, 40.0]); // top, right, bottom, left
|
||||||
|
add_stroked_rect(&mut pool, id, Uuid::nil(), (0.0, 0.0, 140.0, 100.0), stroke);
|
||||||
|
{
|
||||||
|
let shape = pool.get_mut(&id).unwrap();
|
||||||
|
shape.set_fills(vec![Fill::Solid(SolidColor(skia::Color::from_rgb(
|
||||||
|
0xff, 0xd4, 0x00,
|
||||||
|
)))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill-rule=\"evenodd\""),
|
||||||
|
"per-side inner stroke must emit an evenodd band: {svg}"
|
||||||
|
);
|
||||||
|
// Inner hole for (0,0)-(140,100) with [4,12,24,40]: (40,4)-(128,76).
|
||||||
|
// Uniform width=20 would incorrectly hole at (20,20)-(120,80).
|
||||||
|
assert!(
|
||||||
|
svg.contains("40") && svg.contains("128") && svg.contains("76"),
|
||||||
|
"per-side hole must reflect left=40 / right=12 / bottom=24, got: {svg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!svg.contains("M20 20") && !svg.contains("L20 20"),
|
||||||
|
"must not use uniform width=20 inset: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_rect_with_solid_center_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_rect(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(10.0, 10.0, 110.0, 90.0),
|
||||||
|
solid_stroke(StrokeKind::Center, 8.0, skia::Color::from_rgb(0, 0, 255)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("stroke=\"blue\"") || svg.to_ascii_lowercase().contains("stroke=\"#0000ff\""),
|
||||||
|
"center stroke must keep a stroke attribute: {svg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!svg.contains("fill-rule=\"evenodd\""),
|
||||||
|
"center stroke must not expand to an evenodd outline: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_rect_with_solid_outer_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_rect(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(20.0, 20.0, 120.0, 100.0),
|
||||||
|
solid_stroke(StrokeKind::Outer, 10.0, skia::Color::from_rgb(255, 0, 0)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill=\"red\"") || svg.to_ascii_lowercase().contains("fill=\"#ff0000\""),
|
||||||
|
"outer stroke must emit a filled outline: {svg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill-rule=\"evenodd\""),
|
||||||
|
"aligned stroke outline should use evenodd: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_rotated_rect_with_solid_outer_stroke() {
|
||||||
|
// Regression: outline strokes must use local selrect geometry. Baking
|
||||||
|
// `centered_transform` into the path (via rect_segments) while the leaf
|
||||||
|
// canvas also concatenates it double-rotates the stroke.
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_rect(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 140.0, 100.0),
|
||||||
|
solid_stroke(
|
||||||
|
StrokeKind::Outer,
|
||||||
|
12.0,
|
||||||
|
skia::Color::from_rgb(0x10, 0x40, 0xff),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let shape = pool.get_mut(&id).unwrap();
|
||||||
|
// 30° rotation (cos≈0.866, sin=0.5), matching a workspace export case.
|
||||||
|
let c = 0.866_025_4_f32;
|
||||||
|
let s = 0.5_f32;
|
||||||
|
shape.set_transform(c, s, -s, c, 0.0, 0.0);
|
||||||
|
shape.set_rotation(30.0);
|
||||||
|
shape.set_fills(vec![Fill::Solid(SolidColor(skia::Color::from_rgb(
|
||||||
|
0xff, 0xd4, 0x00,
|
||||||
|
)))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill-rule=\"evenodd\""),
|
||||||
|
"rotated outer stroke must emit an evenodd outline: {svg}"
|
||||||
|
);
|
||||||
|
// Fill rect + stroke path should both carry the same leaf CTM (one rotation).
|
||||||
|
assert!(
|
||||||
|
svg.matches("matrix(0.866025").count() >= 2,
|
||||||
|
"fill and stroke must each use the rotation matrix once: {svg}"
|
||||||
|
);
|
||||||
|
// Outline path data must stay in local selrect space (roughly [-stroke, w+stroke]).
|
||||||
|
// Double rotation bakes world-space points into `d` before the CTM is applied.
|
||||||
|
let d_attr = svg
|
||||||
|
.split("d=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.unwrap_or("");
|
||||||
|
let first_num = d_attr
|
||||||
|
.trim_start_matches(|c: char| !c.is_ascii_digit() && c != '-' && c != '.')
|
||||||
|
.split(|c: char| !c.is_ascii_digit() && c != '-' && c != '.')
|
||||||
|
.find(|s| !s.is_empty())
|
||||||
|
.and_then(|s| s.parse::<f32>().ok());
|
||||||
|
assert!(
|
||||||
|
matches!(first_num, Some(n) if (-40.0..180.0).contains(&n)),
|
||||||
|
"stroke path d= should start in local coords, got {first_num:?} from {d_attr}: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_closed_path_with_solid_inner_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_closed_path(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 100.0, 80.0),
|
||||||
|
solid_stroke(StrokeKind::Inner, 8.0, skia::Color::from_rgb(0, 0, 255)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill=\"blue\"") || svg.to_ascii_lowercase().contains("fill=\"#0000ff\""),
|
||||||
|
"closed path inner stroke must emit a filled outline: {svg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill-rule=\"evenodd\""),
|
||||||
|
"aligned stroke outline should use evenodd: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_rotated_closed_path_with_solid_inner_stroke() {
|
||||||
|
// Regression: path content is stored in parent space; stroke outlines must
|
||||||
|
// apply `to_path_transform` (like fills via get_skia_path) before drawing
|
||||||
|
// under the leaf `centered_transform`, or the stroke double-rotates.
|
||||||
|
use crate::shapes::Type;
|
||||||
|
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_closed_path(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 140.0, 100.0),
|
||||||
|
solid_stroke(
|
||||||
|
StrokeKind::Inner,
|
||||||
|
12.0,
|
||||||
|
skia::Color::from_rgb(0x10, 0x40, 0xff),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let shape = pool.get_mut(&id).unwrap();
|
||||||
|
let c = 0.866_025_4_f32;
|
||||||
|
let s = 0.5_f32;
|
||||||
|
shape.set_transform(c, s, -s, c, 0.0, 0.0);
|
||||||
|
shape.set_rotation(30.0);
|
||||||
|
shape.set_fills(vec![Fill::Solid(SolidColor(skia::Color::from_rgb(
|
||||||
|
0xff, 0xd4, 0x00,
|
||||||
|
)))]);
|
||||||
|
|
||||||
|
// Bake rotation into path points (Penpot path storage model).
|
||||||
|
let bake = shape.centered_transform();
|
||||||
|
if let Type::Path(ref mut path) = shape.shape_type {
|
||||||
|
path.transform(&bake);
|
||||||
|
let b = path.bounds();
|
||||||
|
shape.set_selrect(b.min_x(), b.min_y(), b.max_x(), b.max_y());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill-rule=\"evenodd\""),
|
||||||
|
"rotated path inner stroke must emit an evenodd outline: {svg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
svg.matches("matrix(0.866025").count() >= 2,
|
||||||
|
"fill and stroke must each use the rotation matrix once: {svg}"
|
||||||
|
);
|
||||||
|
// Stroke outline `d` must stay in local (unrotated) space like the fill.
|
||||||
|
let stroke_d = svg
|
||||||
|
.split("fill-rule=\"evenodd\"")
|
||||||
|
.next()
|
||||||
|
.and_then(|before| before.rsplit("d=\"").next())
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.unwrap_or("");
|
||||||
|
let first_num = stroke_d
|
||||||
|
.trim_start_matches(|c: char| !c.is_ascii_digit() && c != '-' && c != '.')
|
||||||
|
.split(|c: char| !c.is_ascii_digit() && c != '-' && c != '.')
|
||||||
|
.find(|s| !s.is_empty())
|
||||||
|
.and_then(|s| s.parse::<f32>().ok());
|
||||||
|
assert!(
|
||||||
|
matches!(first_num, Some(n) if (-40.0..180.0).contains(&n)),
|
||||||
|
"stroke path d= should start in local coords, got {first_num:?} from {stroke_d}: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_closed_path_with_solid_center_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_closed_path(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 100.0, 80.0),
|
||||||
|
solid_stroke(StrokeKind::Center, 8.0, skia::Color::from_rgb(0, 128, 0)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("stroke=\"green\"") || svg.to_ascii_lowercase().contains("stroke=\"#008000\""),
|
||||||
|
"closed path center stroke must keep a stroke attribute: {svg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!svg.contains("fill-rule=\"evenodd\""),
|
||||||
|
"center stroke must not expand to an evenodd outline: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_closed_path_with_solid_outer_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_closed_path(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 100.0, 80.0),
|
||||||
|
solid_stroke(StrokeKind::Outer, 8.0, skia::Color::from_rgb(0, 128, 0)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
// Path outer previously used save_layer+Clear (dropped by SkSVG). Must not
|
||||||
|
// be a bare stroked path with no visible paint.
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill=\"green\"") || svg.to_ascii_lowercase().contains("fill=\"#008000\""),
|
||||||
|
"closed path outer stroke must emit a filled outline: {svg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill-rule=\"evenodd\""),
|
||||||
|
"aligned stroke outline should use evenodd: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_open_path_with_solid_inner_stroke() {
|
||||||
|
// Open paths force Center alignment regardless of the requested kind.
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_open_path(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 100.0, 80.0),
|
||||||
|
solid_stroke(StrokeKind::Inner, 8.0, skia::Color::from_rgb(0, 0, 255)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("stroke=\"blue\"") || svg.to_ascii_lowercase().contains("stroke=\"#0000ff\""),
|
||||||
|
"open path inner stroke must render as center stroke: {svg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!svg.contains("fill-rule=\"evenodd\""),
|
||||||
|
"open path must not expand to an evenodd outline: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_open_path_with_solid_center_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_open_path(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 100.0, 80.0),
|
||||||
|
solid_stroke(StrokeKind::Center, 8.0, skia::Color::from_rgb(255, 0, 0)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("stroke=\"red\"") || svg.to_ascii_lowercase().contains("stroke=\"#ff0000\""),
|
||||||
|
"open path center stroke must keep a stroke attribute: {svg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!svg.contains("fill-rule=\"evenodd\""),
|
||||||
|
"open path must not expand to an evenodd outline: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_open_path_with_solid_outer_stroke() {
|
||||||
|
// Open paths force Center alignment regardless of the requested kind.
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_open_path(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 100.0, 80.0),
|
||||||
|
solid_stroke(StrokeKind::Outer, 8.0, skia::Color::from_rgb(0, 128, 0)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("stroke=\"green\"") || svg.to_ascii_lowercase().contains("stroke=\"#008000\""),
|
||||||
|
"open path outer stroke must render as center stroke: {svg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!svg.contains("fill-rule=\"evenodd\""),
|
||||||
|
"open path must not expand to an evenodd outline: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_rect_with_dotted_inner_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_rect(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(10.0, 10.0, 110.0, 90.0),
|
||||||
|
dotted_stroke(StrokeKind::Inner, 10.0, skia::Color::from_rgb(0, 0, 255)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill=\"blue\"") || svg.to_ascii_lowercase().contains("fill=\"#0000ff\""),
|
||||||
|
"dotted inner stroke must emit filled geometry: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_rect_with_dotted_center_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_rect(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(10.0, 10.0, 110.0, 90.0),
|
||||||
|
dotted_stroke(StrokeKind::Center, 8.0, skia::Color::from_rgb(0, 0, 255)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
// PathEffect does not serialize; dots expand to filled outline geometry.
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill=\"blue\"") || svg.to_ascii_lowercase().contains("fill=\"#0000ff\""),
|
||||||
|
"dotted center stroke must emit filled geometry: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_rect_with_dotted_outer_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_rect(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(20.0, 20.0, 120.0, 100.0),
|
||||||
|
dotted_stroke(StrokeKind::Outer, 10.0, skia::Color::from_rgb(255, 0, 0)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill=\"red\"") || svg.to_ascii_lowercase().contains("fill=\"#ff0000\""),
|
||||||
|
"dotted outer stroke must emit filled geometry: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_closed_path_with_dotted_inner_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_closed_path(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 100.0, 80.0),
|
||||||
|
dotted_stroke(StrokeKind::Inner, 8.0, skia::Color::from_rgb(0, 0, 255)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill=\"blue\"") || svg.to_ascii_lowercase().contains("fill=\"#0000ff\""),
|
||||||
|
"closed path dotted inner stroke must emit filled geometry: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_closed_path_with_dotted_center_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_closed_path(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 100.0, 80.0),
|
||||||
|
dotted_stroke(StrokeKind::Center, 8.0, skia::Color::from_rgb(0, 128, 0)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill=\"green\"") || svg.to_ascii_lowercase().contains("fill=\"#008000\""),
|
||||||
|
"closed path dotted center stroke must emit filled geometry: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_closed_path_with_dotted_outer_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_closed_path(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 100.0, 80.0),
|
||||||
|
dotted_stroke(StrokeKind::Outer, 8.0, skia::Color::from_rgb(0, 128, 0)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill=\"green\"") || svg.to_ascii_lowercase().contains("fill=\"#008000\""),
|
||||||
|
"closed path dotted outer stroke must emit filled geometry: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_open_path_with_dotted_center_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_open_path(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 100.0, 80.0),
|
||||||
|
dotted_stroke(StrokeKind::Center, 8.0, skia::Color::from_rgb(255, 0, 0)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill=\"red\"") || svg.to_ascii_lowercase().contains("fill=\"#ff0000\""),
|
||||||
|
"open path dotted stroke must emit filled geometry: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_open_path_with_dotted_stroke_and_caps() {
|
||||||
|
// Regression: dotted/dashed SVG expansion used stroke_to_path and returned
|
||||||
|
// before draw_stroke_geometry, so open-path caps (triangle/circle/…) were
|
||||||
|
// dropped. Caps must be overlaid after the expanded outline.
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
let mut stroke = dotted_stroke(
|
||||||
|
StrokeKind::Center,
|
||||||
|
12.0,
|
||||||
|
skia::Color::from_rgb(0x10, 0x40, 0xff),
|
||||||
|
);
|
||||||
|
stroke.cap_start = Some(StrokeCap::TriangleArrow);
|
||||||
|
stroke.cap_end = Some(StrokeCap::CircleMarker);
|
||||||
|
add_stroked_open_path(&mut pool, id, Uuid::nil(), (0.0, 0.0, 140.0, 90.0), stroke);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill=\"#1040FF\"") || svg.to_ascii_lowercase().contains("fill=\"#1040ff\""),
|
||||||
|
"dotted stroke with caps must emit filled geometry: {svg}"
|
||||||
|
);
|
||||||
|
// Caps are separate filled draws (triangle + circle), not only the dotted outline.
|
||||||
|
assert!(
|
||||||
|
svg.matches("<path ").count() >= 2 || svg.contains("<circle"),
|
||||||
|
"expected separate cap geometry besides the dotted outline: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_rect_with_dashed_center_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_rect(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(10.0, 10.0, 110.0, 90.0),
|
||||||
|
dashed_stroke(StrokeKind::Center, 8.0, skia::Color::from_rgb(0, 0, 255)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill=\"blue\"") || svg.to_ascii_lowercase().contains("fill=\"#0000ff\""),
|
||||||
|
"dashed center stroke must emit filled geometry: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_rect_with_dashed_outer_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_rect(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(20.0, 20.0, 120.0, 100.0),
|
||||||
|
dashed_stroke(StrokeKind::Outer, 10.0, skia::Color::from_rgb(255, 0, 0)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill=\"red\"") || svg.to_ascii_lowercase().contains("fill=\"#ff0000\""),
|
||||||
|
"dashed outer stroke must emit filled geometry: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_closed_path_with_dashed_inner_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_closed_path(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 100.0, 80.0),
|
||||||
|
dashed_stroke(StrokeKind::Inner, 8.0, skia::Color::from_rgb(0, 128, 0)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill=\"green\"") || svg.to_ascii_lowercase().contains("fill=\"#008000\""),
|
||||||
|
"closed path dashed inner stroke must emit filled geometry: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_rect_with_mixed_center_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_rect(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(10.0, 10.0, 110.0, 90.0),
|
||||||
|
mixed_stroke(StrokeKind::Center, 8.0, skia::Color::from_rgb(0, 0, 255)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill=\"blue\"") || svg.to_ascii_lowercase().contains("fill=\"#0000ff\""),
|
||||||
|
"mixed center stroke must emit filled geometry: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exports_closed_path_with_mixed_outer_stroke() {
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let id = uid(1);
|
||||||
|
add_stroked_closed_path(
|
||||||
|
&mut pool,
|
||||||
|
id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 100.0, 80.0),
|
||||||
|
mixed_stroke(StrokeKind::Outer, 8.0, skia::Color::from_rgb(255, 0, 0)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg = render(&pool, id);
|
||||||
|
assert!(
|
||||||
|
svg.contains("fill=\"red\"") || svg.to_ascii_lowercase().contains("fill=\"#ff0000\""),
|
||||||
|
"closed path mixed outer stroke must emit filled geometry: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn exports_solid_text_with_font_face() {
|
fn exports_solid_text_with_font_face() {
|
||||||
let mut pool = ShapesPool::new();
|
let mut pool = ShapesPool::new();
|
||||||
|
|||||||
@ -2,7 +2,8 @@ use skia_safe::{self as skia, Canvas, Paint, RRect};
|
|||||||
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::shapes::{
|
use crate::shapes::{
|
||||||
merge_fills, radius_to_sigma, BlurType, Fill, Frame, Rect, Shape, Stroke, StrokeKind, Type,
|
circle_segments_local, merge_fills, radius_to_sigma, rect_segments_local, stroke_to_path,
|
||||||
|
BlurType, Fill, Frame, Path, Rect, Shape, Stroke, StrokeKind, StrokeStyle, Type,
|
||||||
};
|
};
|
||||||
use crate::state::ShapesPoolRef;
|
use crate::state::ShapesPoolRef;
|
||||||
use crate::uuid::Uuid;
|
use crate::uuid::Uuid;
|
||||||
@ -22,9 +23,10 @@ pub(super) struct VectorRenderer<'a> {
|
|||||||
canvas: &'a Canvas,
|
canvas: &'a Canvas,
|
||||||
shared: &'a mut RenderResources,
|
shared: &'a mut RenderResources,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
/// When `true`, multiple fills are composited into a single shader (PDF).
|
/// When `true`, use PDF/GPU-friendly compositing (`merge_fills`,
|
||||||
/// When `false`, each fill is drawn separately so SkSVGDevice can emit
|
/// `save_layer` for outer strokes). When `false` (SVG export), avoid
|
||||||
/// `fill` attributes (SVG export).
|
/// techniques that `SkSVGDevice` drops: draw fills individually and emit
|
||||||
|
/// solid Inner/Outer strokes as filled outlines.
|
||||||
compose_fills: bool,
|
compose_fills: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,8 +84,16 @@ impl ShapeRenderer for VectorRenderer<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_strokes(&mut self, shape: &Shape, strokes: &[&Stroke]) -> Result<()> {
|
fn draw_strokes(&mut self, shape: &Shape, strokes: &[&Stroke]) -> Result<()> {
|
||||||
|
let svg_export = !self.compose_fills;
|
||||||
for stroke in strokes.iter().rev() {
|
for stroke in strokes.iter().rev() {
|
||||||
draw_single_stroke(self.canvas, self.shared, self.scale, shape, stroke)?;
|
draw_single_stroke(
|
||||||
|
self.canvas,
|
||||||
|
self.shared,
|
||||||
|
self.scale,
|
||||||
|
shape,
|
||||||
|
stroke,
|
||||||
|
svg_export,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -840,9 +850,9 @@ fn render_frame(
|
|||||||
canvas.save_layer(&layer_rec);
|
canvas.save_layer(&layer_rec);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clip to frame bounds in the frame's own space, then undo the transform so
|
// Clip fills + children only. Strokes render outside the content clip so
|
||||||
// children draw at their absolute coords while staying clipped (mirrors the
|
// outer/center strokes are not trimmed (same as GPU render_shape_exit).
|
||||||
// GPU clip). Outset ~0.5px like the GPU clip to avoid an AA seam.
|
canvas.save();
|
||||||
if element.clip_content {
|
if element.clip_content {
|
||||||
canvas.concat(&matrix);
|
canvas.concat(&matrix);
|
||||||
clip_to_frame_content(canvas, element, scale);
|
clip_to_frame_content(canvas, element, scale);
|
||||||
@ -866,8 +876,9 @@ fn render_frame(
|
|||||||
for child_id in &children {
|
for child_id in &children {
|
||||||
render_tree_inner(shared, canvas, child_id, tree, scale, opts)?;
|
render_tree_inner(shared, canvas, child_id, tree, scale, opts)?;
|
||||||
}
|
}
|
||||||
|
canvas.restore(); // content clip
|
||||||
|
|
||||||
// Strokes over children (clipped frames), in the frame's space.
|
// Strokes over children, outside the frame content clip.
|
||||||
let visible_strokes: Vec<&Stroke> = element.visible_strokes().collect();
|
let visible_strokes: Vec<&Stroke> = element.visible_strokes().collect();
|
||||||
if !visible_strokes.is_empty() {
|
if !visible_strokes.is_empty() {
|
||||||
canvas.save();
|
canvas.save();
|
||||||
@ -1064,16 +1075,109 @@ fn draw_single_stroke(
|
|||||||
scale: f32,
|
scale: f32,
|
||||||
shape: &Shape,
|
shape: &Shape,
|
||||||
stroke: &Stroke,
|
stroke: &Stroke,
|
||||||
|
svg_export: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Image-fill strokes: the stroke masks the visible area of the image.
|
// Image-fill strokes: the stroke masks the visible area of the image.
|
||||||
if let Fill::Image(image_fill) = &stroke.fill {
|
if let Fill::Image(image_fill) = &stroke.fill {
|
||||||
return draw_image_stroke(canvas, shared, scale, shape, stroke, image_fill);
|
return draw_image_stroke(canvas, shared, scale, shape, stroke, image_fill);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Techniques SkSVGDevice cannot keep (save_layer+Clear/clip for Outer,
|
||||||
|
// PathEffect stamps for dots/dashes): expand to a filled outline instead.
|
||||||
|
// Solid Center stays on the shared stroke path.
|
||||||
|
if svg_export && draw_svg_stroke_as_fill(canvas, shape, stroke) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
draw_stroke_geometry(canvas, scale, shape, stroke, false);
|
draw_stroke_geometry(canvas, scale, shape, stroke, false);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draws a stroke as a filled path outline for SVG export.
|
||||||
|
///
|
||||||
|
/// Handles solid Inner/Outer and all dotted/dashed/mixed alignments (including
|
||||||
|
/// Center and open paths, which force Center). Returns `true` when handled.
|
||||||
|
fn draw_svg_stroke_as_fill(canvas: &Canvas, shape: &Shape, stroke: &Stroke) -> bool {
|
||||||
|
let is_open = shape.is_open();
|
||||||
|
let kind = stroke.render_kind(is_open);
|
||||||
|
|
||||||
|
// Per-side rect/frame strokes already expand to an evenodd band in
|
||||||
|
// `draw_stroke_on_rect`. `stroke_to_path` only knows a uniform width.
|
||||||
|
if stroke.per_side_widths().is_some()
|
||||||
|
&& matches!(shape.shape_type, Type::Rect(_) | Type::Frame(_))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let solid_outline = match stroke.style {
|
||||||
|
StrokeStyle::Solid => match kind {
|
||||||
|
// Solid Center already serializes as a native SVG stroke.
|
||||||
|
StrokeKind::Center => return false,
|
||||||
|
StrokeKind::Inner | StrokeKind::Outer => {
|
||||||
|
if is_open {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// PathEffects (path_1d / dash) do not survive SkSVGDevice; expand them.
|
||||||
|
StrokeStyle::Dotted | StrokeStyle::Dashed | StrokeStyle::Mixed => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Local (untransformed) geometry: the SVG leaf canvas already has
|
||||||
|
// `centered_transform`. Using `rect_segments` / `circle_segments` here
|
||||||
|
// would bake the same transform into the path and double-rotate.
|
||||||
|
// Path/Bool content is stored in parent space; `to_path_transform`
|
||||||
|
// undoes that so the outline matches `get_skia_path` (fills) under CTM.
|
||||||
|
let shape_path = match &shape.shape_type {
|
||||||
|
Type::Rect(r) => Path::new(rect_segments_local(shape, r.corners)),
|
||||||
|
Type::Frame(f) => Path::new(rect_segments_local(shape, f.corners)),
|
||||||
|
Type::Circle => Path::new(circle_segments_local(shape)),
|
||||||
|
Type::Path(_) | Type::Bool(_) => match shape.shape_type.path() {
|
||||||
|
Some(path) => {
|
||||||
|
let mut local = path.clone();
|
||||||
|
if let Some(t) = shape.to_path_transform() {
|
||||||
|
local.transform(&t);
|
||||||
|
}
|
||||||
|
local
|
||||||
|
}
|
||||||
|
None => return false,
|
||||||
|
},
|
||||||
|
Type::Text(_) | Type::SVGRaw(_) | Type::Group(_) => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(outline) = stroke_to_path(
|
||||||
|
stroke,
|
||||||
|
&shape_path,
|
||||||
|
None,
|
||||||
|
&shape.selrect,
|
||||||
|
shape.svg_attrs.as_ref(),
|
||||||
|
solid_outline,
|
||||||
|
) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut paint = stroke.fill.to_paint(&shape.selrect, true);
|
||||||
|
paint.set_style(skia::PaintStyle::Fill);
|
||||||
|
paint.set_anti_alias(true);
|
||||||
|
canvas.draw_path(&outline.to_skia_path(shape.svg_attrs.as_ref()), &paint);
|
||||||
|
|
||||||
|
// Expanded dotted/dashed strokes skip `draw_stroke_geometry`, which is
|
||||||
|
// where open-path caps are drawn. Overlay them here in local path space
|
||||||
|
// (same as fills / the outline above under the leaf CTM).
|
||||||
|
if is_open {
|
||||||
|
if let Some(cap_path) = transformed_skia_path(shape) {
|
||||||
|
let cap_paint =
|
||||||
|
stroke.to_stroked_paint(true, &shape.selrect, shape.svg_attrs.as_ref(), true);
|
||||||
|
super::strokes::handle_stroke_caps(
|
||||||
|
&cap_path, stroke, canvas, true, &cap_paint, None, true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// Draws a stroke's geometry by shape type, kind and dash style. Rect/Circle
|
/// Draws a stroke's geometry by shape type, kind and dash style. Rect/Circle
|
||||||
/// reuse the GPU stroke fns (dash/alignment parity); Path/Bool use double-width
|
/// reuse the GPU stroke fns (dash/alignment parity); Path/Bool use double-width
|
||||||
/// + clip/clear + caps. `opaque` forces black for an image-stroke silhouette.
|
/// + clip/clear + caps. `opaque` forces black for an image-stroke silhouette.
|
||||||
|
|||||||
@ -102,9 +102,18 @@ fn fix_radius(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn rect_segments(shape: &Shape, corners: Option<Corners>) -> Vec<Segment> {
|
pub fn rect_segments(shape: &Shape, corners: Option<Corners>) -> Vec<Segment> {
|
||||||
|
transform_segments(rect_segments_local(shape, corners), shape)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Axis-aligned rect path in selrect space (no `shape.transform`).
|
||||||
|
///
|
||||||
|
/// Use when the caller already applies [`Shape::centered_transform`] on the
|
||||||
|
/// canvas (e.g. SVG leaf export); [`rect_segments`] would bake the transform
|
||||||
|
/// into the path and double-rotate.
|
||||||
|
pub fn rect_segments_local(shape: &Shape, corners: Option<Corners>) -> Vec<Segment> {
|
||||||
let sr = shape.selrect;
|
let sr = shape.selrect;
|
||||||
|
|
||||||
let segments = if let Some([r1, r2, r3, r4]) = corners {
|
if let Some([r1, r2, r3, r4]) = corners {
|
||||||
let (r1, r2, r3, r4) = fix_radius(r1, r2, r3, r4, sr.width(), sr.height());
|
let (r1, r2, r3, r4) = fix_radius(r1, r2, r3, r4, sr.width(), sr.height());
|
||||||
|
|
||||||
let p1 = (sr.x(), sr.y() + r1.y);
|
let p1 = (sr.x(), sr.y() + r1.y);
|
||||||
@ -139,9 +148,7 @@ pub fn rect_segments(shape: &Shape, corners: Option<Corners>) -> Vec<Segment> {
|
|||||||
Segment::LineTo(p4),
|
Segment::LineTo(p4),
|
||||||
Segment::Close,
|
Segment::Close,
|
||||||
]
|
]
|
||||||
};
|
}
|
||||||
|
|
||||||
transform_segments(segments, shape)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn transform_point(p: (f32, f32), matrix: &skia_safe::Matrix) -> (f32, f32) {
|
fn transform_point(p: (f32, f32), matrix: &skia_safe::Matrix) -> (f32, f32) {
|
||||||
@ -151,6 +158,11 @@ fn transform_point(p: (f32, f32), matrix: &skia_safe::Matrix) -> (f32, f32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn circle_segments(shape: &Shape) -> Vec<Segment> {
|
pub fn circle_segments(shape: &Shape) -> Vec<Segment> {
|
||||||
|
transform_segments(circle_segments_local(shape), shape)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Circle path in selrect space (no `shape.transform`). See [`rect_segments_local`].
|
||||||
|
pub fn circle_segments_local(shape: &Shape) -> Vec<Segment> {
|
||||||
let sr = shape.selrect;
|
let sr = shape.selrect;
|
||||||
let c = BEZIER_CIRCLE_C;
|
let c = BEZIER_CIRCLE_C;
|
||||||
let c1x = sr.x() + (sr.width() / 2.0 * (1.0 - c));
|
let c1x = sr.x() + (sr.width() / 2.0 * (1.0 - c));
|
||||||
@ -168,15 +180,13 @@ pub fn circle_segments(shape: &Shape) -> Vec<Segment> {
|
|||||||
let p3 = (mx, ey);
|
let p3 = (mx, ey);
|
||||||
let p4 = (sr.x(), my);
|
let p4 = (sr.x(), my);
|
||||||
|
|
||||||
let segments = vec![
|
vec![
|
||||||
Segment::MoveTo(p1),
|
Segment::MoveTo(p1),
|
||||||
Segment::CurveTo(((c2x, p1.1), (p2.0, c1y), p2)),
|
Segment::CurveTo(((c2x, p1.1), (p2.0, c1y), p2)),
|
||||||
Segment::CurveTo(((p2.0, c2y), (c2x, p3.1), p3)),
|
Segment::CurveTo(((p2.0, c2y), (c2x, p3.1), p3)),
|
||||||
Segment::CurveTo(((c1x, p3.1), (p4.0, c2y), p4)),
|
Segment::CurveTo(((c1x, p3.1), (p4.0, c2y), p4)),
|
||||||
Segment::CurveTo(((p4.0, c1y), (c1x, p1.1), p1)),
|
Segment::CurveTo(((p4.0, c1y), (c1x, p1.1), p1)),
|
||||||
];
|
]
|
||||||
|
|
||||||
transform_segments(segments, shape)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn join_paths(path: Path, other: Path) -> Path {
|
fn join_paths(path: Path, other: Path) -> Path {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user