Skip to main content

azul_layout/xml/
svg.rs

1//! SVG tessellation, rendering, and geometric operations.
2//!
3//! This module provides:
4//! - **Tessellation** of SVG primitives (paths, circles, rects, multi-polygons)
5//!   via the lyon tessellation library (behind the `svg` feature flag).
6//! - **CPU clip-mask rendering** via the agg-rust rasterizer (`render_node_clipmask_cpu`).
7//! - **FXAA post-processing** for GPU-rendered textures (`apply_fxaa`).
8//! - **Boolean polygon operations** (union, intersection, difference, XOR)
9//!   on `SvgMultiPolygon` shapes via agg scanline boolean algebra.
10//! - **SVG parsing and rendering** (`svg_parse`, `svg_render`) using an
11//!   XML parser and the agg-rust rendering pipeline.
12
13use alloc::boxed::Box;
14use core::fmt;
15
16#[cfg(not(feature = "svg"))]
17pub use azul_core::svg::*;
18// re-export everything except for Svg and SvgXmlNode
19#[cfg(feature = "svg")]
20pub use azul_core::svg::{
21    c_void,
22    FontDatabase,
23    ImageRendering,
24    Indent,
25    OptionSvgDashPattern,
26    ResultSvgSvgParseError,
27    ResultSvgXmlNodeSvgParseError,
28    ShapeRendering,
29    SvgCircle,
30    SvgColoredVertex,
31    SvgColoredVertexVec,
32    SvgColoredVertexVecDestructor,
33    SvgDashPattern,
34    SvgFillRule,
35    SvgFillStyle,
36    SvgFitTo,
37    SvgLine,
38    SvgLineCap,
39    SvgLineJoin,
40    SvgMultiPolygon,
41    SvgMultiPolygonVec,
42    SvgMultiPolygonVecDestructor,
43    SvgNode,
44    SvgParseError,
45    SvgParseOptions,
46    SvgPath,
47    SvgPathElement,
48    SvgPathElementVec,
49    SvgPathElementVecDestructor,
50    SvgPathVec,
51    SvgPathVecDestructor,
52    SvgRenderOptions,
53    SvgRenderTransform,
54
55    SvgSimpleNode,
56    SvgSimpleNodeVec,
57    SvgSimpleNodeVecDestructor,
58    SvgSize,
59    SvgStrokeStyle,
60    SvgStyle,
61    SvgStyledNode,
62    SvgTransform,
63    SvgVertex,
64    SvgVertexVec,
65    SvgVertexVecDestructor,
66    SvgXmlOptions,
67    TessellatedColoredSvgNode,
68    TessellatedColoredSvgNodeVec,
69    TessellatedColoredSvgNodeVecDestructor,
70    // SvgXmlNode, Svg
71    TessellatedGPUSvgNode,
72    TessellatedSvgNode,
73    TessellatedSvgNodeVec,
74    TessellatedSvgNodeVecDestructor,
75    TessellatedSvgNodeVecRef,
76    TextRendering,
77};
78use azul_core::{
79    geom::PhysicalSizeU32,
80    gl::{GlContextPtr, Texture},
81    resources::{RawImage, RawImageFormat},
82};
83#[cfg(feature = "svg")]
84pub use azul_css::props::basic::animation::{
85    SvgCubicCurve, SvgPoint, SvgQuadraticCurve, SvgRect, SvgVector,
86};
87use azul_css::{
88    impl_result, impl_result_inner,
89    props::basic::{ColorU, LayoutSize, OptionColorU, OptionLayoutSize},
90    AzString, OptionI16, OptionString, OptionU16, StringVec, U8Vec,
91};
92#[cfg(feature = "svg")]
93use lyon::{
94    geom::euclid::{Point2D, Rect, Size2D, UnknownUnit},
95    math::Point,
96    path::Path,
97    tessellation::{
98        BuffersBuilder, FillOptions, FillTessellator, FillVertex, StrokeOptions, StrokeTessellator,
99        StrokeVertex, VertexBuffers,
100    },
101};
102
103use crate::xml::XmlError;
104
105#[cfg(feature = "svg")]
106extern crate agg_rust;
107
108use azul_core::gl::GL_RESTART_INDEX;
109
110/// Kappa constant for approximating a circle with 4 cubic Bezier curves: 4/3 * (sqrt(2) - 1).
111const CIRCLE_BEZIER_KAPPA: f64 = 0.552_284_749_8;
112
113/// Default render size (width, height) when no target size is specified for SVG rendering.
114const DEFAULT_SVG_RENDER_SIZE: (u32, u32) = (800, 600);
115
116#[cfg(feature = "svg")]
117const fn translate_svg_line_join(e: SvgLineJoin) -> lyon::tessellation::LineJoin {
118    use azul_core::svg::SvgLineJoin::{Miter, MiterClip, Round, Bevel};
119    match e {
120        Miter => lyon::tessellation::LineJoin::Miter,
121        MiterClip => lyon::tessellation::LineJoin::MiterClip,
122        Round => lyon::tessellation::LineJoin::Round,
123        Bevel => lyon::tessellation::LineJoin::Bevel,
124    }
125}
126
127#[cfg(feature = "svg")]
128const fn translate_svg_line_cap(e: SvgLineCap) -> lyon::tessellation::LineCap {
129    use azul_core::svg::SvgLineCap::{Butt, Square, Round};
130    match e {
131        Butt => lyon::tessellation::LineCap::Butt,
132        Square => lyon::tessellation::LineCap::Square,
133        Round => lyon::tessellation::LineCap::Round,
134    }
135}
136
137#[cfg(feature = "svg")]
138fn translate_svg_stroke_style(e: SvgStrokeStyle) -> StrokeOptions {
139    StrokeOptions::tolerance(e.tolerance)
140        .with_start_cap(translate_svg_line_cap(e.start_cap))
141        .with_end_cap(translate_svg_line_cap(e.end_cap))
142        .with_line_join(translate_svg_line_join(e.line_join))
143        .with_line_width(e.line_width)
144        .with_miter_limit(e.miter_limit)
145    // TODO: e.apply_line_width - not present in lyon 17!
146}
147
148#[cfg(feature = "svg")]
149fn svg_multipolygon_to_lyon_path(polygon: &SvgMultiPolygon) -> Path {
150    let mut builder = Path::builder();
151
152    for p in polygon.rings.as_ref() {
153        if p.items.as_ref().is_empty() {
154            continue;
155        }
156
157        let start_item = p.items.as_ref()[0];
158        let first_point = Point2D::new(start_item.get_start().x, start_item.get_start().y);
159
160        builder.begin(first_point);
161
162        for q in p.items.as_ref().iter().rev()
163        /* NOTE: REVERSE ITERATOR */
164        {
165            match q {
166                SvgPathElement::Line(l) => {
167                    builder.line_to(Point2D::new(l.end.x, l.end.y));
168                }
169                SvgPathElement::QuadraticCurve(qc) => {
170                    builder.quadratic_bezier_to(
171                        Point2D::new(qc.ctrl.x, qc.ctrl.y),
172                        Point2D::new(qc.end.x, qc.end.y),
173                    );
174                }
175                SvgPathElement::CubicCurve(cc) => {
176                    builder.cubic_bezier_to(
177                        Point2D::new(cc.ctrl_1.x, cc.ctrl_1.y),
178                        Point2D::new(cc.ctrl_2.x, cc.ctrl_2.y),
179                        Point2D::new(cc.end.x, cc.end.y),
180                    );
181                }
182            }
183        }
184
185        builder.end(p.is_closed());
186    }
187
188    builder.build()
189}
190
191#[cfg(feature = "svg")]
192fn svg_multi_shape_to_lyon_path(polygon: &[SvgSimpleNode]) -> Path {
193    use lyon::{
194        geom::Box2D,
195        path::{traits::PathBuilder, Winding},
196    };
197
198    let mut builder = Path::builder();
199
200    for p in polygon {
201        match p {
202            SvgSimpleNode::Path(p) => {
203                if p.items.as_ref().is_empty() {
204                    continue;
205                }
206
207                let start_item = p.items.as_ref()[0];
208                let first_point = Point2D::new(start_item.get_start().x, start_item.get_start().y);
209
210                builder.begin(first_point);
211
212                for q in p.items.as_ref().iter().rev()
213                /* NOTE: REVERSE ITERATOR */
214                {
215                    match q {
216                        SvgPathElement::Line(l) => {
217                            builder.line_to(Point2D::new(l.end.x, l.end.y));
218                        }
219                        SvgPathElement::QuadraticCurve(qc) => {
220                            builder.quadratic_bezier_to(
221                                Point2D::new(qc.ctrl.x, qc.ctrl.y),
222                                Point2D::new(qc.end.x, qc.end.y),
223                            );
224                        }
225                        SvgPathElement::CubicCurve(cc) => {
226                            builder.cubic_bezier_to(
227                                Point2D::new(cc.ctrl_1.x, cc.ctrl_1.y),
228                                Point2D::new(cc.ctrl_2.x, cc.ctrl_2.y),
229                                Point2D::new(cc.end.x, cc.end.y),
230                            );
231                        }
232                    }
233                }
234
235                builder.end(p.is_closed());
236            }
237            SvgSimpleNode::Circle(c) => {
238                builder.add_circle(
239                    Point::new(c.center_x, c.center_y),
240                    c.radius,
241                    Winding::Positive,
242                );
243            }
244            SvgSimpleNode::CircleHole(c) => {
245                builder.add_circle(
246                    Point::new(c.center_x, c.center_y),
247                    c.radius,
248                    Winding::Negative,
249                );
250            }
251            SvgSimpleNode::Rect(c) => {
252                builder.add_rectangle(
253                    &Box2D::from_origin_and_size(
254                        Point::new(c.x, c.y),
255                        Size2D::new(c.width, c.height),
256                    ),
257                    Winding::Positive,
258                );
259            }
260            SvgSimpleNode::RectHole(c) => {
261                builder.add_rectangle(
262                    &Box2D::from_origin_and_size(
263                        Point::new(c.x, c.y),
264                        Size2D::new(c.width, c.height),
265                    ),
266                    Winding::Negative,
267                );
268            }
269        }
270    }
271
272    builder.build()
273}
274
275#[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
276#[allow(clippy::similar_names)] // domain-standard coordinate/geometry/short-lived names
277#[must_use] pub fn raw_line_intersection(p: &SvgLine, q: &SvgLine) -> Option<SvgPoint> {
278    let p_min_x = p.start.x.min(p.end.x);
279    let p_min_y = p.start.y.min(p.end.y);
280    let p_max_x = p.start.x.max(p.end.x);
281    let p_max_y = p.start.y.max(p.end.y);
282
283    let q_min_x = q.start.x.min(q.end.x);
284    let q_min_y = q.start.y.min(q.end.y);
285    let q_max_x = q.start.x.max(q.end.x);
286    let q_max_y = q.start.y.max(q.end.y);
287
288    let int_min_x = p_min_x.max(q_min_x);
289    let int_max_x = p_max_x.min(q_max_x);
290    let int_min_y = p_min_y.max(q_min_y);
291    let int_max_y = p_max_y.min(q_max_y);
292
293    let two = 2.0;
294    let mid_x = (int_min_x + int_max_x) / two;
295    let mid_y = (int_min_y + int_max_y) / two;
296
297    // condition ordinate values by subtracting midpoint
298    let p1x = p.start.x - mid_x;
299    let p1y = p.start.y - mid_y;
300    let p2x = p.end.x - mid_x;
301    let p2y = p.end.y - mid_y;
302    let q1x = q.start.x - mid_x;
303    let q1y = q.start.y - mid_y;
304    let q2x = q.end.x - mid_x;
305    let q2y = q.end.y - mid_y;
306
307    // unrolled computation using homogeneous coordinates eqn
308    let px = p1y - p2y;
309    let py = p2x - p1x;
310    let pw = p1x * p2y - p2x * p1y;
311
312    let qx = q1y - q2y;
313    let qy = q2x - q1x;
314    let qw = q1x * q2y - q2x * q1y;
315
316    let xw = py * qw - qy * pw;
317    let yw = qx * pw - px * qw;
318    let w = px * qy - qx * py;
319
320    let x_int = xw / w;
321    let y_int = yw / w;
322
323    // check for parallel lines
324    if (x_int.is_nan() || x_int.is_infinite()) || (y_int.is_nan() || y_int.is_infinite()) {
325        None
326    } else {
327        // de-condition intersection point
328        Some(SvgPoint {
329            x: x_int + mid_x,
330            y: y_int + mid_y,
331        })
332    }
333}
334
335/// By-value wrapper for `raw_line_intersection` (for FFI)
336#[must_use] pub fn raw_line_intersection_byval(p: &SvgLine, q: SvgLine) -> Option<SvgPoint> {
337    raw_line_intersection(p, &q)
338}
339
340#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
341#[must_use] pub fn svg_path_offset(p: &SvgPath, distance: f32, join: SvgLineJoin, cap: SvgLineCap) -> SvgPath {
342    if distance == 0.0 {
343        return p.clone();
344    }
345
346    let mut items = p.items.as_slice().to_vec();
347    if let Some(mut first) = items.first() {
348        items.push(*first);
349    }
350
351    let mut items = items
352        .iter()
353        .map(|l| match l {
354            SvgPathElement::Line(q) => {
355                let normal = match q.outwards_normal() {
356                    Some(s) => SvgPoint {
357                        x: s.x * distance,
358                        y: s.y * distance,
359                    },
360                    None => return *l,
361                };
362
363                SvgPathElement::Line(SvgLine {
364                    start: SvgPoint {
365                        x: q.start.x + normal.x,
366                        y: q.start.y + normal.y,
367                    },
368                    end: SvgPoint {
369                        x: q.end.x + normal.x,
370                        y: q.end.y + normal.y,
371                    },
372                })
373            }
374            SvgPathElement::QuadraticCurve(q) => {
375                let n1 = match (SvgLine {
376                    start: q.start,
377                    end: q.ctrl,
378                }
379                .outwards_normal())
380                {
381                    Some(s) => SvgPoint {
382                        x: s.x * distance,
383                        y: s.y * distance,
384                    },
385                    None => return *l,
386                };
387
388                let n2 = match (SvgLine {
389                    start: q.ctrl,
390                    end: q.end,
391                }
392                .outwards_normal())
393                {
394                    Some(s) => SvgPoint {
395                        x: s.x * distance,
396                        y: s.y * distance,
397                    },
398                    None => return *l,
399                };
400
401                let nl1 = SvgLine {
402                    start: SvgPoint {
403                        x: q.start.x + n1.x,
404                        y: q.start.y + n1.y,
405                    },
406                    end: SvgPoint {
407                        x: q.ctrl.x + n1.x,
408                        y: q.ctrl.y + n1.y,
409                    },
410                };
411
412                let nl2 = SvgLine {
413                    start: SvgPoint {
414                        x: q.ctrl.x + n2.x,
415                        y: q.ctrl.y + n2.y,
416                    },
417                    end: SvgPoint {
418                        x: q.end.x + n2.x,
419                        y: q.end.y + n2.y,
420                    },
421                };
422
423                let Some(nctrl) = raw_line_intersection(&nl1, &nl2) else {
424                    return *l;
425                };
426
427                SvgPathElement::QuadraticCurve(SvgQuadraticCurve {
428                    start: nl1.start,
429                    ctrl: nctrl,
430                    end: nl2.end,
431                })
432            }
433            SvgPathElement::CubicCurve(q) => {
434                let n1 = match (SvgLine {
435                    start: q.start,
436                    end: q.ctrl_1,
437                }
438                .outwards_normal())
439                {
440                    Some(s) => SvgPoint {
441                        x: s.x * distance,
442                        y: s.y * distance,
443                    },
444                    None => return *l,
445                };
446
447                let n2 = match (SvgLine {
448                    start: q.ctrl_1,
449                    end: q.ctrl_2,
450                }
451                .outwards_normal())
452                {
453                    Some(s) => SvgPoint {
454                        x: s.x * distance,
455                        y: s.y * distance,
456                    },
457                    None => return *l,
458                };
459
460                let n3 = match (SvgLine {
461                    start: q.ctrl_2,
462                    end: q.end,
463                }
464                .outwards_normal())
465                {
466                    Some(s) => SvgPoint {
467                        x: s.x * distance,
468                        y: s.y * distance,
469                    },
470                    None => return *l,
471                };
472
473                let nl1 = SvgLine {
474                    start: SvgPoint {
475                        x: q.start.x + n1.x,
476                        y: q.start.y + n1.y,
477                    },
478                    end: SvgPoint {
479                        x: q.ctrl_1.x + n1.x,
480                        y: q.ctrl_1.y + n1.y,
481                    },
482                };
483
484                let nl2 = SvgLine {
485                    start: SvgPoint {
486                        x: q.ctrl_1.x + n2.x,
487                        y: q.ctrl_1.y + n2.y,
488                    },
489                    end: SvgPoint {
490                        x: q.ctrl_2.x + n2.x,
491                        y: q.ctrl_2.y + n2.y,
492                    },
493                };
494
495                let nl3 = SvgLine {
496                    start: SvgPoint {
497                        x: q.ctrl_2.x + n3.x,
498                        y: q.ctrl_2.y + n3.y,
499                    },
500                    end: SvgPoint {
501                        x: q.end.x + n3.x,
502                        y: q.end.y + n3.y,
503                    },
504                };
505
506                let Some(nctrl_1) = raw_line_intersection(&nl1, &nl2) else {
507                    return *l;
508                };
509
510                let Some(nctrl_2) = raw_line_intersection(&nl2, &nl3) else {
511                    return *l;
512                };
513
514                SvgPathElement::CubicCurve(SvgCubicCurve {
515                    start: nl1.start,
516                    ctrl_1: nctrl_1,
517                    ctrl_2: nctrl_2,
518                    end: nl3.end,
519                })
520            }
521        })
522        .collect::<Vec<_>>();
523
524    for i in 0..items.len().saturating_sub(2) {
525        let a_end_line = match items[i] {
526            SvgPathElement::Line(q) => q,
527            SvgPathElement::QuadraticCurve(q) => SvgLine {
528                start: q.ctrl,
529                end: q.end,
530            },
531            SvgPathElement::CubicCurve(q) => SvgLine {
532                start: q.ctrl_2,
533                end: q.end,
534            },
535        };
536
537        let b_start_line = match items[i + 1] {
538            SvgPathElement::Line(q) => q,
539            SvgPathElement::QuadraticCurve(q) => SvgLine {
540                start: q.ctrl,
541                end: q.start,
542            },
543            SvgPathElement::CubicCurve(q) => SvgLine {
544                start: q.ctrl_1,
545                end: q.start,
546            },
547        };
548
549        if let Some(intersect_pt) = raw_line_intersection(&a_end_line, &b_start_line) {
550            items[i].set_last(intersect_pt);
551            items[i + 1].set_first(intersect_pt);
552        }
553    }
554
555    items.pop();
556
557    SvgPath {
558        items: items.into(),
559    }
560}
561
562#[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
563fn shorten_line_end_by(line: SvgLine, distance: f32) -> SvgLine {
564    let dx = line.end.x - line.start.x;
565    let dy = line.end.y - line.start.y;
566    let dt = dx.hypot(dy);
567    let dt_short = dt - distance;
568
569    SvgLine {
570        start: line.start,
571        end: SvgPoint {
572            x: line.start.x + (dt_short / dt) * dx,
573            y: line.start.y + (dt_short / dt) * dy,
574        },
575    }
576}
577
578#[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
579fn shorten_line_start_by(line: SvgLine, distance: f32) -> SvgLine {
580    let dx = line.end.x - line.start.x;
581    let dy = line.end.y - line.start.y;
582    let dt = dx.hypot(dy);
583    let dt_short = dt - distance;
584
585    SvgLine {
586        start: SvgPoint {
587            x: line.start.x + (1.0 - dt_short / dt) * dx,
588            y: line.start.y + (1.0 - dt_short / dt) * dy,
589        },
590        end: line.end,
591    }
592}
593
594// Creates a "bevel"
595#[must_use] pub fn svg_path_bevel(p: &SvgPath, distance: f32) -> SvgPath {
596    let mut items = p.items.as_slice().to_vec();
597
598    // duplicate first & last items
599    let first = items.first().copied();
600    let last = items.last().copied();
601    if let Some(first) = first {
602        items.push(first);
603    }
604    items.reverse();
605    if let Some(last) = last {
606        items.push(last);
607    }
608    items.reverse();
609
610    let mut final_items = Vec::new();
611    for i in 0..items.len().saturating_sub(1) {
612        let a = items[i];
613        let b = items[i + 1];
614        match (a, b) {
615            (SvgPathElement::Line(a), SvgPathElement::Line(b)) => {
616                let a_short = shorten_line_end_by(a, distance);
617                let b_short = shorten_line_start_by(b, distance);
618                final_items.push(SvgPathElement::Line(a_short));
619                final_items.push(SvgPathElement::CubicCurve(SvgCubicCurve {
620                    start: a_short.end,
621                    ctrl_1: a.end,
622                    ctrl_2: b.start,
623                    end: b_short.start,
624                }));
625                final_items.push(SvgPathElement::Line(b_short));
626            }
627            (other_a, other_b) => {
628                final_items.push(other_a);
629                final_items.push(other_b);
630            }
631        }
632    }
633
634    // remove first & last items again
635    final_items.pop();
636    final_items.reverse();
637    final_items.pop();
638    final_items.reverse();
639
640    SvgPath {
641        items: final_items.into(),
642    }
643}
644
645#[cfg(feature = "svg")]
646fn svg_path_to_lyon_path_events(path: &SvgPath) -> Path {
647    let mut builder = Path::builder();
648
649    if !path.items.as_ref().is_empty() {
650        let start_item = path.items.as_ref()[0];
651        let first_point = Point2D::new(start_item.get_start().x, start_item.get_start().y);
652
653        builder.begin(first_point);
654
655        for p in path.items.as_ref() {
656            match p {
657                SvgPathElement::Line(l) => {
658                    builder.line_to(Point2D::new(l.end.x, l.end.y));
659                }
660                SvgPathElement::QuadraticCurve(qc) => {
661                    builder.quadratic_bezier_to(
662                        Point2D::new(qc.ctrl.x, qc.ctrl.y),
663                        Point2D::new(qc.end.x, qc.end.y),
664                    );
665                }
666                SvgPathElement::CubicCurve(cc) => {
667                    builder.cubic_bezier_to(
668                        Point2D::new(cc.ctrl_1.x, cc.ctrl_1.y),
669                        Point2D::new(cc.ctrl_2.x, cc.ctrl_2.y),
670                        Point2D::new(cc.end.x, cc.end.y),
671                    );
672                }
673            }
674        }
675
676        builder.end(path.is_closed());
677    }
678
679    builder.build()
680}
681
682#[cfg(feature = "svg")]
683#[inline]
684fn vertex_buffers_to_tessellated_cpu_node(v: VertexBuffers<SvgVertex, u32>) -> TessellatedSvgNode {
685    TessellatedSvgNode {
686        vertices: v.vertices.into(),
687        indices: v.indices.into(),
688    }
689}
690
691#[cfg(feature = "svg")]
692#[must_use] pub fn tessellate_multi_polygon_fill(
693    polygon: &SvgMultiPolygon,
694    fill_style: SvgFillStyle,
695) -> TessellatedSvgNode {
696    let polygon = svg_multipolygon_to_lyon_path(polygon);
697
698    let mut geometry = VertexBuffers::new();
699    let mut tessellator = FillTessellator::new();
700
701    let tess_result = tessellator.tessellate_path(
702        &polygon,
703        &FillOptions::tolerance(fill_style.tolerance),
704        &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex<'_>| {
705            let xy_arr = vertex.position();
706            SvgVertex {
707                x: xy_arr.x,
708                y: xy_arr.y,
709            }
710        }),
711    );
712
713    if tess_result.is_err() {
714        TessellatedSvgNode::empty()
715    } else {
716        vertex_buffers_to_tessellated_cpu_node(geometry)
717    }
718}
719
720#[cfg(not(feature = "svg"))]
721pub fn tessellate_multi_polygon_fill(
722    polygon: &SvgMultiPolygon,
723    fill_style: SvgFillStyle,
724) -> TessellatedSvgNode {
725    TessellatedSvgNode::default()
726}
727
728#[cfg(feature = "svg")]
729#[must_use] pub fn tessellate_multi_shape_fill(
730    ms: &[SvgSimpleNode],
731    fill_style: SvgFillStyle,
732) -> TessellatedSvgNode {
733    let polygon = svg_multi_shape_to_lyon_path(ms);
734
735    let mut geometry = VertexBuffers::new();
736    let mut tessellator = FillTessellator::new();
737
738    let tess_result = tessellator.tessellate_path(
739        &polygon,
740        &FillOptions::tolerance(fill_style.tolerance),
741        &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex<'_>| {
742            let xy_arr = vertex.position();
743            SvgVertex {
744                x: xy_arr.x,
745                y: xy_arr.y,
746            }
747        }),
748    );
749
750    if tess_result.is_err() {
751        TessellatedSvgNode::empty()
752    } else {
753        vertex_buffers_to_tessellated_cpu_node(geometry)
754    }
755}
756
757#[cfg(not(feature = "svg"))]
758pub fn tessellate_multi_shape_fill(
759    ms: &[SvgSimpleNode],
760    fill_style: SvgFillStyle,
761) -> TessellatedSvgNode {
762    TessellatedSvgNode::default()
763}
764
765#[must_use] pub fn svg_node_contains_point(
766    node: &SvgNode,
767    point: SvgPoint,
768    fill_rule: SvgFillRule,
769    tolerance: f32,
770) -> bool {
771    match node {
772        SvgNode::MultiPolygonCollection(a) => a
773            .as_ref()
774            .iter()
775            .any(|e| polygon_contains_point(e, point, fill_rule, tolerance)),
776        SvgNode::MultiPolygon(a) => polygon_contains_point(a, point, fill_rule, tolerance),
777        SvgNode::Path(a) => {
778            if !a.is_closed() {
779                return false;
780            }
781            path_contains_point(a, point, fill_rule, tolerance)
782        }
783        SvgNode::Circle(a) => a.contains_point(point.x, point.y),
784        SvgNode::Rect(a) => a.contains_point(point),
785        SvgNode::MultiShape(a) => a.as_ref().iter().any(|e| match e {
786            SvgSimpleNode::Path(a) => {
787                if !a.is_closed() {
788                    return false;
789                }
790                path_contains_point(a, point, fill_rule, tolerance)
791            }
792            SvgSimpleNode::Circle(a) => a.contains_point(point.x, point.y),
793            SvgSimpleNode::Rect(a) => a.contains_point(point),
794            SvgSimpleNode::CircleHole(a) => !a.contains_point(point.x, point.y),
795            SvgSimpleNode::RectHole(a) => !a.contains_point(point),
796        }),
797    }
798}
799
800#[cfg(feature = "svg")]
801#[must_use] pub fn path_contains_point(
802    path: &SvgPath,
803    point: SvgPoint,
804    fill_rule: SvgFillRule,
805    tolerance: f32,
806) -> bool {
807    use lyon::{
808        algorithms::hit_test::hit_test_path, math::Point as LyonPoint,
809        path::FillRule as LyonFillRule,
810    };
811    let path = svg_path_to_lyon_path_events(path);
812    let fill_rule = match fill_rule {
813        SvgFillRule::Winding => LyonFillRule::NonZero,
814        SvgFillRule::EvenOdd => LyonFillRule::EvenOdd,
815    };
816    let point = LyonPoint::new(point.x, point.y);
817    hit_test_path(&point, path.iter(), fill_rule, tolerance)
818}
819
820#[cfg(not(feature = "svg"))]
821pub fn path_contains_point(
822    path: &SvgPath,
823    point: SvgPoint,
824    fill_rule: SvgFillRule,
825    tolerance: f32,
826) -> bool {
827    false
828}
829
830#[cfg(feature = "svg")]
831#[must_use] pub fn polygon_contains_point(
832    polygon: &SvgMultiPolygon,
833    point: SvgPoint,
834    fill_rule: SvgFillRule,
835    tolerance: f32,
836) -> bool {
837    use lyon::{
838        algorithms::hit_test::hit_test_path, math::Point as LyonPoint,
839        path::FillRule as LyonFillRule,
840    };
841    polygon.rings.iter().any(|path| {
842        let path = svg_path_to_lyon_path_events(path);
843        let fill_rule = match fill_rule {
844            SvgFillRule::Winding => LyonFillRule::NonZero,
845            SvgFillRule::EvenOdd => LyonFillRule::EvenOdd,
846        };
847        let point = LyonPoint::new(point.x, point.y);
848        hit_test_path(&point, path.iter(), fill_rule, tolerance)
849    })
850}
851
852#[cfg(not(feature = "svg"))]
853pub fn polygon_contains_point(
854    polygon: &SvgMultiPolygon,
855    point: SvgPoint,
856    fill_rule: SvgFillRule,
857    tolerance: f32,
858) -> bool {
859    false
860}
861
862#[cfg(feature = "svg")]
863#[must_use] pub fn tessellate_multi_shape_stroke(
864    ms: &[SvgSimpleNode],
865    stroke_style: SvgStrokeStyle,
866) -> TessellatedSvgNode {
867    let stroke_options: StrokeOptions = translate_svg_stroke_style(stroke_style);
868    let polygon = svg_multi_shape_to_lyon_path(ms);
869
870    let mut stroke_geometry = VertexBuffers::new();
871    let mut stroke_tess = StrokeTessellator::new();
872
873    let tess_result = stroke_tess.tessellate_path(
874        &polygon,
875        &stroke_options,
876        &mut BuffersBuilder::new(&mut stroke_geometry, |vertex: StrokeVertex<'_, '_>| {
877            let xy_arr = vertex.position();
878            SvgVertex {
879                x: xy_arr.x,
880                y: xy_arr.y,
881            }
882        }),
883    );
884
885    if tess_result.is_err() {
886        TessellatedSvgNode::empty()
887    } else {
888        vertex_buffers_to_tessellated_cpu_node(stroke_geometry)
889    }
890}
891
892#[cfg(not(feature = "svg"))]
893pub fn tessellate_multi_shape_stroke(
894    polygon: &[SvgSimpleNode],
895    stroke_style: SvgStrokeStyle,
896) -> TessellatedSvgNode {
897    TessellatedSvgNode::default()
898}
899
900#[cfg(feature = "svg")]
901#[must_use] pub fn tessellate_multi_polygon_stroke(
902    polygon: &SvgMultiPolygon,
903    stroke_style: SvgStrokeStyle,
904) -> TessellatedSvgNode {
905    let stroke_options: StrokeOptions = translate_svg_stroke_style(stroke_style);
906    let polygon = svg_multipolygon_to_lyon_path(polygon);
907
908    let mut stroke_geometry = VertexBuffers::new();
909    let mut stroke_tess = StrokeTessellator::new();
910
911    let tess_result = stroke_tess.tessellate_path(
912        &polygon,
913        &stroke_options,
914        &mut BuffersBuilder::new(&mut stroke_geometry, |vertex: StrokeVertex<'_, '_>| {
915            let xy_arr = vertex.position();
916            SvgVertex {
917                x: xy_arr.x,
918                y: xy_arr.y,
919            }
920        }),
921    );
922
923    if tess_result.is_err() {
924        TessellatedSvgNode::empty()
925    } else {
926        vertex_buffers_to_tessellated_cpu_node(stroke_geometry)
927    }
928}
929
930#[cfg(not(feature = "svg"))]
931pub fn tessellate_multi_polygon_stroke(
932    polygon: &SvgMultiPolygon,
933    stroke_style: SvgStrokeStyle,
934) -> TessellatedSvgNode {
935    TessellatedSvgNode::default()
936}
937
938#[cfg(feature = "svg")]
939#[must_use] pub fn tessellate_path_fill(path: &SvgPath, fill_style: SvgFillStyle) -> TessellatedSvgNode {
940    let polygon = svg_path_to_lyon_path_events(path);
941
942    let mut geometry = VertexBuffers::new();
943    let mut tessellator = FillTessellator::new();
944
945    let tess_result = tessellator.tessellate_path(
946        &polygon,
947        &FillOptions::tolerance(fill_style.tolerance),
948        &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex<'_>| {
949            let xy_arr = vertex.position();
950            SvgVertex {
951                x: xy_arr.x,
952                y: xy_arr.y,
953            }
954        }),
955    );
956
957    if tess_result.is_err() {
958        TessellatedSvgNode::empty()
959    } else {
960        vertex_buffers_to_tessellated_cpu_node(geometry)
961    }
962}
963
964#[cfg(not(feature = "svg"))]
965pub fn tessellate_path_fill(path: &SvgPath, fill_style: SvgFillStyle) -> TessellatedSvgNode {
966    TessellatedSvgNode::default()
967}
968
969#[cfg(feature = "svg")]
970#[must_use] pub fn tessellate_path_stroke(path: &SvgPath, stroke_style: SvgStrokeStyle) -> TessellatedSvgNode {
971    let stroke_options: StrokeOptions = translate_svg_stroke_style(stroke_style);
972    let polygon = svg_path_to_lyon_path_events(path);
973
974    let mut stroke_geometry = VertexBuffers::new();
975    let mut stroke_tess = StrokeTessellator::new();
976
977    let tess_result = stroke_tess.tessellate_path(
978        &polygon,
979        &stroke_options,
980        &mut BuffersBuilder::new(&mut stroke_geometry, |vertex: StrokeVertex<'_, '_>| {
981            let xy_arr = vertex.position();
982            SvgVertex {
983                x: xy_arr.x,
984                y: xy_arr.y,
985            }
986        }),
987    );
988
989    if tess_result.is_err() {
990        TessellatedSvgNode::empty()
991    } else {
992        vertex_buffers_to_tessellated_cpu_node(stroke_geometry)
993    }
994}
995
996#[cfg(not(feature = "svg"))]
997pub fn tessellate_path_stroke(path: &SvgPath, stroke_style: SvgStrokeStyle) -> TessellatedSvgNode {
998    TessellatedSvgNode::default()
999}
1000
1001#[cfg(feature = "svg")]
1002#[must_use] pub fn tessellate_circle_fill(c: &SvgCircle, fill_style: SvgFillStyle) -> TessellatedSvgNode {
1003    let center = Point2D::new(c.center_x, c.center_y);
1004
1005    let mut geometry = VertexBuffers::new();
1006    let mut tesselator = FillTessellator::new();
1007    let tess_result = tesselator.tessellate_circle(
1008        center,
1009        c.radius,
1010        &FillOptions::tolerance(fill_style.tolerance),
1011        &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex<'_>| {
1012            let xy_arr = vertex.position();
1013            SvgVertex {
1014                x: xy_arr.x,
1015                y: xy_arr.y,
1016            }
1017        }),
1018    );
1019
1020    if tess_result.is_err() {
1021        TessellatedSvgNode::empty()
1022    } else {
1023        vertex_buffers_to_tessellated_cpu_node(geometry)
1024    }
1025}
1026
1027#[cfg(not(feature = "svg"))]
1028pub fn tessellate_circle_fill(c: &SvgCircle, fill_style: SvgFillStyle) -> TessellatedSvgNode {
1029    TessellatedSvgNode::default()
1030}
1031
1032#[cfg(feature = "svg")]
1033#[must_use] pub fn tessellate_circle_stroke(c: &SvgCircle, stroke_style: SvgStrokeStyle) -> TessellatedSvgNode {
1034    let stroke_options: StrokeOptions = translate_svg_stroke_style(stroke_style);
1035    let center = Point2D::new(c.center_x, c.center_y);
1036
1037    let mut stroke_geometry = VertexBuffers::new();
1038    let mut tesselator = StrokeTessellator::new();
1039
1040    let tess_result = tesselator.tessellate_circle(
1041        center,
1042        c.radius,
1043        &stroke_options,
1044        &mut BuffersBuilder::new(&mut stroke_geometry, |vertex: StrokeVertex<'_, '_>| {
1045            let xy_arr = vertex.position();
1046            SvgVertex {
1047                x: xy_arr.x,
1048                y: xy_arr.y,
1049            }
1050        }),
1051    );
1052
1053    if tess_result.is_err() {
1054        TessellatedSvgNode::empty()
1055    } else {
1056        vertex_buffers_to_tessellated_cpu_node(stroke_geometry)
1057    }
1058}
1059
1060#[cfg(not(feature = "svg"))]
1061pub fn tessellate_circle_stroke(c: &SvgCircle, stroke_style: SvgStrokeStyle) -> TessellatedSvgNode {
1062    TessellatedSvgNode::default()
1063}
1064
1065// TODO: radii not respected on latest version of lyon
1066#[cfg(feature = "svg")]
1067fn get_radii(r: &SvgRect) -> lyon::geom::Box2D<f32> {
1068    
1069    /*
1070    let radii = BorderRadii {
1071        top_left: r.radius_top_left,
1072        top_right: r.radius_top_right,
1073        bottom_left: r.radius_bottom_left,
1074        bottom_right: r.radius_bottom_right
1075    };*/
1076    lyon::geom::Box2D::from_origin_and_size(
1077        Point2D::new(r.x, r.y),
1078        Size2D::new(r.width, r.height),
1079    )
1080}
1081
1082#[cfg(feature = "svg")]
1083#[must_use] pub fn tessellate_rect_fill(r: &SvgRect, fill_style: SvgFillStyle) -> TessellatedSvgNode {
1084    let rect = get_radii(r);
1085    let mut geometry = VertexBuffers::new();
1086    let mut tesselator = FillTessellator::new();
1087
1088    let tess_result = tesselator.tessellate_rectangle(
1089        &rect,
1090        &FillOptions::tolerance(fill_style.tolerance),
1091        &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex<'_>| {
1092            let xy_arr = vertex.position();
1093            SvgVertex {
1094                x: xy_arr.x,
1095                y: xy_arr.y,
1096            }
1097        }),
1098    );
1099
1100    if tess_result.is_err() {
1101        TessellatedSvgNode::empty()
1102    } else {
1103        vertex_buffers_to_tessellated_cpu_node(geometry)
1104    }
1105}
1106
1107#[cfg(not(feature = "svg"))]
1108pub fn tessellate_rect_fill(r: &SvgRect, fill_style: SvgFillStyle) -> TessellatedSvgNode {
1109    TessellatedSvgNode::default()
1110}
1111
1112#[cfg(feature = "svg")]
1113#[must_use] pub fn tessellate_rect_stroke(r: &SvgRect, stroke_style: SvgStrokeStyle) -> TessellatedSvgNode {
1114    let stroke_options: StrokeOptions = translate_svg_stroke_style(stroke_style);
1115    let rect = get_radii(r);
1116
1117    let mut stroke_geometry = VertexBuffers::new();
1118    let mut tesselator = StrokeTessellator::new();
1119
1120    let tess_result = tesselator.tessellate_rectangle(
1121        &rect,
1122        &stroke_options,
1123        &mut BuffersBuilder::new(&mut stroke_geometry, |vertex: StrokeVertex<'_, '_>| {
1124            let xy_arr = vertex.position();
1125            SvgVertex {
1126                x: xy_arr.x,
1127                y: xy_arr.y,
1128            }
1129        }),
1130    );
1131
1132    if tess_result.is_err() {
1133        TessellatedSvgNode::empty()
1134    } else {
1135        vertex_buffers_to_tessellated_cpu_node(stroke_geometry)
1136    }
1137}
1138
1139#[cfg(not(feature = "svg"))]
1140pub fn tessellate_rect_stroke(r: &SvgRect, stroke_style: SvgStrokeStyle) -> TessellatedSvgNode {
1141    TessellatedSvgNode::default()
1142}
1143
1144/// Tessellate the path using lyon
1145#[cfg(feature = "svg")]
1146#[must_use] pub fn tessellate_styled_node(node: &SvgStyledNode) -> TessellatedSvgNode {
1147    match node.style {
1148        SvgStyle::Fill(fs) => tessellate_node_fill(&node.geometry, fs),
1149        SvgStyle::Stroke(ss) => tessellate_node_stroke(&node.geometry, ss),
1150    }
1151}
1152
1153#[cfg(not(feature = "svg"))]
1154pub fn tessellate_styled_node(node: &SvgStyledNode) -> TessellatedSvgNode {
1155    TessellatedSvgNode::default()
1156}
1157
1158#[cfg(feature = "svg")]
1159#[must_use] pub fn tessellate_line_stroke(
1160    svgline: &SvgLine,
1161    stroke_style: SvgStrokeStyle,
1162) -> TessellatedSvgNode {
1163    let stroke_options: StrokeOptions = translate_svg_stroke_style(stroke_style);
1164
1165    let mut builder = Path::builder();
1166    builder.begin(Point2D::new(svgline.start.x, svgline.start.y));
1167    builder.line_to(Point2D::new(svgline.end.x, svgline.end.y));
1168    builder.end(/* closed */ false);
1169    let path = builder.build();
1170
1171    let mut stroke_geometry = VertexBuffers::new();
1172    let mut stroke_tess = StrokeTessellator::new();
1173
1174    let tess_result = stroke_tess.tessellate_path(
1175        &path,
1176        &stroke_options,
1177        &mut BuffersBuilder::new(&mut stroke_geometry, |vertex: StrokeVertex<'_, '_>| {
1178            let xy_arr = vertex.position();
1179            SvgVertex {
1180                x: xy_arr.x,
1181                y: xy_arr.y,
1182            }
1183        }),
1184    );
1185
1186    if tess_result.is_err() {
1187        TessellatedSvgNode::empty()
1188    } else {
1189        vertex_buffers_to_tessellated_cpu_node(stroke_geometry)
1190    }
1191}
1192
1193#[cfg(not(feature = "svg"))]
1194pub fn tessellate_line_stroke(
1195    svgline: &SvgLine,
1196    stroke_style: SvgStrokeStyle,
1197) -> TessellatedSvgNode {
1198    TessellatedSvgNode::default()
1199}
1200
1201#[cfg(feature = "svg")]
1202#[must_use] pub fn tessellate_cubiccurve_stroke(
1203    svgcubiccurve: &SvgCubicCurve,
1204    stroke_style: SvgStrokeStyle,
1205) -> TessellatedSvgNode {
1206    let stroke_options: StrokeOptions = translate_svg_stroke_style(stroke_style);
1207
1208    let mut builder = Path::builder();
1209    builder.begin(Point2D::new(svgcubiccurve.start.x, svgcubiccurve.start.y));
1210    builder.cubic_bezier_to(
1211        Point2D::new(svgcubiccurve.ctrl_1.x, svgcubiccurve.ctrl_1.y),
1212        Point2D::new(svgcubiccurve.ctrl_2.x, svgcubiccurve.ctrl_2.y),
1213        Point2D::new(svgcubiccurve.end.x, svgcubiccurve.end.y),
1214    );
1215    builder.end(/* closed */ false);
1216    let path = builder.build();
1217
1218    let mut stroke_geometry = VertexBuffers::new();
1219    let mut stroke_tess = StrokeTessellator::new();
1220
1221    let tess_result = stroke_tess.tessellate_path(
1222        &path,
1223        &stroke_options,
1224        &mut BuffersBuilder::new(&mut stroke_geometry, |vertex: StrokeVertex<'_, '_>| {
1225            let xy_arr = vertex.position();
1226            SvgVertex {
1227                x: xy_arr.x,
1228                y: xy_arr.y,
1229            }
1230        }),
1231    );
1232
1233    if tess_result.is_err() {
1234        TessellatedSvgNode::empty()
1235    } else {
1236        vertex_buffers_to_tessellated_cpu_node(stroke_geometry)
1237    }
1238}
1239
1240#[cfg(not(feature = "svg"))]
1241pub fn tessellate_cubiccurve_stroke(
1242    svgline: &SvgCubicCurve,
1243    stroke_style: SvgStrokeStyle,
1244) -> TessellatedSvgNode {
1245    TessellatedSvgNode::default()
1246}
1247
1248#[cfg(feature = "svg")]
1249#[must_use] pub fn tessellate_quadraticcurve_stroke(
1250    svgquadraticcurve: &SvgQuadraticCurve,
1251    stroke_style: SvgStrokeStyle,
1252) -> TessellatedSvgNode {
1253    let stroke_options: StrokeOptions = translate_svg_stroke_style(stroke_style);
1254
1255    let mut builder = Path::builder();
1256    builder.begin(Point2D::new(
1257        svgquadraticcurve.start.x,
1258        svgquadraticcurve.start.y,
1259    ));
1260    builder.quadratic_bezier_to(
1261        Point2D::new(svgquadraticcurve.ctrl.x, svgquadraticcurve.ctrl.y),
1262        Point2D::new(svgquadraticcurve.end.x, svgquadraticcurve.end.y),
1263    );
1264    builder.end(/* closed */ false);
1265    let path = builder.build();
1266
1267    let mut stroke_geometry = VertexBuffers::new();
1268    let mut stroke_tess = StrokeTessellator::new();
1269
1270    let tess_result = stroke_tess.tessellate_path(
1271        &path,
1272        &stroke_options,
1273        &mut BuffersBuilder::new(&mut stroke_geometry, |vertex: StrokeVertex<'_, '_>| {
1274            let xy_arr = vertex.position();
1275            SvgVertex {
1276                x: xy_arr.x,
1277                y: xy_arr.y,
1278            }
1279        }),
1280    );
1281
1282    if tess_result.is_err() {
1283        TessellatedSvgNode::empty()
1284    } else {
1285        vertex_buffers_to_tessellated_cpu_node(stroke_geometry)
1286    }
1287}
1288
1289#[cfg(not(feature = "svg"))]
1290pub fn tessellate_quadraticcurve_stroke(
1291    svgquadraticcurve: &SvgQuadraticCurve,
1292    stroke_style: SvgStrokeStyle,
1293) -> TessellatedSvgNode {
1294    TessellatedSvgNode::default()
1295}
1296
1297#[cfg(feature = "svg")]
1298#[must_use] pub fn tessellate_svgpathelement_stroke(
1299    svgpathelement: &SvgPathElement,
1300    stroke_style: SvgStrokeStyle,
1301) -> TessellatedSvgNode {
1302    match svgpathelement {
1303        SvgPathElement::Line(l) => tessellate_line_stroke(l, stroke_style),
1304        SvgPathElement::QuadraticCurve(l) => tessellate_quadraticcurve_stroke(l, stroke_style),
1305        SvgPathElement::CubicCurve(l) => tessellate_cubiccurve_stroke(l, stroke_style),
1306    }
1307}
1308
1309#[cfg(not(feature = "svg"))]
1310pub fn tessellate_svgpathelement_stroke(
1311    svgpathelement: &SvgPathElement,
1312    stroke_style: SvgStrokeStyle,
1313) -> TessellatedSvgNode {
1314    TessellatedSvgNode::default()
1315}
1316
1317#[cfg(feature = "svg")]
1318#[allow(clippy::cast_possible_truncation)] // bounded layout/render numeric cast
1319#[must_use] pub fn join_tessellated_nodes(nodes: &[TessellatedSvgNode]) -> TessellatedSvgNode {
1320    let mut index_offset = 0;
1321
1322    // note: can not be parallelized!
1323    let all_index_offsets = nodes
1324        .as_ref()
1325        .iter()
1326        .map(|t| {
1327            let i = index_offset;
1328            index_offset += t.vertices.len();
1329            i
1330        })
1331        .collect::<Vec<_>>();
1332
1333    let all_vertices = nodes
1334        .as_ref()
1335        .iter()
1336        .flat_map(|t| t.vertices.clone().into_library_owned_vec())
1337        .collect::<Vec<_>>();
1338
1339    let all_indices = nodes
1340        .as_ref()
1341        .iter()
1342        .enumerate()
1343        .flat_map(|(buffer_index, t)| {
1344            // since the vertex buffers are now joined,
1345            // offset the indices by the vertex buffers lengths
1346            // encountered so far
1347            let vertex_buffer_offset: u32 = all_index_offsets
1348                .get(buffer_index)
1349                .copied()
1350                .unwrap_or(0)
1351                .min(core::u32::MAX as usize) as u32;
1352
1353            let mut indices = t.indices.clone().into_library_owned_vec();
1354            if vertex_buffer_offset != 0 {
1355                for i in &mut indices {
1356                    if *i != GL_RESTART_INDEX {
1357                        *i += vertex_buffer_offset;
1358                    }
1359                }
1360            }
1361
1362            indices.push(GL_RESTART_INDEX);
1363
1364            indices
1365        })
1366        .collect::<Vec<_>>();
1367
1368    TessellatedSvgNode {
1369        vertices: all_vertices.into(),
1370        indices: all_indices.into(),
1371    }
1372}
1373
1374#[cfg(feature = "svg")]
1375#[allow(clippy::cast_possible_truncation)] // bounded layout/render numeric cast
1376#[must_use] pub fn join_tessellated_colored_nodes(
1377    nodes: &[TessellatedColoredSvgNode],
1378) -> TessellatedColoredSvgNode {
1379    let mut index_offset = 0;
1380
1381    // note: can not be parallelized!
1382    let all_index_offsets = nodes
1383        .as_ref()
1384        .iter()
1385        .map(|t| {
1386            let i = index_offset;
1387            index_offset += t.vertices.len();
1388            i
1389        })
1390        .collect::<Vec<_>>();
1391
1392    let all_vertices = nodes
1393        .as_ref()
1394        .iter()
1395        .flat_map(|t| t.vertices.clone().into_library_owned_vec())
1396        .collect::<Vec<_>>();
1397
1398    let all_indices = nodes
1399        .as_ref()
1400        .iter()
1401        .enumerate()
1402        .flat_map(|(buffer_index, t)| {
1403            // since the vertex buffers are now joined,
1404            // offset the indices by the vertex buffers lengths
1405            // encountered so far
1406            let vertex_buffer_offset: u32 = all_index_offsets
1407                .get(buffer_index)
1408                .copied()
1409                .unwrap_or(0)
1410                .min(core::u32::MAX as usize) as u32;
1411
1412            let mut indices = t.indices.clone().into_library_owned_vec();
1413            if vertex_buffer_offset != 0 {
1414                for i in &mut indices {
1415                    if *i != GL_RESTART_INDEX {
1416                        *i += vertex_buffer_offset;
1417                    }
1418                }
1419            }
1420
1421            indices.push(GL_RESTART_INDEX);
1422
1423            indices
1424        })
1425        .collect::<Vec<_>>();
1426
1427    TessellatedColoredSvgNode {
1428        vertices: all_vertices.into(),
1429        indices: all_indices.into(),
1430    }
1431}
1432
1433#[cfg(not(feature = "svg"))]
1434pub fn join_tessellated_nodes(nodes: &[TessellatedSvgNode]) -> TessellatedSvgNode {
1435    TessellatedSvgNode::default()
1436}
1437
1438#[cfg(not(feature = "svg"))]
1439pub fn join_tessellated_colored_nodes(
1440    nodes: &[TessellatedColoredSvgNode],
1441) -> TessellatedColoredSvgNode {
1442    TessellatedColoredSvgNode::default()
1443}
1444
1445#[cfg(feature = "svg")]
1446#[must_use] pub fn tessellate_node_fill(node: &SvgNode, fs: SvgFillStyle) -> TessellatedSvgNode {
1447    match &node {
1448        SvgNode::MultiPolygonCollection(ref mpc) => {
1449            let tessellated_multipolygons = mpc
1450                .as_ref()
1451                .iter()
1452                .map(|mp| tessellate_multi_polygon_fill(mp, fs))
1453                .collect::<Vec<_>>();
1454            join_tessellated_nodes(&tessellated_multipolygons)
1455        }
1456        SvgNode::MultiPolygon(ref mp) => tessellate_multi_polygon_fill(mp, fs),
1457        SvgNode::Path(ref p) => tessellate_path_fill(p, fs),
1458        SvgNode::Circle(ref c) => tessellate_circle_fill(c, fs),
1459        SvgNode::Rect(ref r) => tessellate_rect_fill(r, fs),
1460        SvgNode::MultiShape(ref r) => tessellate_multi_shape_fill(r.as_ref(), fs),
1461    }
1462}
1463
1464#[cfg(not(feature = "svg"))]
1465pub fn tessellate_node_fill(node: &SvgNode, fs: SvgFillStyle) -> TessellatedSvgNode {
1466    TessellatedSvgNode::default()
1467}
1468
1469#[cfg(feature = "svg")]
1470#[must_use] pub fn tessellate_node_stroke(node: &SvgNode, ss: SvgStrokeStyle) -> TessellatedSvgNode {
1471    match &node {
1472        SvgNode::MultiPolygonCollection(ref mpc) => {
1473            let tessellated_multipolygons = mpc
1474                .as_ref()
1475                .iter()
1476                .map(|mp| tessellate_multi_polygon_stroke(mp, ss))
1477                .collect::<Vec<_>>();
1478            join_tessellated_nodes(&tessellated_multipolygons)
1479        }
1480        SvgNode::MultiPolygon(ref mp) => tessellate_multi_polygon_stroke(mp, ss),
1481        SvgNode::Path(ref p) => tessellate_path_stroke(p, ss),
1482        SvgNode::Circle(ref c) => tessellate_circle_stroke(c, ss),
1483        SvgNode::Rect(ref r) => tessellate_rect_stroke(r, ss),
1484        SvgNode::MultiShape(ms) => tessellate_multi_shape_stroke(ms.as_ref(), ss),
1485    }
1486}
1487
1488#[cfg(not(feature = "svg"))]
1489pub fn tessellate_node_stroke(node: &SvgNode, ss: SvgStrokeStyle) -> TessellatedSvgNode {
1490    TessellatedSvgNode::default()
1491}
1492
1493// NOTE: This is a separate step both in order to reuse GPU textures
1494// and also because texture allocation is heavy and can be offloaded to a different thread
1495/// # Panics
1496///
1497/// Panics if the GL driver returned no texture id.
1498#[must_use] pub fn allocate_clipmask_texture(
1499    gl_context: GlContextPtr,
1500    size: PhysicalSizeU32,
1501    _background: ColorU,
1502) -> Texture {
1503    use azul_core::gl::TextureFlags;
1504
1505    let textures = gl_context.gen_textures(1);
1506    let texture_id = textures.get(0).unwrap();
1507
1508    Texture::create(
1509        *texture_id,
1510        TextureFlags {
1511            is_opaque: true,
1512            is_video_texture: false,
1513        },
1514        size,
1515        ColorU::TRANSPARENT,
1516        gl_context,
1517        RawImageFormat::R8,
1518    )
1519}
1520
1521/// Applies an FXAA filter to the texture using the pre-compiled FXAA shader.
1522///
1523/// Renders a fullscreen quad with the FXAA fragment shader, reading from
1524/// the input texture and writing to a temporary texture, then swaps the
1525/// texture IDs so the caller gets the post-FXAA result.
1526pub fn apply_fxaa(texture: &mut Texture) -> Option<()> {
1527    apply_fxaa_with_config(texture, &azul_core::gl_fxaa::FxaaConfig::enabled())
1528}
1529
1530/// Applies FXAA with custom configuration parameters.
1531#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_precision_loss, clippy::cast_sign_loss)] // bounded layout/render numeric cast
1532#[allow(clippy::similar_names)] // domain-standard coordinate/geometry/short-lived names
1533#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
1534pub fn apply_fxaa_with_config(
1535    texture: &mut Texture,
1536    config: &azul_core::gl_fxaa::FxaaConfig,
1537) -> Option<()> {
1538    use std::mem;
1539
1540    use azul_core::gl::{GLuint, GlVoidPtrConst, VertexAttributeType};
1541    use gl_context_loader::gl;
1542
1543    if !config.enabled || texture.size.width == 0 || texture.size.height == 0 {
1544        return Some(());
1545    }
1546
1547    // FXAA only works on RGBA8 textures
1548    if texture.format != RawImageFormat::RGBA8 {
1549        return Some(());
1550    }
1551
1552    let texture_size = texture.size;
1553    let gl_context = &texture.gl_context;
1554    let fxaa_shader = gl_context.get_fxaa_shader();
1555    let w = texture_size.width as f32;
1556    let h = texture_size.height as f32;
1557
1558    // Save GL state
1559    let mut current_program = [0_i32];
1560    let mut current_framebuffers = [0_i32];
1561    let mut current_texture_2d = [0_i32];
1562    let mut current_vertex_array_object = [0_i32];
1563    let mut current_vertex_buffer = [0_i32];
1564    let mut current_index_buffer = [0_i32];
1565    let mut current_active_texture = [0_i32];
1566    let mut current_blend_enabled = [0_u8];
1567    let mut current_viewport = [0_i32; 4];
1568
1569    gl_context.get_integer_v(gl::CURRENT_PROGRAM, (&mut current_program[..]).into());
1570    gl_context.get_integer_v(gl::FRAMEBUFFER, (&mut current_framebuffers[..]).into());
1571    gl_context.get_integer_v(gl::TEXTURE_2D, (&mut current_texture_2d[..]).into());
1572    gl_context.get_integer_v(
1573        gl::VERTEX_ARRAY_BINDING,
1574        (&mut current_vertex_array_object[..]).into(),
1575    );
1576    gl_context.get_integer_v(
1577        gl::ARRAY_BUFFER_BINDING,
1578        (&mut current_vertex_buffer[..]).into(),
1579    );
1580    gl_context.get_integer_v(
1581        gl::ELEMENT_ARRAY_BUFFER_BINDING,
1582        (&mut current_index_buffer[..]).into(),
1583    );
1584    gl_context.get_integer_v(
1585        gl::ACTIVE_TEXTURE,
1586        (&mut current_active_texture[..]).into(),
1587    );
1588    gl_context.get_boolean_v(gl::BLEND, (&mut current_blend_enabled[..]).into());
1589    gl_context.get_integer_v(gl::VIEWPORT, (&mut current_viewport[..]).into());
1590
1591    // 1. Create temporary output texture
1592    let temp_textures = gl_context.gen_textures(1);
1593    let temp_tex_id = *temp_textures.get(0)?;
1594    gl_context.bind_texture(gl::TEXTURE_2D, temp_tex_id);
1595    gl_context.tex_image_2d(
1596        gl::TEXTURE_2D,
1597        0,
1598        gl::RGBA as i32,
1599        texture_size.width as i32,
1600        texture_size.height as i32,
1601        0,
1602        gl::RGBA,
1603        gl::UNSIGNED_BYTE,
1604        None.into(),
1605    );
1606    gl_context.tex_parameter_i(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as i32);
1607    gl_context.tex_parameter_i(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as i32);
1608    gl_context.tex_parameter_i(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as i32);
1609    gl_context.tex_parameter_i(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as i32);
1610
1611    // 2. Create FBO targeting the temp texture
1612    let fbos = gl_context.gen_framebuffers(1);
1613    let fbo_id = *fbos.get(0)?;
1614    gl_context.bind_framebuffer(gl::FRAMEBUFFER, fbo_id);
1615    gl_context.framebuffer_texture_2d(
1616        gl::FRAMEBUFFER,
1617        gl::COLOR_ATTACHMENT0,
1618        gl::TEXTURE_2D,
1619        temp_tex_id,
1620        0,
1621    );
1622    gl_context.draw_buffers([gl::COLOR_ATTACHMENT0][..].into());
1623
1624    debug_assert!(
1625        gl_context.check_frame_buffer_status(gl::FRAMEBUFFER) == gl::FRAMEBUFFER_COMPLETE
1626    );
1627
1628    // 3. Create fullscreen quad VAO/VBO/IBO
1629    // Vertices in [-1, 1] range; the FXAA vertex shader converts to [0, 1] UVs
1630    let quad_vertices: [f32; 8] = [
1631        -1.0, -1.0, // bottom-left
1632         1.0, -1.0, // bottom-right
1633         1.0,  1.0, // top-right
1634        -1.0,  1.0, // top-left
1635    ];
1636    let quad_indices: [u32; 6] = [0, 1, 2, 0, 2, 3];
1637
1638    let vaos = gl_context.gen_vertex_arrays(1);
1639    let vao_id = *vaos.get(0)?;
1640    gl_context.bind_vertex_array(vao_id);
1641
1642    let vbos = gl_context.gen_buffers(1);
1643    let vbo_id = *vbos.get(0)?;
1644    gl_context.bind_buffer(gl::ARRAY_BUFFER, vbo_id);
1645    gl_context.buffer_data_untyped(
1646        gl::ARRAY_BUFFER,
1647        (size_of::<f32>() * quad_vertices.len()) as isize,
1648        GlVoidPtrConst {
1649            ptr: quad_vertices.as_ptr().cast::<c_void>(),
1650            run_destructor: true,
1651        },
1652        gl::STATIC_DRAW,
1653    );
1654
1655    let ibos = gl_context.gen_buffers(1);
1656    let ibo_id = *ibos.get(0)?;
1657    gl_context.bind_buffer(gl::ELEMENT_ARRAY_BUFFER, ibo_id);
1658    gl_context.buffer_data_untyped(
1659        gl::ELEMENT_ARRAY_BUFFER,
1660        (size_of::<u32>() * quad_indices.len()) as isize,
1661        GlVoidPtrConst {
1662            ptr: quad_indices.as_ptr().cast::<c_void>(),
1663            run_destructor: true,
1664        },
1665        gl::STATIC_DRAW,
1666    );
1667
1668    // Set up vertex attribute for vAttrXY (location 0, bound at shader compilation)
1669    let vertex_type = VertexAttributeType::Float;
1670    let stride = vertex_type.get_mem_size() * 2; // 2 floats per vertex (x, y)
1671    gl_context.vertex_attrib_pointer(0, 2, vertex_type.get_gl_id(), false, stride as i32, 0);
1672    gl_context.enable_vertex_attrib_array(0);
1673
1674    // 4. Render FXAA pass
1675    gl_context.use_program(fxaa_shader);
1676    gl_context.viewport(0, 0, texture_size.width as i32, texture_size.height as i32);
1677    gl_context.disable(gl::BLEND); // FXAA reads exact colors, blending would corrupt output
1678
1679    // Bind input texture to GL_TEXTURE0
1680    gl_context.active_texture(gl::TEXTURE0);
1681    gl_context.bind_texture(gl::TEXTURE_2D, texture.texture_id);
1682
1683    // Set uniforms
1684    let u_texture = gl_context.get_uniform_location(fxaa_shader, "uTexture");
1685    gl_context.uniform_1i(u_texture, 0);
1686
1687    let u_texel_size = gl_context.get_uniform_location(fxaa_shader, "uTexelSize");
1688    gl_context.uniform_2f(u_texel_size, 1.0 / w, 1.0 / h);
1689
1690    let u_edge_threshold =
1691        gl_context.get_uniform_location(fxaa_shader, "uEdgeThreshold");
1692    gl_context.uniform_1f(u_edge_threshold, config.edge_threshold);
1693
1694    let u_edge_threshold_min =
1695        gl_context.get_uniform_location(fxaa_shader, "uEdgeThresholdMin");
1696    gl_context.uniform_1f(u_edge_threshold_min, config.edge_threshold_min);
1697
1698    // Draw the fullscreen quad
1699    gl_context.draw_elements(gl::TRIANGLES, 6, gl::UNSIGNED_INT, 0);
1700
1701    // 5. Swap texture IDs: the temp texture now has the FXAA result.
1702    // We swap so the caller's texture_id points to the anti-aliased result,
1703    // and the old texture_id gets cleaned up.
1704    let old_texture_id = texture.texture_id;
1705    texture.texture_id = temp_tex_id;
1706    // Delete the old texture (which was the input)
1707    gl_context.delete_textures((&[old_texture_id])[..].into());
1708
1709    // 6. Cleanup: delete FBO, quad buffers
1710    gl_context.delete_framebuffers((&[fbo_id])[..].into());
1711    gl_context.disable_vertex_attrib_array(0);
1712    gl_context.delete_vertex_arrays((&[vao_id])[..].into());
1713    gl_context.delete_buffers((&[vbo_id, ibo_id])[..].into());
1714
1715    // Restore GL state
1716    gl_context.bind_framebuffer(gl::FRAMEBUFFER, current_framebuffers[0] as u32);
1717    gl_context.bind_texture(gl::TEXTURE_2D, current_texture_2d[0] as u32);
1718    gl_context.bind_vertex_array(current_vertex_array_object[0] as u32);
1719    gl_context.bind_buffer(gl::ELEMENT_ARRAY_BUFFER, current_index_buffer[0] as u32);
1720    gl_context.bind_buffer(gl::ARRAY_BUFFER, current_vertex_buffer[0] as u32);
1721    gl_context.use_program(current_program[0] as u32);
1722    gl_context.active_texture(current_active_texture[0] as u32);
1723    gl_context.viewport(
1724        current_viewport[0],
1725        current_viewport[1],
1726        current_viewport[2],
1727        current_viewport[3],
1728    );
1729    if u32::from(current_blend_enabled[0]) == gl::TRUE {
1730        gl_context.enable(gl::BLEND);
1731    }
1732
1733    Some(())
1734}
1735
1736#[cfg(feature = "svg")]
1737#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] // bounded layout/render numeric cast
1738#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
1739pub fn render_node_clipmask_cpu(
1740    image: &mut RawImage,
1741    node: &SvgNode,
1742    style: SvgStyle,
1743) -> Option<()> {
1744    use azul_core::resources::RawImageData;
1745    use agg_rust::{
1746        basics::{FillingRule, VertexSource, PATH_FLAGS_NONE},
1747        path_storage::PathStorage,
1748        color::Rgba8,
1749        conv_stroke::ConvStroke,
1750        conv_transform::ConvTransform,
1751        math_stroke::{LineCap, LineJoin},
1752        pixfmt_rgba::{PixfmtRgba32, PixelFormat},
1753        rasterizer_scanline_aa::RasterizerScanlineAa,
1754        renderer_base::RendererBase,
1755        renderer_scanline::render_scanlines_aa_solid,
1756        rendering_buffer::RowAccessor,
1757        scanline_u::ScanlineU8,
1758        trans_affine::TransAffine,
1759    };
1760
1761    #[allow(clippy::many_single_char_names)] // domain-standard coordinate/geometry/short-lived names
1762    #[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
1763    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
1764    fn agg_translate_node(node: &SvgNode) -> Option<PathStorage> {
1765        macro_rules! build_path {
1766            ($path:expr, $p:expr) => {{
1767                if $p.items.as_ref().is_empty() {
1768                    return None;
1769                }
1770
1771                let start = $p.items.as_ref()[0].get_start();
1772                $path.move_to(f64::from(start.x), f64::from(start.y));
1773
1774                for path_element in $p.items.as_ref() {
1775                    match path_element {
1776                        SvgPathElement::Line(l) => {
1777                            $path.line_to(f64::from(l.end.x), f64::from(l.end.y));
1778                        }
1779                        SvgPathElement::QuadraticCurve(qc) => {
1780                            $path.curve3(
1781                                f64::from(qc.ctrl.x), f64::from(qc.ctrl.y),
1782                                f64::from(qc.end.x), f64::from(qc.end.y),
1783                            );
1784                        }
1785                        SvgPathElement::CubicCurve(cc) => {
1786                            $path.curve4(
1787                                f64::from(cc.ctrl_1.x), f64::from(cc.ctrl_1.y),
1788                                f64::from(cc.ctrl_2.x), f64::from(cc.ctrl_2.y),
1789                                f64::from(cc.end.x), f64::from(cc.end.y),
1790                            );
1791                        }
1792                    }
1793                }
1794
1795                if $p.is_closed() {
1796                    $path.close_polygon(PATH_FLAGS_NONE);
1797                }
1798            }};
1799        }
1800
1801        let mut path = PathStorage::new();
1802        match node {
1803            SvgNode::MultiPolygonCollection(mpc) => {
1804                for mp in mpc {
1805                    for p in &mp.rings {
1806                        build_path!(path, p);
1807                    }
1808                }
1809            }
1810            SvgNode::MultiPolygon(mp) => {
1811                for p in &mp.rings {
1812                    build_path!(path, p);
1813                }
1814            }
1815            SvgNode::Path(p) => {
1816                build_path!(path, p);
1817            }
1818            SvgNode::Circle(c) => {
1819                // Approximate circle with 4 cubic beziers
1820                let cx = f64::from(c.center_x);
1821                let cy = f64::from(c.center_y);
1822                let r = f64::from(c.radius);
1823                let k = CIRCLE_BEZIER_KAPPA;
1824                let kr = k * r;
1825                path.move_to(cx + r, cy);
1826                path.curve4(cx + r, cy + kr, cx + kr, cy + r, cx, cy + r);
1827                path.curve4(cx - kr, cy + r, cx - r, cy + kr, cx - r, cy);
1828                path.curve4(cx - r, cy - kr, cx - kr, cy - r, cx, cy - r);
1829                path.curve4(cx + kr, cy - r, cx + r, cy - kr, cx + r, cy);
1830                path.close_polygon(PATH_FLAGS_NONE);
1831            }
1832            SvgNode::Rect(r) => {
1833                let x = f64::from(r.x);
1834                let y = f64::from(r.y);
1835                let w = f64::from(r.width);
1836                let h = f64::from(r.height);
1837                path.move_to(x, y);
1838                path.line_to(x + w, y);
1839                path.line_to(x + w, y + h);
1840                path.line_to(x, y + h);
1841                path.close_polygon(PATH_FLAGS_NONE);
1842            }
1843            SvgNode::MultiShape(ms) => {
1844                for p in ms.as_ref() {
1845                    match p {
1846                        SvgSimpleNode::Path(p) => {
1847                            build_path!(path, p);
1848                        }
1849                        SvgSimpleNode::Rect(r) => {
1850                            let x = f64::from(r.x);
1851                            let y = f64::from(r.y);
1852                            let w = f64::from(r.width);
1853                            let h = f64::from(r.height);
1854                            path.move_to(x, y);
1855                            path.line_to(x + w, y);
1856                            path.line_to(x + w, y + h);
1857                            path.line_to(x, y + h);
1858                            path.close_polygon(PATH_FLAGS_NONE);
1859                        }
1860                        SvgSimpleNode::Circle(c) | SvgSimpleNode::CircleHole(c) => {
1861                            let cx = f64::from(c.center_x);
1862                            let cy = f64::from(c.center_y);
1863                            let r = f64::from(c.radius);
1864                            let k = CIRCLE_BEZIER_KAPPA;
1865                            let kr = k * r;
1866                            path.move_to(cx + r, cy);
1867                            path.curve4(cx + r, cy + kr, cx + kr, cy + r, cx, cy + r);
1868                            path.curve4(cx - kr, cy + r, cx - r, cy + kr, cx - r, cy);
1869                            path.curve4(cx - r, cy - kr, cx - kr, cy - r, cx, cy - r);
1870                            path.curve4(cx + kr, cy - r, cx + r, cy - kr, cx + r, cy);
1871                            path.close_polygon(PATH_FLAGS_NONE);
1872                        }
1873                        SvgSimpleNode::RectHole(r) => {
1874                            let x = f64::from(r.x);
1875                            let y = f64::from(r.y);
1876                            let w = f64::from(r.width);
1877                            let h = f64::from(r.height);
1878                            path.move_to(x, y);
1879                            path.line_to(x + w, y);
1880                            path.line_to(x + w, y + h);
1881                            path.line_to(x, y + h);
1882                            path.close_polygon(PATH_FLAGS_NONE);
1883                        }
1884                    }
1885                }
1886            }
1887        }
1888        if path.total_vertices() == 0 {
1889            return None;
1890        }
1891        Some(path)
1892    }
1893
1894    let w = image.width as u32;
1895    let h = image.height as u32;
1896    if w == 0 || h == 0 {
1897        return None;
1898    }
1899
1900    let transform_data = style.get_transform();
1901    let transform = TransAffine::new_custom(
1902        f64::from(transform_data.sx),
1903        f64::from(transform_data.ky),
1904        f64::from(transform_data.kx),
1905        f64::from(transform_data.sy),
1906        f64::from(transform_data.tx),
1907        f64::from(transform_data.ty),
1908    );
1909
1910    let mut agg_path = agg_translate_node(node)?;
1911    let white = Rgba8::new(255, 255, 255, 255);
1912
1913    // Create pixel buffer and render
1914    let mut buf = vec![0u8; (w as usize) * (h as usize) * 4];
1915    let stride = (w * 4) as i32;
1916    let mut ra = unsafe { RowAccessor::new_with_buf(buf.as_mut_ptr(), w, h, stride) };
1917    let mut pf = PixfmtRgba32::new(&mut ra);
1918    let mut rb = RendererBase::new(pf);
1919    let mut ras = RasterizerScanlineAa::new();
1920    let mut sl = ScanlineU8::new();
1921
1922    match style {
1923        SvgStyle::Fill(fs) => {
1924            ras.filling_rule(match fs.fill_rule {
1925                SvgFillRule::Winding => FillingRule::NonZero,
1926                SvgFillRule::EvenOdd => FillingRule::EvenOdd,
1927            });
1928            if transform.is_identity(0.0001) {
1929                ras.add_path(&mut agg_path, 0);
1930            } else {
1931                let mut transformed = ConvTransform::new(&mut agg_path, transform);
1932                ras.add_path(&mut transformed, 0);
1933            }
1934            render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &white);
1935        }
1936        SvgStyle::Stroke(ss) => {
1937            let mut stroke = ConvStroke::new(agg_path);
1938            stroke.set_width(f64::from(ss.line_width));
1939            stroke.set_miter_limit(f64::from(ss.miter_limit));
1940            stroke.set_line_cap(match ss.start_cap {
1941                SvgLineCap::Butt => LineCap::Butt,
1942                SvgLineCap::Square => LineCap::Square,
1943                SvgLineCap::Round => LineCap::Round,
1944            });
1945            stroke.set_line_join(match ss.line_join {
1946                SvgLineJoin::Miter | SvgLineJoin::MiterClip => LineJoin::Miter,
1947                SvgLineJoin::Round => LineJoin::Round,
1948                SvgLineJoin::Bevel => LineJoin::Bevel,
1949            });
1950            if transform.is_identity(0.0001) {
1951                ras.add_path(&mut stroke, 0);
1952            } else {
1953                let mut transformed = ConvTransform::new(&mut stroke, transform);
1954                ras.add_path(&mut transformed, 0);
1955            }
1956            render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &white);
1957        }
1958    }
1959
1960    // Extract red channel from RGBA buffer
1961    let red_channel = buf
1962        .chunks_exact(4)
1963        .map(|r| r[0])
1964        .collect::<Vec<_>>();
1965
1966    image.premultiplied_alpha = true;
1967    image.pixels = RawImageData::U8(red_channel.into());
1968    image.data_format = RawImageFormat::R8;
1969
1970    Some(())
1971}
1972
1973#[cfg(not(feature = "svg"))]
1974pub fn render_node_clipmask_cpu(
1975    image: &mut RawImage,
1976    node: &SvgNode,
1977    style: SvgStyle,
1978) -> Option<()> {
1979    None
1980}
1981
1982// ============================================================================
1983// Boolean operations on SvgMultiPolygon — via agg scanline boolean algebra
1984// ============================================================================
1985
1986/// Rasterize an `SvgMultiPolygon` into an agg `RasterizerScanlineAa`.
1987fn rasterize_multi_polygon(mp: &SvgMultiPolygon) -> agg_rust::rasterizer_scanline_aa::RasterizerScanlineAa {
1988    use agg_rust::{
1989        basics::{FillingRule, PATH_FLAGS_NONE},
1990        path_storage::PathStorage,
1991        rasterizer_scanline_aa::RasterizerScanlineAa,
1992    };
1993
1994    let mut ras = RasterizerScanlineAa::new();
1995    ras.filling_rule(FillingRule::NonZero);
1996
1997    let mut path = PathStorage::new();
1998    for ring in mp.rings.as_ref() {
1999        let mut first = true;
2000        for item in ring.items.as_ref() {
2001            match item {
2002                SvgPathElement::Line(l) => {
2003                    if first {
2004                        path.move_to(f64::from(l.start.x), f64::from(l.start.y));
2005                        first = false;
2006                    }
2007                    path.line_to(f64::from(l.end.x), f64::from(l.end.y));
2008                }
2009                SvgPathElement::QuadraticCurve(q) => {
2010                    if first {
2011                        path.move_to(f64::from(q.start.x), f64::from(q.start.y));
2012                        first = false;
2013                    }
2014                    path.curve3(f64::from(q.ctrl.x), f64::from(q.ctrl.y), f64::from(q.end.x), f64::from(q.end.y));
2015                }
2016                SvgPathElement::CubicCurve(c) => {
2017                    if first {
2018                        path.move_to(f64::from(c.start.x), f64::from(c.start.y));
2019                        first = false;
2020                    }
2021                    path.curve4(
2022                        f64::from(c.ctrl_1.x), f64::from(c.ctrl_1.y),
2023                        f64::from(c.ctrl_2.x), f64::from(c.ctrl_2.y),
2024                        f64::from(c.end.x), f64::from(c.end.y),
2025                    );
2026                }
2027            }
2028        }
2029        path.close_polygon(PATH_FLAGS_NONE);
2030    }
2031    ras.add_path(&mut path, 0);
2032    ras
2033}
2034
2035/// Extract polygon contours from a `ScanlineStorageAa` by tracing
2036/// horizontal span edges across consecutive scanlines.
2037///
2038/// For each row, we collect the solid spans (coverage > 128). Then we
2039/// trace left/right boundaries of connected span groups into closed
2040/// polygons (go down on the left edge, come back up on the right edge).
2041#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_precision_loss, clippy::cast_sign_loss)] // bounded layout/render numeric cast
2042fn storage_to_multi_polygon(
2043    storage: &mut agg_rust::scanline_storage_aa::ScanlineStorageAa,
2044) -> SvgMultiPolygon {
2045    use agg_rust::rasterizer_scanline_aa::Scanline;
2046    use azul_css::props::basic::SvgPoint;
2047
2048    // Collect solid spans per row
2049    let mut rows: Vec<(i32, Vec<(i32, i32)>)> = Vec::new(); // (y, [(x_start, x_end)])
2050
2051    let mut sl = agg_rust::scanline_u::ScanlineU8::new();
2052    if storage.rewind_scanlines() {
2053        sl.reset(storage.min_x(), storage.max_x());
2054        while storage.sweep_scanline(&mut sl) {
2055            let y = Scanline::y(&sl);
2056            let mut row_spans: Vec<(i32, i32)> = Vec::new();
2057            for span in sl.begin() {
2058                // Span with positive len: per-pixel coverage
2059                let len = span.len;
2060                if len <= 0 { continue; }
2061                // Check if any pixel in the span has enough coverage
2062                let covers = sl.covers();
2063                let mut x_start = None;
2064                for j in 0..len as usize {
2065                    let cov = covers.get(span.cover_offset + j).copied().unwrap_or(0);
2066                    if cov > 128 {
2067                        if x_start.is_none() { x_start = Some(span.x + j as i32); }
2068                    } else if let Some(xs) = x_start.take() {
2069                        row_spans.push((xs, span.x + j as i32));
2070                    }
2071                }
2072                if let Some(xs) = x_start {
2073                    row_spans.push((xs, span.x + len));
2074                }
2075            }
2076            if !row_spans.is_empty() {
2077                rows.push((y, row_spans));
2078            }
2079        }
2080    }
2081
2082    if rows.is_empty() {
2083        return SvgMultiPolygon { rings: SvgPathVec::from_const_slice(&[]) };
2084    }
2085
2086    // Simple contour extraction: for each row, create horizontal line segments.
2087    // Then connect consecutive rows into closed polygons.
2088    // This produces axis-aligned polygons (staircase approximation).
2089    let mut rings = Vec::new();
2090
2091    for (y, spans) in &rows {
2092        let yf = *y as f32;
2093        for &(x0, x1) in spans {
2094            let x0f = x0 as f32;
2095            let x1f = x1 as f32;
2096            // Create a small horizontal rectangle for this span
2097            let elements = vec![
2098                SvgPathElement::Line(SvgLine::new(
2099                    SvgPoint { x: x0f, y: yf },
2100                    SvgPoint { x: x1f, y: yf },
2101                )),
2102                SvgPathElement::Line(SvgLine::new(
2103                    SvgPoint { x: x1f, y: yf },
2104                    SvgPoint { x: x1f, y: yf + 1.0 },
2105                )),
2106                SvgPathElement::Line(SvgLine::new(
2107                    SvgPoint { x: x1f, y: yf + 1.0 },
2108                    SvgPoint { x: x0f, y: yf + 1.0 },
2109                )),
2110                SvgPathElement::Line(SvgLine::new(
2111                    SvgPoint { x: x0f, y: yf + 1.0 },
2112                    SvgPoint { x: x0f, y: yf },
2113                )),
2114            ];
2115            rings.push(SvgPath { items: SvgPathElementVec::from_vec(elements) });
2116        }
2117    }
2118
2119    SvgMultiPolygon { rings: SvgPathVec::from_vec(rings) }
2120}
2121
2122/// Perform a boolean operation on two `SvgMultiPolygon` shapes using agg scanline algebra.
2123fn svg_bool_op(
2124    a: &SvgMultiPolygon,
2125    b: &SvgMultiPolygon,
2126    op: agg_rust::scanline_boolean_algebra::SBoolOp,
2127) -> SvgMultiPolygon {
2128    use agg_rust::{
2129        scanline_boolean_algebra::sbool_combine_shapes_aa,
2130        scanline_storage_aa::ScanlineStorageAa,
2131        scanline_u::ScanlineU8,
2132    };
2133
2134    let mut ras1 = rasterize_multi_polygon(a);
2135    let mut ras2 = rasterize_multi_polygon(b);
2136
2137    let mut sl1 = ScanlineU8::new();
2138    let mut sl2 = ScanlineU8::new();
2139    let mut sl_result = ScanlineU8::new();
2140    let mut storage1 = ScanlineStorageAa::new();
2141    let mut storage2 = ScanlineStorageAa::new();
2142    let mut storage_result = ScanlineStorageAa::new();
2143
2144    sbool_combine_shapes_aa(
2145        op,
2146        &mut ras1, &mut ras2,
2147        &mut sl1, &mut sl2, &mut sl_result,
2148        &mut storage1, &mut storage2, &mut storage_result,
2149    );
2150
2151    storage_to_multi_polygon(&mut storage_result)
2152}
2153
2154#[must_use] pub fn svg_multi_polygon_union(a: &SvgMultiPolygon, b: &SvgMultiPolygon) -> SvgMultiPolygon {
2155    svg_bool_op(a, b, agg_rust::scanline_boolean_algebra::SBoolOp::Or)
2156}
2157
2158// FFI by-value variant: `b` is taken owned to mirror the exported api.json signature.
2159#[allow(clippy::needless_pass_by_value)]
2160#[must_use] pub fn svg_multi_polygon_union_byval(a: &SvgMultiPolygon, b: SvgMultiPolygon) -> SvgMultiPolygon {
2161    svg_multi_polygon_union(a, &b)
2162}
2163
2164#[must_use] pub fn svg_multi_polygon_intersection(a: &SvgMultiPolygon, b: &SvgMultiPolygon) -> SvgMultiPolygon {
2165    svg_bool_op(a, b, agg_rust::scanline_boolean_algebra::SBoolOp::And)
2166}
2167
2168// FFI by-value variant: `b` is taken owned to mirror the exported api.json signature.
2169#[allow(clippy::needless_pass_by_value)]
2170#[must_use] pub fn svg_multi_polygon_intersection_byval(
2171    a: &SvgMultiPolygon, b: SvgMultiPolygon,
2172) -> SvgMultiPolygon {
2173    svg_multi_polygon_intersection(a, &b)
2174}
2175
2176#[must_use] pub fn svg_multi_polygon_difference(a: &SvgMultiPolygon, b: &SvgMultiPolygon) -> SvgMultiPolygon {
2177    svg_bool_op(a, b, agg_rust::scanline_boolean_algebra::SBoolOp::AMinusB)
2178}
2179
2180// FFI by-value variant: `b` is taken owned to mirror the exported api.json signature.
2181#[allow(clippy::needless_pass_by_value)]
2182#[must_use] pub fn svg_multi_polygon_difference_byval(
2183    a: &SvgMultiPolygon, b: SvgMultiPolygon,
2184) -> SvgMultiPolygon {
2185    svg_multi_polygon_difference(a, &b)
2186}
2187
2188#[must_use] pub fn svg_multi_polygon_xor(a: &SvgMultiPolygon, b: &SvgMultiPolygon) -> SvgMultiPolygon {
2189    svg_bool_op(a, b, agg_rust::scanline_boolean_algebra::SBoolOp::Xor)
2190}
2191
2192// FFI by-value variant: `b` is taken owned to mirror the exported api.json signature.
2193#[allow(clippy::needless_pass_by_value)]
2194#[must_use] pub fn svg_multi_polygon_xor_byval(a: &SvgMultiPolygon, b: SvgMultiPolygon) -> SvgMultiPolygon {
2195    svg_multi_polygon_xor(a, &b)
2196}
2197
2198// ============================================================================
2199// SVG Rendering — ParsedSvg wraps the XML tree, no usvg
2200// ============================================================================
2201
2202/// Parsed SVG document — wraps the XML node tree.
2203///
2204/// Previously wrapped `usvg::Tree`; now stores our own `XmlNode` tree parsed via xmlparser.
2205/// Rendering uses the agg-rust pipeline in `cpurender::render_svg_to_png()`.
2206#[derive(Debug, Clone)]
2207#[repr(C)]
2208pub struct ParsedSvgXmlNode {
2209    pub run_destructor: bool,
2210}
2211
2212impl Drop for ParsedSvgXmlNode {
2213    fn drop(&mut self) { self.run_destructor = false; }
2214}
2215
2216/// # Errors
2217///
2218/// Returns a `SvgParseError` if the input is not valid SVG.
2219pub fn svgxmlnode_parse(
2220    svg_file_data: &[u8],
2221    _options: SvgParseOptions,
2222) -> Result<ParsedSvgXmlNode, SvgParseError> {
2223    // Verify we can parse the XML
2224    let s = core::str::from_utf8(svg_file_data)
2225        .map_err(|_| SvgParseError::NotAnUtf8Str)?;
2226    let _nodes = crate::xml::parse_xml_string(s)
2227        .map_err(|_| SvgParseError::NoParserAvailable)?;
2228    Ok(ParsedSvgXmlNode { run_destructor: true })
2229}
2230
2231/// Parsed SVG document. Stores the raw SVG bytes for deferred rendering.
2232#[derive(Clone)]
2233#[repr(C)]
2234pub struct ParsedSvg {
2235    pub svg_data: azul_css::U8Vec,
2236    pub run_destructor: bool,
2237}
2238
2239impl Drop for ParsedSvg {
2240    fn drop(&mut self) { self.run_destructor = false; }
2241}
2242
2243impl_result!(
2244    ParsedSvg,
2245    SvgParseError,
2246    ResultParsedSvgSvgParseError,
2247    copy = false,
2248    [Debug, Clone]
2249);
2250
2251impl From<ParsedSvg> for azul_core::svg::Svg {
2252    fn from(_parsed: ParsedSvg) -> Self {
2253        Self {
2254            tree: core::ptr::null(),
2255            run_destructor: false,
2256        }
2257    }
2258}
2259
2260impl fmt::Debug for ParsedSvg {
2261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2262        write!(f, "ParsedSvg({} bytes)", self.svg_data.as_ref().len())
2263    }
2264}
2265
2266impl ParsedSvg {
2267    /// # Errors
2268    ///
2269    /// Returns a `SvgParseError` if the input is not valid SVG.
2270    pub fn from_string(
2271        svg_string: &str,
2272        parse_options: SvgParseOptions,
2273    ) -> Result<Self, SvgParseError> {
2274        svg_parse(svg_string.as_bytes(), parse_options)
2275    }
2276
2277    /// # Errors
2278    ///
2279    /// Returns a `SvgParseError` if the input is not valid SVG.
2280    pub fn from_bytes(
2281        svg_bytes: &[u8],
2282        parse_options: SvgParseOptions,
2283    ) -> Result<Self, SvgParseError> {
2284        svg_parse(svg_bytes, parse_options)
2285    }
2286
2287    #[must_use] pub const fn get_root(&self) -> ParsedSvgXmlNode {
2288        svg_root(self)
2289    }
2290
2291    #[must_use] pub fn render(&self, options: SvgRenderOptions) -> Option<RawImage> {
2292        svg_render(self, options)
2293    }
2294
2295    #[must_use] pub fn to_string(&self, _options: SvgXmlOptions) -> String {
2296        String::from_utf8_lossy(self.svg_data.as_ref()).into_owned()
2297    }
2298}
2299
2300/// Parse SVG data into a `ParsedSvg` (validates XML, stores bytes for deferred rendering).
2301/// # Errors
2302///
2303/// Returns a `SvgParseError` if the input is not valid SVG.
2304pub fn svg_parse(
2305    svg_file_data: &[u8],
2306    _options: SvgParseOptions,
2307) -> Result<ParsedSvg, SvgParseError> {
2308    // Validate that it's parseable XML
2309    let s = core::str::from_utf8(svg_file_data)
2310        .map_err(|_| SvgParseError::NotAnUtf8Str)?;
2311    let _nodes = crate::xml::parse_xml_string(s)
2312        .map_err(|_| SvgParseError::NoParserAvailable)?;
2313    Ok(ParsedSvg {
2314        svg_data: svg_file_data.to_vec().into(),
2315        run_destructor: true,
2316    })
2317}
2318
2319#[must_use] pub const fn svg_root(s: &ParsedSvg) -> ParsedSvgXmlNode {
2320    ParsedSvgXmlNode { run_destructor: true }
2321}
2322
2323/// Render a `ParsedSvg` to a `RawImage` using the agg-rust pipeline.
2324///
2325/// Requires the `cpurender` feature (the agg-rust + png rasterization pipeline).
2326/// Without it, SVG parsing/layout still work but rasterizing yields `None`.
2327#[cfg(feature = "cpurender")]
2328#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // bounded layout/render numeric cast
2329#[must_use] pub fn svg_render(s: &ParsedSvg, options: SvgRenderOptions) -> Option<RawImage> {
2330    use azul_core::resources::RawImageData;
2331
2332    let (target_width, target_height) = options.target_size.as_ref().map_or(DEFAULT_SVG_RENDER_SIZE, |s| (s.width as u32, s.height as u32));
2333
2334    if target_width == 0 || target_height == 0 {
2335        return None;
2336    }
2337
2338    let png_data = crate::cpurender::render_svg_to_png(s.svg_data.as_ref(), target_width, target_height).ok()?;
2339
2340    // Decode PNG back to raw RGBA (TODO: render_svg_to_rgba to avoid PNG round-trip)
2341    let decoder = png::Decoder::new(std::io::Cursor::new(&png_data));
2342    let mut reader = decoder.read_info().ok()?;
2343    let mut buf = vec![0u8; reader.output_buffer_size()?];
2344    let info = reader.next_frame(&mut buf).ok()?;
2345    buf.truncate(info.buffer_size());
2346
2347    Some(RawImage {
2348        tag: Vec::new().into(),
2349        pixels: RawImageData::U8(buf.into()),
2350        width: info.width as usize,
2351        height: info.height as usize,
2352        premultiplied_alpha: false,
2353        data_format: RawImageFormat::RGBA8,
2354    })
2355}
2356
2357/// `cpurender`-less stub: SVG rasterization needs the agg-rust pipeline, so
2358/// without that feature there is nothing to render to. Parsing and layout are
2359/// unaffected — only the raster output is unavailable.
2360#[cfg(not(feature = "cpurender"))]
2361pub fn svg_render(_s: &ParsedSvg, _options: SvgRenderOptions) -> Option<RawImage> {
2362    None
2363}
2364
2365#[must_use] pub fn svg_to_string(s: &ParsedSvg, _options: SvgXmlOptions) -> String {
2366    String::from_utf8_lossy(s.svg_data.as_ref()).into_owned()
2367}
2368
2369// ============================================================================
2370// Lyon tessellation (kept — no usvg dependency)
2371// ============================================================================
2372
2373/// Trait for tessellating `SvgMultiPolygon` shapes
2374pub trait SvgMultiPolygonTessellation {
2375    fn tessellate_fill(&self, fill_style: SvgFillStyle) -> TessellatedSvgNode;
2376    fn tessellate_stroke(&self, stroke_style: SvgStrokeStyle) -> TessellatedSvgNode;
2377}
2378
2379impl SvgMultiPolygonTessellation for SvgMultiPolygon {
2380    fn tessellate_fill(&self, fill_style: SvgFillStyle) -> TessellatedSvgNode {
2381        tessellate_multi_polygon_fill(self, fill_style)
2382    }
2383    fn tessellate_stroke(&self, stroke_style: SvgStrokeStyle) -> TessellatedSvgNode {
2384        tessellate_multi_polygon_stroke(self, stroke_style)
2385    }
2386}
2387
2388// ============================================================================
2389// Adversarial unit tests
2390// ============================================================================
2391//
2392// The whole module is `#[cfg(feature = "svg")]`, so the `#[cfg(not(feature =
2393// "svg"))]` stubs above are never compiled and are therefore not exercised here.
2394// `cpurender` is a separate flag and is gated per-test.
2395
2396#[cfg(test)]
2397mod autotest_generated {
2398    use azul_core::resources::RawImageData;
2399
2400    use super::*;
2401
2402    // ------------------------------------------------------------------
2403    // helpers
2404    // ------------------------------------------------------------------
2405
2406    fn pt(x: f32, y: f32) -> SvgPoint {
2407        SvgPoint { x, y }
2408    }
2409
2410    fn ln(x0: f32, y0: f32, x1: f32, y1: f32) -> SvgLine {
2411        SvgLine {
2412            start: pt(x0, y0),
2413            end: pt(x1, y1),
2414        }
2415    }
2416
2417    fn mk_path(items: Vec<SvgPathElement>) -> SvgPath {
2418        SvgPath {
2419            items: SvgPathElementVec::from_vec(items),
2420        }
2421    }
2422
2423    fn empty_path() -> SvgPath {
2424        SvgPath {
2425            items: SvgPathElementVec::from_const_slice(&[]),
2426        }
2427    }
2428
2429    /// A closed, axis-aligned 10x10 square whose lower-left corner is at `d`.
2430    fn square_at(d: f32) -> SvgPath {
2431        mk_path(vec![
2432            SvgPathElement::Line(ln(d, d, d + 10.0, d)),
2433            SvgPathElement::Line(ln(d + 10.0, d, d + 10.0, d + 10.0)),
2434            SvgPathElement::Line(ln(d + 10.0, d + 10.0, d, d + 10.0)),
2435            SvgPathElement::Line(ln(d, d + 10.0, d, d)),
2436        ])
2437    }
2438
2439    fn square_path() -> SvgPath {
2440        square_at(0.0)
2441    }
2442
2443    fn polygon_of(rings: Vec<SvgPath>) -> SvgMultiPolygon {
2444        SvgMultiPolygon {
2445            rings: SvgPathVec::from_vec(rings),
2446        }
2447    }
2448
2449    fn square_polygon() -> SvgMultiPolygon {
2450        polygon_of(vec![square_path()])
2451    }
2452
2453    fn empty_polygon() -> SvgMultiPolygon {
2454        SvgMultiPolygon {
2455            rings: SvgPathVec::from_const_slice(&[]),
2456        }
2457    }
2458
2459    fn rect(x: f32, y: f32, w: f32, h: f32) -> SvgRect {
2460        SvgRect {
2461            width: w,
2462            height: h,
2463            x,
2464            y,
2465            ..SvgRect::default()
2466        }
2467    }
2468
2469    /// `SvgTransform::default()` is the **zero** matrix (every field is `0.0`),
2470    /// which collapses all geometry onto the origin — tests that want a real
2471    /// 1:1 mapping must build the identity by hand.
2472    const fn identity_transform() -> SvgTransform {
2473        SvgTransform {
2474            sx: 1.0,
2475            kx: 0.0,
2476            ky: 0.0,
2477            sy: 1.0,
2478            tx: 0.0,
2479            ty: 0.0,
2480        }
2481    }
2482
2483    fn tess(vertices: &[(f32, f32)], indices: &[u32]) -> TessellatedSvgNode {
2484        TessellatedSvgNode {
2485            vertices: vertices
2486                .iter()
2487                .map(|&(x, y)| SvgVertex { x, y })
2488                .collect::<Vec<_>>()
2489                .into(),
2490            indices: indices.to_vec().into(),
2491        }
2492    }
2493
2494    fn mask_image(w: usize, h: usize) -> RawImage {
2495        RawImage {
2496            pixels: RawImageData::U8(vec![0u8; w * h].into()),
2497            width: w,
2498            height: h,
2499            premultiplied_alpha: false,
2500            data_format: RawImageFormat::R8,
2501            tag: Vec::new().into(),
2502        }
2503    }
2504
2505    fn mask_bytes(img: &RawImage) -> Vec<u8> {
2506        match &img.pixels {
2507            RawImageData::U8(v) => v.as_ref().to_vec(),
2508            _ => panic!("a clip mask must always come back as 8-bit data"),
2509        }
2510    }
2511
2512    /// Every index must address a vertex that actually exists.
2513    fn assert_indices_in_range(t: &TessellatedSvgNode) {
2514        let verts = t.vertices.as_ref().len();
2515        for i in t.indices.as_ref() {
2516            if *i == GL_RESTART_INDEX {
2517                continue;
2518            }
2519            assert!(
2520                (*i as usize) < verts,
2521                "index {i} points past a {verts}-vertex buffer"
2522            );
2523        }
2524    }
2525
2526    const MINIMAL_SVG: &[u8] =
2527        br#"<svg viewBox="0 0 8 8"><rect x="0" y="0" width="8" height="8" fill="red"/></svg>"#;
2528
2529    // ==================================================================
2530    // translate_svg_line_join / translate_svg_line_cap
2531    // ==================================================================
2532
2533    #[test]
2534    fn translate_svg_line_join_maps_every_variant() {
2535        use lyon::tessellation::LineJoin as L;
2536        assert_eq!(translate_svg_line_join(SvgLineJoin::Miter), L::Miter);
2537        assert_eq!(translate_svg_line_join(SvgLineJoin::MiterClip), L::MiterClip);
2538        assert_eq!(translate_svg_line_join(SvgLineJoin::Round), L::Round);
2539        assert_eq!(translate_svg_line_join(SvgLineJoin::Bevel), L::Bevel);
2540        assert_eq!(translate_svg_line_join(SvgLineJoin::default()), L::Miter);
2541    }
2542
2543    #[test]
2544    fn translate_svg_line_cap_maps_every_variant() {
2545        use lyon::tessellation::LineCap as C;
2546        assert_eq!(translate_svg_line_cap(SvgLineCap::Butt), C::Butt);
2547        assert_eq!(translate_svg_line_cap(SvgLineCap::Square), C::Square);
2548        assert_eq!(translate_svg_line_cap(SvgLineCap::Round), C::Round);
2549        assert_eq!(translate_svg_line_cap(SvgLineCap::default()), C::Butt);
2550    }
2551
2552    // ==================================================================
2553    // translate_svg_stroke_style
2554    // ==================================================================
2555
2556    #[test]
2557    fn translate_svg_stroke_style_carries_every_field_across() {
2558        let s = SvgStrokeStyle {
2559            start_cap: SvgLineCap::Round,
2560            end_cap: SvgLineCap::Square,
2561            line_join: SvgLineJoin::Bevel,
2562            line_width: 3.5,
2563            miter_limit: 7.25,
2564            tolerance: 0.25,
2565            ..SvgStrokeStyle::default()
2566        };
2567        let o = translate_svg_stroke_style(s);
2568        assert_eq!(o.start_cap, lyon::tessellation::LineCap::Round);
2569        assert_eq!(o.end_cap, lyon::tessellation::LineCap::Square);
2570        assert_eq!(o.line_join, lyon::tessellation::LineJoin::Bevel);
2571        assert!((o.line_width - 3.5).abs() < 1e-6, "{o:?}");
2572        assert!((o.miter_limit - 7.25).abs() < 1e-6, "{o:?}");
2573        assert!((o.tolerance - 0.25).abs() < 1e-6, "{o:?}");
2574    }
2575
2576    #[test]
2577    fn translate_svg_stroke_style_accepts_the_azul_default() {
2578        let o = translate_svg_stroke_style(SvgStrokeStyle::default());
2579        assert!(o.miter_limit >= 1.0, "the default must clear lyon's assert");
2580    }
2581
2582    // FINDING (pinned): lyon's `StrokeOptions::with_miter_limit` is a *hard*
2583    // `assert!(limit >= 1.0)` (not a debug assert), so an `SvgStrokeStyle` whose
2584    // miter_limit is below 1.0 aborts every stroke entry point instead of being
2585    // clamped. `SvgStrokeStyle` accepts such a value without complaint.
2586    #[test]
2587    #[should_panic(expected = "limit")]
2588    fn translate_svg_stroke_style_miter_limit_below_one_panics() {
2589        let s = SvgStrokeStyle {
2590            miter_limit: 0.0,
2591            ..SvgStrokeStyle::default()
2592        };
2593        let _ = translate_svg_stroke_style(s);
2594    }
2595
2596    #[test]
2597    #[should_panic(expected = "limit")]
2598    fn translate_svg_stroke_style_nan_miter_limit_panics() {
2599        let s = SvgStrokeStyle {
2600            miter_limit: f32::NAN,
2601            ..SvgStrokeStyle::default()
2602        };
2603        let _ = translate_svg_stroke_style(s);
2604    }
2605
2606    // ==================================================================
2607    // raw_line_intersection / raw_line_intersection_byval
2608    // ==================================================================
2609
2610    #[test]
2611    fn raw_line_intersection_crossing_diagonals_meet_in_the_middle() {
2612        let i = raw_line_intersection(&ln(0.0, 0.0, 10.0, 10.0), &ln(0.0, 10.0, 10.0, 0.0))
2613            .expect("crossing diagonals intersect");
2614        assert!((i.x - 5.0).abs() < 1e-4, "{i:?}");
2615        assert!((i.y - 5.0).abs() < 1e-4, "{i:?}");
2616    }
2617
2618    #[test]
2619    fn raw_line_intersection_parallel_lines_are_none() {
2620        assert_eq!(
2621            raw_line_intersection(&ln(0.0, 0.0, 10.0, 0.0), &ln(0.0, 5.0, 10.0, 5.0)),
2622            None
2623        );
2624    }
2625
2626    #[test]
2627    fn raw_line_intersection_identical_lines_are_none() {
2628        // Collinear overlap has infinitely many solutions -> 0/0 -> NaN -> None.
2629        let l = ln(0.0, 0.0, 10.0, 0.0);
2630        assert_eq!(raw_line_intersection(&l, &l), None);
2631    }
2632
2633    #[test]
2634    fn raw_line_intersection_zero_length_lines_are_none() {
2635        assert_eq!(
2636            raw_line_intersection(&ln(1.0, 1.0, 1.0, 1.0), &ln(0.0, 0.0, 2.0, 2.0)),
2637            None
2638        );
2639        assert_eq!(
2640            raw_line_intersection(&ln(0.0, 0.0, 0.0, 0.0), &ln(0.0, 0.0, 0.0, 0.0)),
2641            None
2642        );
2643    }
2644
2645    #[test]
2646    fn raw_line_intersection_never_leaks_nan_for_extreme_inputs() {
2647        let extremes = [
2648            ln(f32::NAN, 0.0, 1.0, 1.0),
2649            ln(0.0, f32::NAN, 1.0, 1.0),
2650            ln(f32::INFINITY, f32::INFINITY, 1.0, 1.0),
2651            ln(f32::NEG_INFINITY, 0.0, f32::INFINITY, 0.0),
2652            ln(f32::MAX, f32::MAX, f32::MIN, f32::MIN),
2653            ln(f32::MIN_POSITIVE, 0.0, -f32::MIN_POSITIVE, 0.0),
2654            ln(0.0, 0.0, 0.0, 0.0),
2655            ln(-1.0, -1.0, 1.0, 1.0),
2656        ];
2657        for p in &extremes {
2658            for q in &extremes {
2659                if let Some(i) = raw_line_intersection(p, q) {
2660                    assert!(
2661                        !i.x.is_nan() && !i.y.is_nan(),
2662                        "NaN escaped for {p:?} x {q:?} -> {i:?}"
2663                    );
2664                }
2665            }
2666        }
2667    }
2668
2669    #[test]
2670    fn raw_line_intersection_byval_matches_the_by_ref_form() {
2671        let p = ln(0.0, 0.0, 10.0, 10.0);
2672        let q = ln(0.0, 10.0, 10.0, 0.0);
2673        assert_eq!(
2674            raw_line_intersection_byval(&p, q),
2675            raw_line_intersection(&p, &q)
2676        );
2677        let horizontal = ln(0.0, 0.0, 10.0, 0.0);
2678        let parallel = ln(0.0, 5.0, 10.0, 5.0);
2679        assert_eq!(raw_line_intersection_byval(&horizontal, parallel), None);
2680    }
2681
2682    // ==================================================================
2683    // shorten_line_end_by / shorten_line_start_by
2684    // ==================================================================
2685
2686    #[test]
2687    fn shorten_line_end_by_trims_the_end_only() {
2688        let out = shorten_line_end_by(ln(0.0, 0.0, 10.0, 0.0), 4.0);
2689        assert_eq!(out.start, pt(0.0, 0.0));
2690        assert!((out.end.x - 6.0).abs() < 1e-3, "{out:?}");
2691        assert!(out.end.y.abs() < 1e-3, "{out:?}");
2692    }
2693
2694    #[test]
2695    fn shorten_line_end_by_zero_is_the_identity() {
2696        let l = ln(1.0, 2.0, 11.0, 2.0);
2697        let out = shorten_line_end_by(l, 0.0);
2698        assert!((out.end.x - 11.0).abs() < 1e-3, "{out:?}");
2699        assert!((out.end.y - 2.0).abs() < 1e-3, "{out:?}");
2700    }
2701
2702    #[test]
2703    fn shorten_line_end_by_more_than_the_length_overshoots_past_the_start() {
2704        // 20 taken off a 10-long line puts the end 10 units *behind* the start.
2705        let out = shorten_line_end_by(ln(0.0, 0.0, 10.0, 0.0), 20.0);
2706        assert!((out.end.x + 10.0).abs() < 1e-3, "{out:?}");
2707    }
2708
2709    #[test]
2710    fn shorten_line_end_by_negative_distance_extends_the_line() {
2711        let out = shorten_line_end_by(ln(0.0, 0.0, 10.0, 0.0), -5.0);
2712        assert!((out.end.x - 15.0).abs() < 1e-3, "{out:?}");
2713    }
2714
2715    #[test]
2716    fn shorten_line_end_by_degenerate_segment_yields_nan() {
2717        // LATENT DEFECT (pinned): a zero-length segment gives dt == 0, so
2718        // (dt - distance) / dt is -inf and -inf * 0.0 == NaN. A fix that returns
2719        // the line unchanged will flip this test loudly rather than silently.
2720        let out = shorten_line_end_by(ln(5.0, 5.0, 5.0, 5.0), 1.0);
2721        assert!(out.end.x.is_nan() && out.end.y.is_nan(), "{out:?}");
2722        assert_eq!(out.start, pt(5.0, 5.0), "the start is never touched");
2723    }
2724
2725    #[test]
2726    fn shorten_line_end_by_non_finite_distance_does_not_panic() {
2727        for d in [
2728            f32::NAN,
2729            f32::INFINITY,
2730            f32::NEG_INFINITY,
2731            f32::MAX,
2732            f32::MIN,
2733        ] {
2734            let out = shorten_line_end_by(ln(0.0, 0.0, 10.0, 0.0), d);
2735            assert_eq!(out.start, pt(0.0, 0.0), "distance {d}: start must survive");
2736        }
2737    }
2738
2739    #[test]
2740    fn shorten_line_start_by_trims_the_start_only() {
2741        let out = shorten_line_start_by(ln(0.0, 0.0, 10.0, 0.0), 4.0);
2742        assert_eq!(out.end, pt(10.0, 0.0));
2743        assert!((out.start.x - 4.0).abs() < 1e-3, "{out:?}");
2744        assert!(out.start.y.abs() < 1e-3, "{out:?}");
2745    }
2746
2747    #[test]
2748    fn shorten_line_start_by_zero_is_the_identity() {
2749        let out = shorten_line_start_by(ln(2.0, 3.0, 12.0, 3.0), 0.0);
2750        assert!((out.start.x - 2.0).abs() < 1e-3, "{out:?}");
2751        assert!((out.start.y - 3.0).abs() < 1e-3, "{out:?}");
2752    }
2753
2754    #[test]
2755    fn shorten_line_start_by_degenerate_segment_yields_nan() {
2756        // Same latent defect as `shorten_line_end_by`, mirrored onto the start.
2757        let out = shorten_line_start_by(ln(-3.0, 7.0, -3.0, 7.0), 2.0);
2758        assert!(out.start.x.is_nan() && out.start.y.is_nan(), "{out:?}");
2759        assert_eq!(out.end, pt(-3.0, 7.0));
2760    }
2761
2762    // ==================================================================
2763    // svg_path_offset
2764    // ==================================================================
2765
2766    #[test]
2767    fn svg_path_offset_zero_distance_returns_the_input_unchanged() {
2768        let p = square_path();
2769        assert_eq!(
2770            svg_path_offset(&p, 0.0, SvgLineJoin::Miter, SvgLineCap::Butt),
2771            p
2772        );
2773        // -0.0 == 0.0 under IEEE-754, so the early-out must fire for it too.
2774        assert_eq!(
2775            svg_path_offset(&p, -0.0, SvgLineJoin::Miter, SvgLineCap::Butt),
2776            p
2777        );
2778    }
2779
2780    #[test]
2781    fn svg_path_offset_empty_path_stays_empty() {
2782        // The `items.pop()` at the end must not underflow on an empty vec.
2783        let out = svg_path_offset(&empty_path(), 5.0, SvgLineJoin::Round, SvgLineCap::Round);
2784        assert!(out.items.as_ref().is_empty());
2785    }
2786
2787    #[test]
2788    fn svg_path_offset_single_line_moves_along_its_outwards_normal() {
2789        // (0,0)->(10,0) has inwards normal (0, 1), so outwards is (0, -1).
2790        let p = mk_path(vec![SvgPathElement::Line(ln(0.0, 0.0, 10.0, 0.0))]);
2791        let out = svg_path_offset(&p, 5.0, SvgLineJoin::Miter, SvgLineCap::Butt);
2792        assert_eq!(out.items.as_ref().len(), 1);
2793        match out.items.as_ref()[0] {
2794            SvgPathElement::Line(l) => {
2795                assert!(l.start.x.abs() < 1e-4, "{l:?}");
2796                assert!((l.end.x - 10.0).abs() < 1e-4, "{l:?}");
2797                assert!((l.start.y + 5.0).abs() < 1e-4, "{l:?}");
2798                assert!((l.end.y + 5.0).abs() < 1e-4, "{l:?}");
2799            }
2800            other => panic!("expected a line, got {other:?}"),
2801        }
2802    }
2803
2804    #[test]
2805    fn svg_path_offset_preserves_the_item_count_for_any_distance() {
2806        let p = square_path();
2807        for d in [
2808            1.0_f32,
2809            -1.0,
2810            1e-30,
2811            1e30,
2812            f32::MAX,
2813            f32::MIN,
2814            f32::NAN,
2815            f32::INFINITY,
2816            f32::NEG_INFINITY,
2817        ] {
2818            let out = svg_path_offset(&p, d, SvgLineJoin::Miter, SvgLineCap::Butt);
2819            assert_eq!(
2820                out.items.as_ref().len(),
2821                p.items.as_ref().len(),
2822                "distance {d} changed the element count"
2823            );
2824        }
2825    }
2826
2827    #[test]
2828    fn svg_path_offset_degenerate_segments_are_passed_through_unchanged() {
2829        // A zero-length line has no normal, so the element must come back as-is.
2830        let p = mk_path(vec![SvgPathElement::Line(ln(4.0, 4.0, 4.0, 4.0))]);
2831        assert_eq!(
2832            svg_path_offset(&p, 9.0, SvgLineJoin::Bevel, SvgLineCap::Square),
2833            p
2834        );
2835    }
2836
2837    #[test]
2838    fn svg_path_offset_ignores_its_join_and_cap_arguments() {
2839        // Documented gap (pinned): neither parameter is ever read; the function
2840        // only offsets the segments and re-intersects the neighbours.
2841        let p = square_path();
2842        let a = svg_path_offset(&p, 3.0, SvgLineJoin::Miter, SvgLineCap::Butt);
2843        let b = svg_path_offset(&p, 3.0, SvgLineJoin::Round, SvgLineCap::Round);
2844        assert_eq!(a, b);
2845    }
2846
2847    #[test]
2848    fn svg_path_offset_handles_curve_elements() {
2849        let p = mk_path(vec![
2850            SvgPathElement::QuadraticCurve(SvgQuadraticCurve {
2851                start: pt(0.0, 0.0),
2852                ctrl: pt(5.0, 10.0),
2853                end: pt(10.0, 0.0),
2854            }),
2855            SvgPathElement::CubicCurve(SvgCubicCurve {
2856                start: pt(10.0, 0.0),
2857                ctrl_1: pt(7.0, -5.0),
2858                ctrl_2: pt(3.0, -5.0),
2859                end: pt(0.0, 0.0),
2860            }),
2861        ]);
2862        let out = svg_path_offset(&p, 2.0, SvgLineJoin::Miter, SvgLineCap::Butt);
2863        assert_eq!(out.items.as_ref().len(), 2);
2864    }
2865
2866    // ==================================================================
2867    // svg_path_bevel
2868    // ==================================================================
2869
2870    #[test]
2871    fn svg_path_bevel_empty_path_stays_empty() {
2872        // Two unconditional `pop()`s on an empty vec must not underflow.
2873        assert!(svg_path_bevel(&empty_path(), 2.0).items.as_ref().is_empty());
2874    }
2875
2876    #[test]
2877    fn svg_path_bevel_single_line_expands_to_four_elements() {
2878        // The single element is duplicated at both ends (3 items -> 2 pairs ->
2879        // 6 pushes), then one element is trimmed off each side.
2880        let p = mk_path(vec![SvgPathElement::Line(ln(0.0, 0.0, 10.0, 0.0))]);
2881        let out = svg_path_bevel(&p, 2.0);
2882        assert_eq!(out.items.as_ref().len(), 4);
2883        for e in out.items.as_ref() {
2884            let (s, t) = (e.get_start(), e.get_end());
2885            assert!(
2886                s.x.is_finite() && s.y.is_finite() && t.x.is_finite() && t.y.is_finite(),
2887                "{e:?}"
2888            );
2889        }
2890    }
2891
2892    #[test]
2893    fn svg_path_bevel_non_line_pairs_are_passed_straight_through() {
2894        let p = mk_path(vec![
2895            SvgPathElement::Line(ln(0.0, 0.0, 10.0, 0.0)),
2896            SvgPathElement::CubicCurve(SvgCubicCurve {
2897                start: pt(10.0, 0.0),
2898                ctrl_1: pt(12.0, 0.0),
2899                ctrl_2: pt(14.0, 2.0),
2900                end: pt(14.0, 4.0),
2901            }),
2902        ]);
2903        // No (Line, Line) pair survives the duplication, so every pair takes the
2904        // 2-push fall-through arm: 3 pairs -> 6 pushes -> 4 after trimming.
2905        assert_eq!(svg_path_bevel(&p, 1.0).items.as_ref().len(), 4);
2906    }
2907
2908    #[test]
2909    fn svg_path_bevel_zero_distance_keeps_every_coordinate_finite() {
2910        for e in svg_path_bevel(&square_path(), 0.0).items.as_ref() {
2911            let (s, t) = (e.get_start(), e.get_end());
2912            assert!(
2913                s.x.is_finite() && s.y.is_finite() && t.x.is_finite() && t.y.is_finite(),
2914                "{e:?}"
2915            );
2916        }
2917    }
2918
2919    #[test]
2920    fn svg_path_bevel_extreme_distances_do_not_panic() {
2921        let p = square_path();
2922        for d in [
2923            f32::NAN,
2924            f32::INFINITY,
2925            f32::NEG_INFINITY,
2926            f32::MAX,
2927            -f32::MAX,
2928            1e-30,
2929        ] {
2930            assert!(
2931                !svg_path_bevel(&p, d).items.as_ref().is_empty(),
2932                "distance {d} produced an empty path"
2933            );
2934        }
2935    }
2936
2937    #[test]
2938    fn svg_path_bevel_degenerate_segments_do_not_panic() {
2939        let p = mk_path(vec![
2940            SvgPathElement::Line(ln(1.0, 1.0, 1.0, 1.0)),
2941            SvgPathElement::Line(ln(1.0, 1.0, 1.0, 1.0)),
2942        ]);
2943        // NaN coordinates leak out of `shorten_line_*_by` here, but nothing panics.
2944        assert!(!svg_path_bevel(&p, 3.0).items.as_ref().is_empty());
2945    }
2946
2947    // ==================================================================
2948    // svg_node_contains_point / path_contains_point / polygon_contains_point
2949    // ==================================================================
2950
2951    #[test]
2952    fn svg_node_contains_point_rect_excludes_its_own_border() {
2953        let node = SvgNode::Rect(rect(0.0, 0.0, 10.0, 10.0));
2954        assert!(svg_node_contains_point(
2955            &node,
2956            pt(5.0, 5.0),
2957            SvgFillRule::Winding,
2958            0.1
2959        ));
2960        assert!(!svg_node_contains_point(
2961            &node,
2962            pt(15.0, 5.0),
2963            SvgFillRule::Winding,
2964            0.1
2965        ));
2966        // `>` / `<`, not `>=` / `<=`: the border itself is outside.
2967        assert!(!svg_node_contains_point(
2968            &node,
2969            pt(0.0, 0.0),
2970            SvgFillRule::Winding,
2971            0.1
2972        ));
2973        assert!(!svg_node_contains_point(
2974            &node,
2975            pt(10.0, 10.0),
2976            SvgFillRule::Winding,
2977            0.1
2978        ));
2979    }
2980
2981    #[test]
2982    fn svg_node_contains_point_circle_excludes_its_own_rim() {
2983        let node = SvgNode::Circle(SvgCircle {
2984            center_x: 0.0,
2985            center_y: 0.0,
2986            radius: 5.0,
2987        });
2988        assert!(svg_node_contains_point(
2989            &node,
2990            pt(0.0, 0.0),
2991            SvgFillRule::Winding,
2992            0.1
2993        ));
2994        assert!(!svg_node_contains_point(
2995            &node,
2996            pt(5.0, 0.0),
2997            SvgFillRule::Winding,
2998            0.1
2999        ));
3000        assert!(!svg_node_contains_point(
3001            &node,
3002            pt(100.0, 100.0),
3003            SvgFillRule::Winding,
3004            0.1
3005        ));
3006    }
3007
3008    #[test]
3009    fn svg_node_contains_point_nan_and_infinite_points_are_outside() {
3010        let r = SvgNode::Rect(rect(0.0, 0.0, 10.0, 10.0));
3011        let c = SvgNode::Circle(SvgCircle {
3012            center_x: 0.0,
3013            center_y: 0.0,
3014            radius: 5.0,
3015        });
3016        for p in [
3017            pt(f32::NAN, f32::NAN),
3018            pt(f32::NAN, 5.0),
3019            pt(5.0, f32::NAN),
3020            pt(f32::INFINITY, f32::INFINITY),
3021            pt(f32::NEG_INFINITY, 0.0),
3022        ] {
3023            assert!(
3024                !svg_node_contains_point(&r, p, SvgFillRule::Winding, 0.1),
3025                "rect / {p:?}"
3026            );
3027            assert!(
3028                !svg_node_contains_point(&c, p, SvgFillRule::EvenOdd, 0.1),
3029                "circle / {p:?}"
3030            );
3031        }
3032    }
3033
3034    #[test]
3035    fn svg_node_contains_point_empty_geometry_is_never_hit() {
3036        for node in [
3037            SvgNode::MultiPolygonCollection(SvgMultiPolygonVec::from_const_slice(&[])),
3038            SvgNode::MultiShape(SvgSimpleNodeVec::from_const_slice(&[])),
3039            SvgNode::MultiPolygon(empty_polygon()),
3040            SvgNode::Path(empty_path()),
3041        ] {
3042            assert!(
3043                !svg_node_contains_point(&node, pt(0.0, 0.0), SvgFillRule::Winding, 0.1),
3044                "{node:?}"
3045            );
3046        }
3047    }
3048
3049    #[test]
3050    fn svg_node_contains_point_open_paths_short_circuit_to_false() {
3051        let open = mk_path(vec![
3052            SvgPathElement::Line(ln(0.0, 0.0, 10.0, 0.0)),
3053            SvgPathElement::Line(ln(10.0, 0.0, 10.0, 10.0)),
3054        ]);
3055        assert!(!svg_node_contains_point(
3056            &SvgNode::Path(open.clone()),
3057            pt(5.0, 5.0),
3058            SvgFillRule::Winding,
3059            0.1
3060        ));
3061        assert!(!svg_node_contains_point(
3062            &SvgNode::MultiShape(SvgSimpleNodeVec::from_vec(vec![SvgSimpleNode::Path(open)])),
3063            pt(5.0, 5.0),
3064            SvgFillRule::Winding,
3065            0.1
3066        ));
3067    }
3068
3069    #[test]
3070    fn svg_node_contains_point_closed_square_path_is_hit() {
3071        let node = SvgNode::Path(square_path());
3072        assert!(svg_node_contains_point(
3073            &node,
3074            pt(5.0, 5.0),
3075            SvgFillRule::Winding,
3076            0.1
3077        ));
3078        assert!(!svg_node_contains_point(
3079            &node,
3080            pt(50.0, 5.0),
3081            SvgFillRule::Winding,
3082            0.1
3083        ));
3084    }
3085
3086    #[test]
3087    fn svg_node_contains_point_lone_hole_reports_everything_outside_it() {
3088        // Pinned semantics: a MultiShape made only of holes inverts, so every
3089        // point *outside* the hole counts as a hit.
3090        let hole = SvgCircle {
3091            center_x: 0.0,
3092            center_y: 0.0,
3093            radius: 5.0,
3094        };
3095        let node = SvgNode::MultiShape(SvgSimpleNodeVec::from_vec(vec![
3096            SvgSimpleNode::CircleHole(hole),
3097        ]));
3098        assert!(!svg_node_contains_point(
3099            &node,
3100            pt(0.0, 0.0),
3101            SvgFillRule::Winding,
3102            0.1
3103        ));
3104        assert!(svg_node_contains_point(
3105            &node,
3106            pt(100.0, 100.0),
3107            SvgFillRule::Winding,
3108            0.1
3109        ));
3110    }
3111
3112    #[test]
3113    fn path_contains_point_square_positive_and_negative_controls() {
3114        let sq = square_path();
3115        assert!(path_contains_point(
3116            &sq,
3117            pt(5.0, 5.0),
3118            SvgFillRule::Winding,
3119            0.1
3120        ));
3121        assert!(path_contains_point(
3122            &sq,
3123            pt(5.0, 5.0),
3124            SvgFillRule::EvenOdd,
3125            0.1
3126        ));
3127        assert!(!path_contains_point(
3128            &sq,
3129            pt(-1.0, 5.0),
3130            SvgFillRule::Winding,
3131            0.1
3132        ));
3133        assert!(!path_contains_point(
3134            &sq,
3135            pt(5.0, 100.0),
3136            SvgFillRule::EvenOdd,
3137            0.1
3138        ));
3139    }
3140
3141    #[test]
3142    fn path_contains_point_empty_path_is_never_hit() {
3143        assert!(!path_contains_point(
3144            &empty_path(),
3145            pt(0.0, 0.0),
3146            SvgFillRule::Winding,
3147            0.1
3148        ));
3149    }
3150
3151    #[test]
3152    fn path_contains_point_tolerance_is_irrelevant_for_straight_edges() {
3153        // A line-only path is never flattened, so even 0 / negative / huge
3154        // tolerances must give the same answer instead of hanging.
3155        let sq = square_path();
3156        for t in [0.0_f32, 1e-6, 1.0, 1e6, -1.0] {
3157            assert!(
3158                path_contains_point(&sq, pt(5.0, 5.0), SvgFillRule::Winding, t),
3159                "tolerance {t}"
3160            );
3161            assert!(
3162                !path_contains_point(&sq, pt(-50.0, 5.0), SvgFillRule::Winding, t),
3163                "tolerance {t}"
3164            );
3165        }
3166    }
3167
3168    #[test]
3169    fn polygon_contains_point_square_ring() {
3170        let poly = square_polygon();
3171        assert!(polygon_contains_point(
3172            &poly,
3173            pt(5.0, 5.0),
3174            SvgFillRule::Winding,
3175            0.1
3176        ));
3177        assert!(!polygon_contains_point(
3178            &poly,
3179            pt(-5.0, 5.0),
3180            SvgFillRule::Winding,
3181            0.1
3182        ));
3183    }
3184
3185    #[test]
3186    fn polygon_contains_point_without_rings_is_false() {
3187        assert!(!polygon_contains_point(
3188            &empty_polygon(),
3189            pt(0.0, 0.0),
3190            SvgFillRule::Winding,
3191            0.1
3192        ));
3193        // A ring that carries no elements must not be treated as a hit either.
3194        assert!(!polygon_contains_point(
3195            &polygon_of(vec![empty_path()]),
3196            pt(0.0, 0.0),
3197            SvgFillRule::Winding,
3198            0.1
3199        ));
3200    }
3201
3202    // ==================================================================
3203    // lyon path conversion helpers
3204    // ==================================================================
3205
3206    #[test]
3207    fn svg_multipolygon_to_lyon_path_of_an_empty_polygon_is_empty() {
3208        assert_eq!(svg_multipolygon_to_lyon_path(&empty_polygon()).iter().count(), 0);
3209    }
3210
3211    #[test]
3212    fn svg_multipolygon_to_lyon_path_skips_rings_without_items() {
3213        let only_empty = polygon_of(vec![empty_path(), empty_path()]);
3214        assert_eq!(svg_multipolygon_to_lyon_path(&only_empty).iter().count(), 0);
3215        // …and an empty ring must not change the result of a real one.
3216        let mixed = polygon_of(vec![empty_path(), square_path()]);
3217        assert_eq!(
3218            svg_multipolygon_to_lyon_path(&mixed).iter().count(),
3219            svg_multipolygon_to_lyon_path(&square_polygon()).iter().count()
3220        );
3221    }
3222
3223    #[test]
3224    fn svg_multi_shape_to_lyon_path_of_an_empty_slice_is_empty() {
3225        assert_eq!(svg_multi_shape_to_lyon_path(&[]).iter().count(), 0);
3226    }
3227
3228    #[test]
3229    fn svg_path_to_lyon_path_events_of_an_empty_path_is_empty() {
3230        assert_eq!(svg_path_to_lyon_path_events(&empty_path()).iter().count(), 0);
3231    }
3232
3233    // ==================================================================
3234    // vertex_buffers_to_tessellated_cpu_node
3235    // ==================================================================
3236
3237    #[test]
3238    fn vertex_buffers_to_tessellated_cpu_node_moves_both_buffers_verbatim() {
3239        let mut vb: VertexBuffers<SvgVertex, u32> = VertexBuffers::new();
3240        vb.vertices.push(SvgVertex { x: 1.0, y: 2.0 });
3241        vb.vertices.push(SvgVertex { x: 3.0, y: 4.0 });
3242        vb.indices.extend_from_slice(&[0, 1, 0]);
3243        let t = vertex_buffers_to_tessellated_cpu_node(vb);
3244        assert_eq!(t.vertices.as_ref().len(), 2);
3245        assert_eq!(t.indices.as_ref(), &[0u32, 1, 0][..]);
3246        assert!((t.vertices.as_ref()[1].x - 3.0).abs() < 1e-6);
3247    }
3248
3249    #[test]
3250    fn vertex_buffers_to_tessellated_cpu_node_of_empty_buffers_is_empty() {
3251        let t = vertex_buffers_to_tessellated_cpu_node(VertexBuffers::<SvgVertex, u32>::new());
3252        assert!(t.vertices.as_ref().is_empty());
3253        assert!(t.indices.as_ref().is_empty());
3254    }
3255
3256    // ==================================================================
3257    // tessellation
3258    // ==================================================================
3259
3260    #[test]
3261    fn tessellate_path_fill_of_a_square_produces_whole_triangles() {
3262        let t = tessellate_path_fill(&square_path(), SvgFillStyle::default());
3263        assert!(!t.vertices.as_ref().is_empty());
3264        assert_eq!(t.indices.as_ref().len() % 3, 0);
3265        assert_indices_in_range(&t);
3266    }
3267
3268    #[test]
3269    fn tessellate_path_fill_and_stroke_of_an_empty_path_are_empty() {
3270        let f = tessellate_path_fill(&empty_path(), SvgFillStyle::default());
3271        assert!(f.vertices.as_ref().is_empty() && f.indices.as_ref().is_empty());
3272        let s = tessellate_path_stroke(&empty_path(), SvgStrokeStyle::default());
3273        assert!(s.vertices.as_ref().is_empty() && s.indices.as_ref().is_empty());
3274    }
3275
3276    #[test]
3277    fn tessellate_circle_fill_zero_radius_is_empty() {
3278        let t = tessellate_circle_fill(
3279            &SvgCircle {
3280                center_x: 0.0,
3281                center_y: 0.0,
3282                radius: 0.0,
3283            },
3284            SvgFillStyle::default(),
3285        );
3286        assert!(t.vertices.as_ref().is_empty());
3287    }
3288
3289    #[test]
3290    fn tessellate_circle_fill_negative_radius_matches_the_positive_one() {
3291        let mk = |r: f32| {
3292            tessellate_circle_fill(
3293                &SvgCircle {
3294                    center_x: 1.0,
3295                    center_y: 2.0,
3296                    radius: r,
3297                },
3298                SvgFillStyle::default(),
3299            )
3300        };
3301        let positive = mk(5.0);
3302        assert!(!positive.vertices.as_ref().is_empty());
3303        assert_eq!(positive, mk(-5.0), "lyon takes |radius|; the sign must not matter");
3304    }
3305
3306    #[test]
3307    fn tessellate_rect_fill_always_emits_exactly_two_triangles() {
3308        for r in [
3309            rect(0.0, 0.0, 10.0, 10.0),
3310            rect(0.0, 0.0, 0.0, 0.0),                     // fully degenerate
3311            rect(5.0, 5.0, -10.0, -10.0),                 // inverted
3312            rect(-f32::MAX, -f32::MAX, f32::MAX, f32::MAX), // extreme but finite
3313        ] {
3314            let t = tessellate_rect_fill(&r, SvgFillStyle::default());
3315            assert_eq!(t.vertices.as_ref().len(), 4, "{r:?}");
3316            assert_eq!(t.indices.as_ref().len(), 6, "{r:?}");
3317            assert_indices_in_range(&t);
3318        }
3319    }
3320
3321    #[test]
3322    fn tessellate_rect_stroke_of_a_real_rect_produces_geometry() {
3323        let t = tessellate_rect_stroke(&rect(0.0, 0.0, 10.0, 10.0), SvgStrokeStyle::default());
3324        assert!(!t.vertices.as_ref().is_empty());
3325        assert_indices_in_range(&t);
3326    }
3327
3328    #[test]
3329    fn get_radii_maps_origin_and_size_onto_a_box() {
3330        let b = get_radii(&rect(1.0, 2.0, 4.0, 6.0));
3331        assert!((b.min.x - 1.0).abs() < 1e-6, "{b:?}");
3332        assert!((b.min.y - 2.0).abs() < 1e-6, "{b:?}");
3333        assert!((b.max.x - 5.0).abs() < 1e-6, "{b:?}");
3334        assert!((b.max.y - 8.0).abs() < 1e-6, "{b:?}");
3335    }
3336
3337    #[test]
3338    fn get_radii_ignores_the_corner_radii() {
3339        // Matches the source TODO: "radii not respected on latest version of lyon".
3340        let base = rect(1.0, 2.0, 4.0, 6.0);
3341        let rounded = SvgRect {
3342            radius_top_left: 3.0,
3343            radius_top_right: 4.0,
3344            radius_bottom_left: 5.0,
3345            radius_bottom_right: 9.0,
3346            ..base
3347        };
3348        let (a, b) = (get_radii(&base), get_radii(&rounded));
3349        assert!((a.min.x - b.min.x).abs() < 1e-6 && (a.min.y - b.min.y).abs() < 1e-6);
3350        assert!((a.max.x - b.max.x).abs() < 1e-6 && (a.max.y - b.max.y).abs() < 1e-6);
3351    }
3352
3353    #[test]
3354    fn get_radii_of_a_negative_size_rect_produces_an_inverted_box() {
3355        let b = get_radii(&rect(5.0, 5.0, -3.0, -4.0));
3356        assert!(b.max.x < b.min.x, "{b:?}");
3357        assert!(b.max.y < b.min.y, "{b:?}");
3358    }
3359
3360    #[test]
3361    fn tessellate_multi_polygon_fill_of_an_empty_polygon_is_empty() {
3362        let t = tessellate_multi_polygon_fill(&empty_polygon(), SvgFillStyle::default());
3363        assert!(t.vertices.as_ref().is_empty());
3364    }
3365
3366    #[test]
3367    fn tessellate_multi_polygon_fill_skips_empty_rings() {
3368        let with_empty = polygon_of(vec![empty_path(), square_path()]);
3369        assert_eq!(
3370            tessellate_multi_polygon_fill(&with_empty, SvgFillStyle::default()),
3371            tessellate_multi_polygon_fill(&square_polygon(), SvgFillStyle::default()),
3372        );
3373    }
3374
3375    #[test]
3376    fn tessellate_multi_shape_fill_of_an_empty_slice_is_empty() {
3377        let t = tessellate_multi_shape_fill(&[], SvgFillStyle::default());
3378        assert!(t.vertices.as_ref().is_empty());
3379    }
3380
3381    #[test]
3382    fn tessellate_multi_shape_fill_of_a_plain_shape_produces_geometry() {
3383        let ms = [
3384            SvgSimpleNode::Circle(SvgCircle {
3385                center_x: 40.0,
3386                center_y: 40.0,
3387                radius: 5.0,
3388            }),
3389            SvgSimpleNode::Rect(rect(60.0, 0.0, 5.0, 5.0)),
3390        ];
3391        let t = tessellate_multi_shape_fill(&ms, SvgFillStyle::default());
3392        assert!(!t.vertices.as_ref().is_empty());
3393        assert_indices_in_range(&t);
3394    }
3395
3396    #[test]
3397    fn tessellate_multi_shape_fill_handles_every_simple_node_kind() {
3398        let ms = [
3399            SvgSimpleNode::Path(square_path()),
3400            SvgSimpleNode::Circle(SvgCircle {
3401                center_x: 40.0,
3402                center_y: 40.0,
3403                radius: 5.0,
3404            }),
3405            SvgSimpleNode::CircleHole(SvgCircle {
3406                center_x: 40.0,
3407                center_y: 40.0,
3408                radius: 2.0,
3409            }),
3410            SvgSimpleNode::Rect(rect(60.0, 0.0, 5.0, 5.0)),
3411            SvgSimpleNode::RectHole(rect(61.0, 1.0, 2.0, 2.0)),
3412        ];
3413        assert_indices_in_range(&tessellate_multi_shape_fill(&ms, SvgFillStyle::default()));
3414        assert_indices_in_range(&tessellate_multi_shape_stroke(
3415            &ms,
3416            SvgStrokeStyle::default(),
3417        ));
3418    }
3419
3420    #[test]
3421    fn tessellate_multi_polygon_stroke_of_an_empty_polygon_is_empty() {
3422        let t = tessellate_multi_polygon_stroke(&empty_polygon(), SvgStrokeStyle::default());
3423        assert!(t.vertices.as_ref().is_empty());
3424    }
3425
3426    #[test]
3427    fn tessellate_styled_node_dispatches_on_the_style() {
3428        let geo = SvgNode::Rect(rect(0.0, 0.0, 10.0, 10.0));
3429        let fill = SvgStyledNode {
3430            geometry: geo.clone(),
3431            style: SvgStyle::Fill(SvgFillStyle::default()),
3432        };
3433        let stroke = SvgStyledNode {
3434            geometry: geo.clone(),
3435            style: SvgStyle::Stroke(SvgStrokeStyle::default()),
3436        };
3437        assert_eq!(
3438            tessellate_styled_node(&fill),
3439            tessellate_node_fill(&geo, SvgFillStyle::default())
3440        );
3441        assert_eq!(
3442            tessellate_styled_node(&stroke),
3443            tessellate_node_stroke(&geo, SvgStrokeStyle::default())
3444        );
3445    }
3446
3447    #[test]
3448    fn tessellate_node_fill_of_a_collection_is_the_join_of_its_parts() {
3449        let mp = square_polygon();
3450        let node =
3451            SvgNode::MultiPolygonCollection(SvgMultiPolygonVec::from_vec(vec![mp.clone(), mp.clone()]));
3452        let one = tessellate_multi_polygon_fill(&mp, SvgFillStyle::default());
3453        assert_eq!(
3454            tessellate_node_fill(&node, SvgFillStyle::default()),
3455            join_tessellated_nodes(&[one.clone(), one])
3456        );
3457    }
3458
3459    #[test]
3460    fn tessellate_node_fill_of_an_empty_collection_is_empty() {
3461        let node = SvgNode::MultiPolygonCollection(SvgMultiPolygonVec::from_const_slice(&[]));
3462        let t = tessellate_node_fill(&node, SvgFillStyle::default());
3463        assert!(t.vertices.as_ref().is_empty() && t.indices.as_ref().is_empty());
3464    }
3465
3466    #[test]
3467    fn tessellate_svgpathelement_stroke_matches_the_per_kind_helpers() {
3468        let ss = SvgStrokeStyle::default();
3469        let l = ln(0.0, 0.0, 10.0, 10.0);
3470        assert_eq!(
3471            tessellate_svgpathelement_stroke(&SvgPathElement::Line(l), ss),
3472            tessellate_line_stroke(&l, ss)
3473        );
3474        let q = SvgQuadraticCurve {
3475            start: pt(0.0, 0.0),
3476            ctrl: pt(5.0, 10.0),
3477            end: pt(10.0, 0.0),
3478        };
3479        assert_eq!(
3480            tessellate_svgpathelement_stroke(&SvgPathElement::QuadraticCurve(q), ss),
3481            tessellate_quadraticcurve_stroke(&q, ss)
3482        );
3483        let c = SvgCubicCurve {
3484            start: pt(0.0, 0.0),
3485            ctrl_1: pt(3.0, 10.0),
3486            ctrl_2: pt(7.0, -10.0),
3487            end: pt(10.0, 0.0),
3488        };
3489        assert_eq!(
3490            tessellate_svgpathelement_stroke(&SvgPathElement::CubicCurve(c), ss),
3491            tessellate_cubiccurve_stroke(&c, ss)
3492        );
3493    }
3494
3495    #[test]
3496    fn tessellate_line_stroke_of_a_zero_length_line_does_not_panic() {
3497        let t = tessellate_line_stroke(&ln(3.0, 3.0, 3.0, 3.0), SvgStrokeStyle::default());
3498        assert_indices_in_range(&t);
3499    }
3500
3501    // ==================================================================
3502    // join_tessellated_nodes / join_tessellated_colored_nodes
3503    // ==================================================================
3504
3505    #[test]
3506    fn join_tessellated_nodes_of_nothing_is_empty() {
3507        let t = join_tessellated_nodes(&[]);
3508        assert!(t.vertices.as_ref().is_empty());
3509        assert!(t.indices.as_ref().is_empty());
3510    }
3511
3512    #[test]
3513    fn join_tessellated_nodes_offsets_and_terminates_each_buffer() {
3514        let a = tess(&[(0.0, 0.0), (1.0, 0.0)], &[0, 1]);
3515        let b = tess(&[(2.0, 0.0), (3.0, 0.0)], &[0, 1]);
3516        let j = join_tessellated_nodes(&[a, b]);
3517        assert_eq!(j.vertices.as_ref().len(), 4);
3518        assert_eq!(
3519            j.indices.as_ref(),
3520            &[0u32, 1, GL_RESTART_INDEX, 2, 3, GL_RESTART_INDEX][..]
3521        );
3522        assert_indices_in_range(&j);
3523    }
3524
3525    #[test]
3526    fn join_tessellated_nodes_leaves_existing_restart_markers_unshifted() {
3527        let a = tess(&[(0.0, 0.0)], &[0]);
3528        let b = tess(&[(1.0, 0.0), (2.0, 0.0)], &[0, GL_RESTART_INDEX, 1]);
3529        let j = join_tessellated_nodes(&[a, b]);
3530        assert_eq!(
3531            j.indices.as_ref(),
3532            &[
3533                0u32,
3534                GL_RESTART_INDEX,
3535                1,
3536                GL_RESTART_INDEX,
3537                2,
3538                GL_RESTART_INDEX
3539            ][..]
3540        );
3541    }
3542
3543    #[test]
3544    fn join_tessellated_nodes_single_node_is_only_terminated() {
3545        let j = join_tessellated_nodes(&[tess(&[(0.0, 0.0), (1.0, 1.0)], &[0, 1, 0])]);
3546        assert_eq!(j.indices.as_ref(), &[0u32, 1, 0, GL_RESTART_INDEX][..]);
3547    }
3548
3549    #[test]
3550    fn join_tessellated_nodes_can_alias_an_index_onto_the_restart_marker() {
3551        // FINDING (pinned): the offset is applied with a bare `+=` and is only
3552        // skipped for indices that *already* equal GL_RESTART_INDEX, so a large
3553        // index can be shifted onto the sentinel and silently turn into a
3554        // primitive-restart marker. (Anything past u32::MAX additionally
3555        // overflow-panics in debug builds.)
3556        let a = tess(&[(0.0, 0.0)], &[0]);
3557        let b = tess(&[(1.0, 0.0)], &[GL_RESTART_INDEX - 1]);
3558        let j = join_tessellated_nodes(&[a, b]);
3559        assert_eq!(
3560            j.indices.as_ref(),
3561            &[
3562                0u32,
3563                GL_RESTART_INDEX,
3564                GL_RESTART_INDEX,
3565                GL_RESTART_INDEX
3566            ][..]
3567        );
3568    }
3569
3570    #[test]
3571    fn join_tessellated_colored_nodes_of_nothing_is_empty() {
3572        let t = join_tessellated_colored_nodes(&[]);
3573        assert!(t.vertices.as_ref().is_empty());
3574        assert!(t.indices.as_ref().is_empty());
3575    }
3576
3577    #[test]
3578    fn join_tessellated_colored_nodes_offsets_like_the_plain_variant() {
3579        fn colored(xs: &[f32], idx: &[u32]) -> TessellatedColoredSvgNode {
3580            TessellatedColoredSvgNode {
3581                vertices: xs
3582                    .iter()
3583                    .map(|&x| SvgColoredVertex {
3584                        x,
3585                        y: 0.0,
3586                        z: 0.0,
3587                        r: 1.0,
3588                        g: 0.0,
3589                        b: 0.0,
3590                        a: 1.0,
3591                    })
3592                    .collect::<Vec<_>>()
3593                    .into(),
3594                indices: idx.to_vec().into(),
3595            }
3596        }
3597        let j = join_tessellated_colored_nodes(&[
3598            colored(&[0.0, 1.0], &[0, 1]),
3599            colored(&[2.0], &[0]),
3600        ]);
3601        assert_eq!(j.vertices.as_ref().len(), 3);
3602        assert_eq!(
3603            j.indices.as_ref(),
3604            &[0u32, 1, GL_RESTART_INDEX, 2, GL_RESTART_INDEX][..]
3605        );
3606    }
3607
3608    // ==================================================================
3609    // boolean operations (agg scanline algebra)
3610    // ==================================================================
3611
3612    #[test]
3613    fn storage_to_multi_polygon_of_an_untouched_storage_is_empty() {
3614        let mut storage = agg_rust::scanline_storage_aa::ScanlineStorageAa::new();
3615        assert!(storage_to_multi_polygon(&mut storage).rings.as_ref().is_empty());
3616    }
3617
3618    #[test]
3619    fn svg_multi_polygon_boolean_ops_on_two_empties_are_empty() {
3620        let e = empty_polygon();
3621        assert!(svg_multi_polygon_union(&e, &e).rings.as_ref().is_empty());
3622        assert!(svg_multi_polygon_intersection(&e, &e).rings.as_ref().is_empty());
3623        assert!(svg_multi_polygon_difference(&e, &e).rings.as_ref().is_empty());
3624        assert!(svg_multi_polygon_xor(&e, &e).rings.as_ref().is_empty());
3625    }
3626
3627    #[test]
3628    fn svg_multi_polygon_union_of_a_square_with_itself_keeps_the_square() {
3629        let sq = square_polygon();
3630        let out = svg_multi_polygon_union(&sq, &sq);
3631        assert!(
3632            !out.rings.as_ref().is_empty(),
3633            "a shape unioned with itself must not vanish"
3634        );
3635        let b = out.get_bounds();
3636        assert!(b.width > 0.0 && b.height > 0.0, "{b:?}");
3637    }
3638
3639    #[test]
3640    fn svg_multi_polygon_intersection_of_disjoint_squares_is_empty() {
3641        let a = square_polygon();
3642        let b = polygon_of(vec![square_at(100.0)]);
3643        assert!(svg_multi_polygon_intersection(&a, &b)
3644            .rings
3645            .as_ref()
3646            .is_empty());
3647    }
3648
3649    #[test]
3650    fn svg_multi_polygon_self_cancelling_ops_are_empty() {
3651        let sq = square_polygon();
3652        assert!(
3653            svg_multi_polygon_difference(&sq, &sq).rings.as_ref().is_empty(),
3654            "A minus A must be empty"
3655        );
3656        assert!(
3657            svg_multi_polygon_xor(&sq, &sq).rings.as_ref().is_empty(),
3658            "A xor A must be empty"
3659        );
3660    }
3661
3662    #[test]
3663    fn svg_multi_polygon_ops_accept_curve_rings() {
3664        let ring = mk_path(vec![
3665            SvgPathElement::QuadraticCurve(SvgQuadraticCurve {
3666                start: pt(0.0, 0.0),
3667                ctrl: pt(5.0, 12.0),
3668                end: pt(10.0, 0.0),
3669            }),
3670            SvgPathElement::CubicCurve(SvgCubicCurve {
3671                start: pt(10.0, 0.0),
3672                ctrl_1: pt(7.0, -6.0),
3673                ctrl_2: pt(3.0, -6.0),
3674                end: pt(0.0, 0.0),
3675            }),
3676        ]);
3677        let mp = polygon_of(vec![ring]);
3678        assert!(!svg_multi_polygon_union(&mp, &mp).rings.as_ref().is_empty());
3679    }
3680
3681    #[test]
3682    fn svg_multi_polygon_byval_wrappers_match_the_by_ref_forms() {
3683        let a = square_polygon();
3684        let b = polygon_of(vec![square_at(5.0)]);
3685        assert_eq!(
3686            svg_multi_polygon_union_byval(&a, b.clone()),
3687            svg_multi_polygon_union(&a, &b)
3688        );
3689        assert_eq!(
3690            svg_multi_polygon_intersection_byval(&a, b.clone()),
3691            svg_multi_polygon_intersection(&a, &b)
3692        );
3693        assert_eq!(
3694            svg_multi_polygon_difference_byval(&a, b.clone()),
3695            svg_multi_polygon_difference(&a, &b)
3696        );
3697        assert_eq!(
3698            svg_multi_polygon_xor_byval(&a, b.clone()),
3699            svg_multi_polygon_xor(&a, &b)
3700        );
3701    }
3702
3703    // ==================================================================
3704    // render_node_clipmask_cpu
3705    // ==================================================================
3706
3707    #[test]
3708    fn render_node_clipmask_cpu_zero_sized_image_is_none() {
3709        let node = SvgNode::Rect(rect(0.0, 0.0, 4.0, 4.0));
3710        let style = SvgStyle::Fill(SvgFillStyle {
3711            transform: identity_transform(),
3712            ..SvgFillStyle::default()
3713        });
3714        assert_eq!(
3715            render_node_clipmask_cpu(&mut mask_image(0, 4), &node, style),
3716            None
3717        );
3718        assert_eq!(
3719            render_node_clipmask_cpu(&mut mask_image(4, 0), &node, style),
3720            None
3721        );
3722        assert_eq!(
3723            render_node_clipmask_cpu(&mut mask_image(0, 0), &node, style),
3724            None
3725        );
3726    }
3727
3728    #[test]
3729    fn render_node_clipmask_cpu_geometry_less_nodes_are_none() {
3730        let style = SvgStyle::Fill(SvgFillStyle {
3731            transform: identity_transform(),
3732            ..SvgFillStyle::default()
3733        });
3734        for node in [
3735            SvgNode::Path(empty_path()),
3736            SvgNode::MultiPolygon(empty_polygon()),
3737            SvgNode::MultiShape(SvgSimpleNodeVec::from_const_slice(&[])),
3738            SvgNode::MultiPolygonCollection(SvgMultiPolygonVec::from_const_slice(&[])),
3739        ] {
3740            assert_eq!(
3741                render_node_clipmask_cpu(&mut mask_image(4, 4), &node, style),
3742                None,
3743                "{node:?}"
3744            );
3745        }
3746    }
3747
3748    #[test]
3749    fn render_node_clipmask_cpu_writes_a_single_channel_mask() {
3750        let node = SvgNode::Rect(rect(0.0, 0.0, 4.0, 4.0));
3751        let style = SvgStyle::Fill(SvgFillStyle {
3752            transform: identity_transform(),
3753            ..SvgFillStyle::default()
3754        });
3755        let mut img = mask_image(4, 4);
3756        assert_eq!(render_node_clipmask_cpu(&mut img, &node, style), Some(()));
3757        assert_eq!(img.data_format, RawImageFormat::R8);
3758        assert!(img.premultiplied_alpha);
3759        let bytes = mask_bytes(&img);
3760        assert_eq!(bytes.len(), 16, "one byte per pixel");
3761        assert!(
3762            bytes.iter().any(|&b| b > 0),
3763            "a rect covering the whole image must leave coverage"
3764        );
3765    }
3766
3767    #[test]
3768    fn render_node_clipmask_cpu_default_style_transform_is_not_the_identity() {
3769        // FINDING (pinned): `SvgTransform::default()` is the *zero* matrix, so a
3770        // style built straight from `SvgFillStyle::default()` collapses all
3771        // geometry onto the origin instead of drawing it 1:1.
3772        let node = SvgNode::Rect(rect(0.0, 0.0, 4.0, 4.0));
3773        let mut zeroed = mask_image(4, 4);
3774        let mut identity = mask_image(4, 4);
3775        assert_eq!(
3776            render_node_clipmask_cpu(&mut zeroed, &node, SvgStyle::Fill(SvgFillStyle::default())),
3777            Some(())
3778        );
3779        assert_eq!(
3780            render_node_clipmask_cpu(
3781                &mut identity,
3782                &node,
3783                SvgStyle::Fill(SvgFillStyle {
3784                    transform: identity_transform(),
3785                    ..SvgFillStyle::default()
3786                })
3787            ),
3788            Some(())
3789        );
3790        assert_ne!(mask_bytes(&zeroed), mask_bytes(&identity));
3791    }
3792
3793    #[test]
3794    fn render_node_clipmask_cpu_stroke_style_renders_without_panicking() {
3795        let node = SvgNode::Circle(SvgCircle {
3796            center_x: 8.0,
3797            center_y: 8.0,
3798            radius: 4.0,
3799        });
3800        let style = SvgStyle::Stroke(SvgStrokeStyle {
3801            transform: identity_transform(),
3802            line_width: 2.0,
3803            ..SvgStrokeStyle::default()
3804        });
3805        let mut img = mask_image(16, 16);
3806        assert_eq!(render_node_clipmask_cpu(&mut img, &node, style), Some(()));
3807        assert_eq!(mask_bytes(&img).len(), 256);
3808    }
3809
3810    #[test]
3811    fn render_node_clipmask_cpu_geometry_far_outside_the_image_is_clipped_not_crashed() {
3812        let node = SvgNode::Rect(rect(-1.0e6, -1.0e6, 10.0, 10.0));
3813        let style = SvgStyle::Fill(SvgFillStyle {
3814            transform: identity_transform(),
3815            ..SvgFillStyle::default()
3816        });
3817        let mut img = mask_image(4, 4);
3818        assert_eq!(render_node_clipmask_cpu(&mut img, &node, style), Some(()));
3819        assert!(mask_bytes(&img).iter().all(|&b| b == 0));
3820    }
3821
3822    // ==================================================================
3823    // svgxmlnode_parse / svg_parse / ParsedSvg
3824    // ==================================================================
3825
3826    #[test]
3827    fn svg_parse_valid_minimal_input_keeps_the_bytes_verbatim() {
3828        let p = svg_parse(MINIMAL_SVG, SvgParseOptions::default()).expect("positive control");
3829        assert_eq!(p.svg_data.as_ref(), MINIMAL_SVG);
3830        assert!(p.run_destructor);
3831    }
3832
3833    #[test]
3834    fn svg_parse_invalid_utf8_is_rejected() {
3835        for bad in [
3836            &[0xFFu8, 0xFE, 0x00][..],
3837            &[0x80][..],
3838            &[0xED, 0xA0, 0x80][..], // encoded surrogate
3839            &[0xC0, 0x80][..],       // overlong NUL
3840        ] {
3841            assert_eq!(
3842                svg_parse(bad, SvgParseOptions::default()).err(),
3843                Some(SvgParseError::NotAnUtf8Str),
3844                "{bad:?}"
3845            );
3846        }
3847    }
3848
3849    #[test]
3850    fn svg_parse_unclosed_elements_are_rejected() {
3851        for bad in [&b"<svg"[..], &b"<svg>"[..], &b"<svg><g>"[..]] {
3852            assert_eq!(
3853                svg_parse(bad, SvgParseOptions::default()).err(),
3854                Some(SvgParseError::NoParserAvailable),
3855                "{bad:?}"
3856            );
3857        }
3858    }
3859
3860    #[test]
3861    fn svg_parse_accepts_input_that_is_not_svg_at_all() {
3862        // PINNED leniency: `svg_parse` only checks that the bytes are UTF-8 and
3863        // tokenize as XML — it never requires an <svg> root, so empty,
3864        // whitespace-only and plain-text input all come back Ok.
3865        for lenient in [
3866            &b""[..],
3867            &b"   "[..],
3868            &b"\t\n\r "[..],
3869            &b"garbage"[..],
3870            &b"</svg>"[..],
3871            &b"<html><body/></html>"[..],
3872        ] {
3873            let p = svg_parse(lenient, SvgParseOptions::default())
3874                .unwrap_or_else(|e| panic!("{lenient:?} was rejected with {e:?}"));
3875            assert_eq!(p.svg_data.as_ref(), lenient);
3876        }
3877    }
3878
3879    #[test]
3880    fn svg_parse_garbage_never_panics_and_never_invents_data() {
3881        for data in [
3882            &b"<<<<<<"[..],
3883            &b"\x00\x01\x02\x03"[..],
3884            &b"{\"json\": true}"[..],
3885            &b"<svg <<>>"[..],
3886            &b"&&&;;;"[..],
3887            &b"<!--"[..],
3888            &b"<?xml"[..],
3889            &b"<!DOCTYPE"[..],
3890        ] {
3891            match svg_parse(data, SvgParseOptions::default()) {
3892                Ok(p) => assert_eq!(p.svg_data.as_ref(), data, "{data:?}"),
3893                Err(e) => assert_eq!(e, SvgParseError::NoParserAvailable, "{data:?}"),
3894            }
3895        }
3896    }
3897
3898    #[test]
3899    fn svg_parse_boundary_numeric_attributes_are_kept_as_opaque_strings() {
3900        let src = concat!(
3901            r#"<svg width="1e400" height="-0" a="9223372036854775807" "#,
3902            r#"b="NaN" c="inf" d="0"><rect width="0"/></svg>"#
3903        );
3904        let p = ParsedSvg::from_string(src, SvgParseOptions::default()).expect("parse");
3905        assert_eq!(p.to_string(SvgXmlOptions::default()), src);
3906    }
3907
3908    #[test]
3909    fn svg_parse_unicode_payloads_survive_untouched() {
3910        for s in [
3911            "<svg><text>\u{1F600}</text></svg>",
3912            "<svg><text>e\u{0301}\u{0328}</text></svg>",
3913            "<svg id=\"\u{65E5}\u{672C}\u{8A9E}\"/>",
3914            "<svg>\u{200b}\u{1F600}</svg>",
3915        ] {
3916            let p = ParsedSvg::from_string(s, SvgParseOptions::default())
3917                .unwrap_or_else(|e| panic!("{s:?} -> {e:?}"));
3918            assert_eq!(p.to_string(SvgXmlOptions::default()), s);
3919        }
3920    }
3921
3922    #[test]
3923    fn svg_parse_leading_and_trailing_junk_is_tolerated_and_preserved() {
3924        let src = "  <svg/>  trailing;garbage";
3925        let p = ParsedSvg::from_string(src, SvgParseOptions::default()).expect("lenient parse");
3926        // The stored bytes are never trimmed — to_string gives back the original.
3927        assert_eq!(p.to_string(SvgXmlOptions::default()), src);
3928    }
3929
3930    #[test]
3931    fn svg_parse_extremely_long_input_does_not_hang() {
3932        let long = format!("<svg>{}</svg>", "a".repeat(1_000_000));
3933        let p = ParsedSvg::from_string(&long, SvgParseOptions::default())
3934            .expect("1 MB of text must parse");
3935        assert_eq!(p.svg_data.as_ref().len(), long.len());
3936    }
3937
3938    #[test]
3939    fn svg_parse_many_sibling_elements_does_not_hang() {
3940        let mut s = String::from("<svg>");
3941        for _ in 0..50_000 {
3942            s.push_str("<g/>");
3943        }
3944        s.push_str("</svg>");
3945        assert!(ParsedSvg::from_string(&s, SvgParseOptions::default()).is_ok());
3946    }
3947
3948    #[test]
3949    fn svg_parse_deeply_nested_input_does_not_stack_overflow() {
3950        // The tokenizer loop is iterative, but the XmlNode tree it builds is torn
3951        // down recursively — run on a big stack so a genuinely linear-depth drop
3952        // is proven safe instead of coin-flipping on the 2 MiB default test stack.
3953        let child = std::thread::Builder::new()
3954            .stack_size(128 * 1024 * 1024)
3955            .spawn(|| {
3956                const DEPTH: usize = 10_000;
3957                let mut s = String::from("<svg>");
3958                for _ in 0..DEPTH {
3959                    s.push_str("<g>");
3960                }
3961                for _ in 0..DEPTH {
3962                    s.push_str("</g>");
3963                }
3964                s.push_str("</svg>");
3965                ParsedSvg::from_string(&s, SvgParseOptions::default()).is_ok()
3966            })
3967            .expect("spawn");
3968        assert!(child
3969            .join()
3970            .expect("10k-deep nesting must not overflow the stack"));
3971    }
3972
3973    #[test]
3974    fn svgxmlnode_parse_mirrors_svg_parse_acceptance() {
3975        assert!(svgxmlnode_parse(MINIMAL_SVG, SvgParseOptions::default()).is_ok());
3976        assert!(svgxmlnode_parse(b"", SvgParseOptions::default()).is_ok());
3977        assert_eq!(
3978            svgxmlnode_parse(&[0xFF, 0xFE], SvgParseOptions::default()).err(),
3979            Some(SvgParseError::NotAnUtf8Str)
3980        );
3981        assert_eq!(
3982            svgxmlnode_parse(b"<svg>", SvgParseOptions::default()).err(),
3983            Some(SvgParseError::NoParserAvailable)
3984        );
3985        let node = svgxmlnode_parse(MINIMAL_SVG, SvgParseOptions::default()).expect("parse");
3986        assert!(node.run_destructor);
3987    }
3988
3989    #[test]
3990    fn parsed_svg_round_trips_through_to_string_and_back() {
3991        let src = r#"<svg viewBox="0 0 8 8"><rect width="8" height="8"/></svg>"#;
3992        let once = ParsedSvg::from_string(src, SvgParseOptions::default()).expect("parse");
3993        let text = once.to_string(SvgXmlOptions::default());
3994        assert_eq!(text, src);
3995        let twice = ParsedSvg::from_string(&text, SvgParseOptions::default()).expect("re-parse");
3996        assert_eq!(
3997            twice.to_string(SvgXmlOptions::default()),
3998            text,
3999            "serialisation must be idempotent"
4000        );
4001        assert_eq!(svg_to_string(&twice, SvgXmlOptions::default()), text);
4002    }
4003
4004    #[test]
4005    fn parsed_svg_from_bytes_and_from_string_agree() {
4006        let a = ParsedSvg::from_bytes(MINIMAL_SVG, SvgParseOptions::default()).expect("bytes");
4007        let b = ParsedSvg::from_string(
4008            core::str::from_utf8(MINIMAL_SVG).expect("ascii"),
4009            SvgParseOptions::default(),
4010        )
4011        .expect("string");
4012        assert_eq!(a.svg_data.as_ref(), b.svg_data.as_ref());
4013    }
4014
4015    #[test]
4016    fn parsed_svg_to_string_replaces_invalid_utf8_instead_of_panicking() {
4017        // `ParsedSvg` is a plain #[repr(C)] FFI struct, so it can hold bytes that
4018        // `svg_parse` would have rejected; `to_string` must stay lossy, not panic.
4019        let p = ParsedSvg {
4020            svg_data: vec![0xFFu8, 0xFE].into(),
4021            run_destructor: true,
4022        };
4023        assert_eq!(
4024            p.to_string(SvgXmlOptions::default()),
4025            "\u{FFFD}\u{FFFD}",
4026            "each invalid byte becomes one replacement char"
4027        );
4028        assert_eq!(
4029            svg_to_string(&p, SvgXmlOptions::default()),
4030            p.to_string(SvgXmlOptions::default())
4031        );
4032    }
4033
4034    #[test]
4035    fn parsed_svg_to_string_of_an_empty_document_is_empty() {
4036        let p = ParsedSvg {
4037            svg_data: Vec::new().into(),
4038            run_destructor: true,
4039        };
4040        assert!(p.to_string(SvgXmlOptions::default()).is_empty());
4041        assert_eq!(format!("{p:?}"), "ParsedSvg(0 bytes)");
4042    }
4043
4044    #[test]
4045    fn parsed_svg_debug_reports_the_byte_length() {
4046        let p = ParsedSvg {
4047            svg_data: vec![1u8, 2, 3].into(),
4048            run_destructor: true,
4049        };
4050        assert_eq!(format!("{p:?}"), "ParsedSvg(3 bytes)");
4051    }
4052
4053    #[test]
4054    fn svg_root_and_get_root_agree() {
4055        let p = svg_parse(MINIMAL_SVG, SvgParseOptions::default()).expect("parse");
4056        assert!(p.get_root().run_destructor);
4057        assert!(svg_root(&p).run_destructor);
4058        // …and stay well-defined for a document with no elements at all.
4059        let empty = svg_parse(b"", SvgParseOptions::default()).expect("lenient parse");
4060        assert!(empty.get_root().run_destructor);
4061    }
4062
4063    // ==================================================================
4064    // svg_render
4065    // ==================================================================
4066
4067    #[test]
4068    fn svg_render_zero_target_size_is_none() {
4069        let p = svg_parse(MINIMAL_SVG, SvgParseOptions::default()).expect("parse");
4070        for size in [
4071            LayoutSize::new(0, 0),
4072            LayoutSize::new(0, 8),
4073            LayoutSize::new(8, 0),
4074        ] {
4075            let opts = SvgRenderOptions {
4076                target_size: OptionLayoutSize::Some(size),
4077                ..SvgRenderOptions::default()
4078            };
4079            assert!(p.render(opts).is_none(), "{size:?}");
4080            assert!(svg_render(&p, opts).is_none(), "{size:?}");
4081        }
4082    }
4083
4084    #[cfg(feature = "cpurender")]
4085    #[test]
4086    fn svg_render_produces_an_image_of_the_requested_size() {
4087        let p = svg_parse(MINIMAL_SVG, SvgParseOptions::default()).expect("parse");
4088        let opts = SvgRenderOptions {
4089            target_size: OptionLayoutSize::Some(LayoutSize::new(8, 8)),
4090            ..SvgRenderOptions::default()
4091        };
4092        let img = p.render(opts).expect("a minimal <svg> must rasterize");
4093        assert_eq!((img.width, img.height), (8, 8));
4094        assert_eq!(img.data_format, RawImageFormat::RGBA8);
4095        assert!(!img.premultiplied_alpha);
4096    }
4097
4098    #[cfg(feature = "cpurender")]
4099    #[test]
4100    fn svg_render_of_a_document_without_an_svg_root_is_none() {
4101        // `svg_parse` accepts it, but rasterization has nothing to draw.
4102        let p = svg_parse(b"<html><body/></html>", SvgParseOptions::default())
4103            .expect("lenient parse");
4104        let opts = SvgRenderOptions {
4105            target_size: OptionLayoutSize::Some(LayoutSize::new(4, 4)),
4106            ..SvgRenderOptions::default()
4107        };
4108        assert!(p.render(opts).is_none());
4109    }
4110
4111    #[cfg(feature = "cpurender")]
4112    #[test]
4113    fn svg_render_one_by_one_target_is_the_smallest_valid_size() {
4114        let p = svg_parse(MINIMAL_SVG, SvgParseOptions::default()).expect("parse");
4115        let opts = SvgRenderOptions {
4116            target_size: OptionLayoutSize::Some(LayoutSize::new(1, 1)),
4117            ..SvgRenderOptions::default()
4118        };
4119        let img = p.render(opts).expect("1x1 must still rasterize");
4120        assert_eq!((img.width, img.height), (1, 1));
4121    }
4122}