Skip to main content

azul_core/
svg.rs

1//! SVG rendering and path tessellation.
2//!
3//! This module provides functionality for parsing, manipulating, and rendering SVG paths.
4//! It includes:
5//!
6//! - **Path tessellation**: Converts SVG paths into triangle meshes for GPU rendering
7//! - **Stroke generation**: Creates stroked paths with various line join and cap styles
8//! - **Transform support**: Applies CSS transforms to SVG elements
9//! - **Style parsing**: Handles SVG fill, stroke, opacity, and other attributes
10//!
11//! The module uses Lyon for geometric tessellation and generates vertex/index buffers
12//! that can be uploaded to WebRender for hardware-accelerated rendering.
13
14use alloc::{
15    string::{String, ToString},
16    vec::Vec,
17};
18use core::fmt;
19
20use azul_css::{
21    props::{
22        basic::{
23            ColorF, ColorU, OptionColorU, OptionLayoutSize, PixelValue, SvgCubicCurve, SvgPoint,
24            SvgQuadraticCurve, SvgRect, SvgVector,
25        },
26        style::{StyleTransform, StyleTransformOrigin, StyleTransformVec},
27    },
28    AzString, OptionString, StringVec, U32Vec,
29};
30
31use crate::{
32    geom::PhysicalSizeU32,
33    gl::{
34        GlContextPtr, GlShader, IndexBufferFormat, Texture, Uniform, UniformType, VertexAttribute,
35        VertexAttributeType, VertexBuffer, VertexLayout, VertexLayoutDescription,
36    },
37    transform::{ComputedTransform3D, RotationMode},
38    xml::XmlError,
39};
40
41/// Default miter limit for stroke joins (ratio of miter length to stroke width)
42const DEFAULT_MITER_LIMIT: f32 = 4.0;
43/// Default stroke width in pixels
44const DEFAULT_LINE_WIDTH: f32 = 1.0;
45/// Default tessellation tolerance in pixels (smaller = more vertices, higher quality)
46const DEFAULT_TOLERANCE: f32 = 0.1;
47
48/// Represents the dimensions of an SVG viewport or element.
49#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
50#[repr(C)]
51pub struct SvgSize {
52    /// Width in SVG user units
53    pub width: f32,
54    /// Height in SVG user units
55    pub height: f32,
56}
57
58/// A line segment in 2D space.
59#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
60#[repr(C)]
61pub struct SvgLine {
62    /// Start point of the line
63    pub start: SvgPoint,
64    /// End point of the line
65    pub end: SvgPoint,
66}
67
68impl SvgLine {
69    /// Creates a new line segment from start to end point
70    #[inline]
71    #[must_use] pub const fn new(start: SvgPoint, end: SvgPoint) -> Self {
72        Self { start, end }
73    }
74
75    /// Computes the inward-facing normal vector for this line.
76    ///
77    /// The normal points 90 degrees to the right of the line direction.
78    /// Returns `None` if the line has zero length.
79    #[must_use] pub fn inwards_normal(&self) -> Option<SvgPoint> {
80        let dx = self.end.x - self.start.x;
81        let dy = self.end.y - self.start.y;
82        let edge_length = dx.hypot(dy);
83        let x = -dy / edge_length;
84        let y = dx / edge_length;
85
86        if x.is_finite() && y.is_finite() {
87            Some(SvgPoint { x, y })
88        } else {
89            None
90        }
91    }
92
93    /// Computes the outward-facing normal vector for this line (opposite of `inwards_normal`).
94    #[must_use] pub fn outwards_normal(&self) -> Option<SvgPoint> {
95        let inwards = self.inwards_normal()?;
96        Some(SvgPoint {
97            x: -inwards.x,
98            y: -inwards.y,
99        })
100    }
101
102    /// Reverses the direction of the line by swapping start and end points.
103    pub const fn reverse(&mut self) {
104        core::mem::swap(&mut self.start, &mut self.end);
105    }
106    /// Returns the start point of the line.
107    #[must_use] pub const fn get_start(&self) -> SvgPoint {
108        self.start
109    }
110    /// Returns the end point of the line.
111    #[must_use] pub const fn get_end(&self) -> SvgPoint {
112        self.end
113    }
114
115    /// Returns the parametric `t` value (0.0–1.0) at the given arc-length offset.
116    #[must_use] pub fn get_t_at_offset(&self, offset: f64) -> f64 {
117        offset / self.get_length()
118    }
119
120    /// Returns the tangent vector of the line.
121    /// For a line, the tangent is constant (same direction everywhere),
122    /// so no `t` parameter is needed.
123    #[must_use] pub fn get_tangent_vector_at_t(&self) -> SvgVector {
124        let dx = self.end.x - self.start.x;
125        let dy = self.end.y - self.start.y;
126        SvgVector {
127            x: f64::from(dx),
128            y: f64::from(dy),
129        }
130        .normalize()
131    }
132
133    /// Returns the X coordinate at parametric position `t` (0.0 = start, 1.0 = end).
134    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
135    #[must_use] pub fn get_x_at_t(&self, t: f64) -> f64 {
136        f64::from(self.start.x) + (f64::from(self.end.x) - f64::from(self.start.x)) * t
137    }
138
139    /// Returns the Y coordinate at parametric position `t` (0.0 = start, 1.0 = end).
140    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
141    #[must_use] pub fn get_y_at_t(&self, t: f64) -> f64 {
142        f64::from(self.start.y) + (f64::from(self.end.y) - f64::from(self.start.y)) * t
143    }
144
145    /// Returns the Euclidean length of the line segment.
146    #[must_use] pub fn get_length(&self) -> f64 {
147        let dx = self.end.x - self.start.x;
148        let dy = self.end.y - self.start.y;
149        f64::from(libm::hypotf(dx, dy))
150    }
151
152    /// Returns the axis-aligned bounding rectangle of this line segment.
153    #[must_use] pub fn get_bounds(&self) -> SvgRect {
154        let min_x = self.start.x.min(self.end.x);
155        let max_x = self.start.x.max(self.end.x);
156
157        let min_y = self.start.y.min(self.end.y);
158        let max_y = self.start.y.max(self.end.y);
159
160        let width = (max_x - min_x).abs();
161        let height = (max_y - min_y).abs();
162
163        SvgRect {
164            width,
165            height,
166            x: min_x,
167            y: min_y,
168            radius_top_left: 0.0,
169            radius_top_right: 0.0,
170            radius_bottom_left: 0.0,
171            radius_bottom_right: 0.0,
172        }
173    }
174}
175
176#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
177#[repr(C, u8)]
178pub enum SvgPathElement {
179    Line(SvgLine),
180    QuadraticCurve(SvgQuadraticCurve),
181    CubicCurve(SvgCubicCurve),
182}
183
184impl_option!(
185    SvgPathElement,
186    OptionSvgPathElement,
187    [Debug, Copy, Clone, PartialEq, PartialOrd]
188);
189
190impl SvgPathElement {
191    /// Creates a line path element from a `SvgLine`
192    #[inline]
193    #[must_use] pub const fn line(l: SvgLine) -> Self {
194        Self::Line(l)
195    }
196
197    /// Creates a quadratic curve path element from a `SvgQuadraticCurve`
198    #[inline]
199    #[must_use] pub const fn quadratic_curve(qc: SvgQuadraticCurve) -> Self {
200        Self::QuadraticCurve(qc)
201    }
202
203    /// Creates a cubic curve path element from a `SvgCubicCurve`
204    #[inline]
205    #[must_use] pub const fn cubic_curve(cc: SvgCubicCurve) -> Self {
206        Self::CubicCurve(cc)
207    }
208
209    /// Sets the end point of this path element.
210    pub const fn set_last(&mut self, point: SvgPoint) {
211        match self {
212            Self::Line(l) => l.end = point,
213            Self::QuadraticCurve(qc) => qc.end = point,
214            Self::CubicCurve(cc) => cc.end = point,
215        }
216    }
217
218    /// Sets the start point of this path element.
219    pub const fn set_first(&mut self, point: SvgPoint) {
220        match self {
221            Self::Line(l) => l.start = point,
222            Self::QuadraticCurve(qc) => qc.start = point,
223            Self::CubicCurve(cc) => cc.start = point,
224        }
225    }
226
227    /// Reverses the direction of this path element.
228    pub const fn reverse(&mut self) {
229        match self {
230            Self::Line(l) => l.reverse(),
231            Self::QuadraticCurve(qc) => qc.reverse(),
232            Self::CubicCurve(cc) => cc.reverse(),
233        }
234    }
235    /// Returns the start point of this path element.
236    #[must_use] pub const fn get_start(&self) -> SvgPoint {
237        match self {
238            Self::Line(l) => l.get_start(),
239            Self::QuadraticCurve(qc) => qc.get_start(),
240            Self::CubicCurve(cc) => cc.get_start(),
241        }
242    }
243    /// Returns the end point of this path element.
244    #[must_use] pub const fn get_end(&self) -> SvgPoint {
245        match self {
246            Self::Line(l) => l.get_end(),
247            Self::QuadraticCurve(qc) => qc.get_end(),
248            Self::CubicCurve(cc) => cc.get_end(),
249        }
250    }
251    /// Returns the axis-aligned bounding rectangle of this path element.
252    #[must_use] pub fn get_bounds(&self) -> SvgRect {
253        match self {
254            Self::Line(l) => l.get_bounds(),
255            Self::QuadraticCurve(qc) => qc.get_bounds(),
256            Self::CubicCurve(cc) => cc.get_bounds(),
257        }
258    }
259    /// Returns the arc length of this path element.
260    #[must_use] pub fn get_length(&self) -> f64 {
261        match self {
262            Self::Line(l) => l.get_length(),
263            Self::QuadraticCurve(qc) => qc.get_length(),
264            Self::CubicCurve(cc) => cc.get_length(),
265        }
266    }
267    /// Returns the parametric `t` value at the given arc-length offset.
268    #[must_use] pub fn get_t_at_offset(&self, offset: f64) -> f64 {
269        match self {
270            Self::Line(l) => l.get_t_at_offset(offset),
271            Self::QuadraticCurve(qc) => qc.get_t_at_offset(offset),
272            Self::CubicCurve(cc) => cc.get_t_at_offset(offset),
273        }
274    }
275    /// Returns the normalized tangent vector at parametric position `t`.
276    #[must_use] pub fn get_tangent_vector_at_t(&self, t: f64) -> SvgVector {
277        match self {
278            Self::Line(l) => l.get_tangent_vector_at_t(),
279            Self::QuadraticCurve(qc) => qc.get_tangent_vector_at_t(t),
280            Self::CubicCurve(cc) => cc.get_tangent_vector_at_t(t),
281        }
282    }
283    /// Returns the X coordinate at parametric position `t`.
284    #[must_use] pub fn get_x_at_t(&self, t: f64) -> f64 {
285        match self {
286            Self::Line(l) => l.get_x_at_t(t),
287            Self::QuadraticCurve(qc) => qc.get_x_at_t(t),
288            Self::CubicCurve(cc) => cc.get_x_at_t(t),
289        }
290    }
291    /// Returns the Y coordinate at parametric position `t`.
292    #[must_use] pub fn get_y_at_t(&self, t: f64) -> f64 {
293        match self {
294            Self::Line(l) => l.get_y_at_t(t),
295            Self::QuadraticCurve(qc) => qc.get_y_at_t(t),
296            Self::CubicCurve(cc) => cc.get_y_at_t(t),
297        }
298    }
299}
300
301impl_vec!(SvgPathElement, SvgPathElementVec, SvgPathElementVecDestructor, SvgPathElementVecDestructorType, SvgPathElementVecSlice, OptionSvgPathElement);
302impl_vec_debug!(SvgPathElement, SvgPathElementVec);
303impl_vec_clone!(
304    SvgPathElement,
305    SvgPathElementVec,
306    SvgPathElementVecDestructor
307);
308impl_vec_partialeq!(SvgPathElement, SvgPathElementVec);
309impl_vec_partialord!(SvgPathElement, SvgPathElementVec);
310
311#[derive(Debug, Clone, PartialEq, PartialOrd)]
312#[repr(C)]
313pub struct SvgPath {
314    pub items: SvgPathElementVec,
315}
316
317impl_option!(
318    SvgPath,
319    OptionSvgPath,
320    copy = false,
321    [Debug, Clone, PartialEq, PartialOrd]
322);
323
324impl SvgPath {
325    /// Creates a new `SvgPath` from a vector of path elements
326    #[inline]
327    #[must_use] pub const fn create(items: SvgPathElementVec) -> Self {
328        Self { items }
329    }
330
331    /// Returns the start point of the first element, or `None` if the path is empty.
332    #[must_use] pub fn get_start(&self) -> Option<SvgPoint> {
333        self.items.as_ref().first().map(SvgPathElement::get_start)
334    }
335
336    /// Returns the end point of the last element, or `None` if the path is empty.
337    #[must_use] pub fn get_end(&self) -> Option<SvgPoint> {
338        self.items.as_ref().last().map(SvgPathElement::get_end)
339    }
340
341    /// Closes the path by appending a line from the last point to the first point, if needed.
342    pub fn close(&mut self) {
343        let Some(first) = self.items.as_ref().first() else {
344            return;
345        };
346        let Some(last) = self.items.as_ref().last() else {
347            return;
348        };
349        if first.get_start() != last.get_end() {
350            let mut elements = self.items.as_slice().to_vec();
351            elements.push(SvgPathElement::Line(SvgLine {
352                start: last.get_end(),
353                end: first.get_start(),
354            }));
355            self.items = elements.into();
356        }
357    }
358
359    /// Returns `true` if the path's first start point equals its last end point.
360    #[must_use] pub fn is_closed(&self) -> bool {
361        let first = self.items.as_ref().first();
362        let last = self.items.as_ref().last();
363        match (first, last) {
364            (Some(f), Some(l)) => (f.get_start() == l.get_end()),
365            _ => false,
366        }
367    }
368
369    /// Reverses the order and direction of all elements in the path.
370    pub fn reverse(&mut self) {
371        // swap self.items with a default vec
372        let mut vec = SvgPathElementVec::from_const_slice(&[]);
373        core::mem::swap(&mut vec, &mut self.items);
374        let mut vec = vec.into_library_owned_vec();
375
376        // reverse the order of items in the vec
377        vec.reverse();
378
379        // reverse the order inside the item itself
380        // i.e. swap line.start and line.end
381        for item in &mut vec {
382            item.reverse();
383        }
384
385        // swap back
386        let mut vec = SvgPathElementVec::from_vec(vec);
387        core::mem::swap(&mut vec, &mut self.items);
388    }
389
390    /// Joins another path onto the end of this one, interpolating the join point.
391    pub fn join_with(&mut self, mut path: Self) -> Option<()> {
392        let self_last_point = self.items.as_ref().last()?.get_end();
393        let other_start_point = path.items.as_ref().first()?.get_start();
394        let interpolated_join_point = SvgPoint {
395            x: f32::midpoint(self_last_point.x, other_start_point.x),
396            y: f32::midpoint(self_last_point.y, other_start_point.y),
397        };
398
399        // swap self.items with a default vec
400        let mut vec = SvgPathElementVec::from_const_slice(&[]);
401        core::mem::swap(&mut vec, &mut self.items);
402        let mut vec = vec.into_library_owned_vec();
403
404        let mut other = SvgPathElementVec::from_const_slice(&[]);
405        core::mem::swap(&mut other, &mut path.items);
406        let mut other = other.into_library_owned_vec();
407
408        let vec_len = vec.len() - 1;
409        vec.get_mut(vec_len)?.set_last(interpolated_join_point);
410        other.get_mut(0)?.set_first(interpolated_join_point);
411        vec.append(&mut other);
412
413        // swap back
414        let mut vec = SvgPathElementVec::from_vec(vec);
415        core::mem::swap(&mut vec, &mut self.items);
416
417        Some(())
418    }
419    /// Returns the axis-aligned bounding rectangle of the entire path.
420    #[must_use] pub fn get_bounds(&self) -> SvgRect {
421        let mut first_bounds = match self.items.as_ref().first() {
422            Some(s) => s.get_bounds(),
423            None => return SvgRect::default(),
424        };
425
426        for mp in self.items.as_ref().iter().skip(1) {
427            let mp_bounds = mp.get_bounds();
428            first_bounds.union_with(&mp_bounds);
429        }
430
431        first_bounds
432    }
433}
434
435#[derive(Debug, Clone, PartialEq, PartialOrd)]
436#[repr(C)]
437pub struct SvgMultiPolygon {
438    /// NOTE: If a ring represents a hole, simply reverse the order of points
439    pub rings: SvgPathVec,
440}
441
442impl_option!(
443    SvgMultiPolygon,
444    OptionSvgMultiPolygon,
445    copy = false,
446    [Debug, Clone, PartialEq, PartialOrd]
447);
448
449impl SvgMultiPolygon {
450    /// Creates a new `SvgMultiPolygon` from a vector of paths (rings)
451    /// NOTE: If a ring represents a hole, simply reverse the order of points
452    #[inline]
453    #[must_use] pub const fn create(rings: SvgPathVec) -> Self {
454        Self { rings }
455    }
456
457    /// Returns the axis-aligned bounding rectangle of all rings in this multi-polygon.
458    #[must_use] pub fn get_bounds(&self) -> SvgRect {
459        let Some(mut first_bounds) = self
460            .rings
461            .get(0)
462            .and_then(|b| b.items.get(0).map(SvgPathElement::get_bounds))
463        else {
464            // Empty polygon has zero-sized bounds at origin
465            return SvgRect::default();
466        };
467
468        for ring in &self.rings {
469            for item in &ring.items {
470                first_bounds.union_with(&item.get_bounds());
471            }
472        }
473
474        first_bounds
475    }
476}
477
478impl_vec!(SvgPath, SvgPathVec, SvgPathVecDestructor, SvgPathVecDestructorType, SvgPathVecSlice, OptionSvgPath);
479impl_vec_debug!(SvgPath, SvgPathVec);
480impl_vec_clone!(SvgPath, SvgPathVec, SvgPathVecDestructor);
481impl_vec_partialeq!(SvgPath, SvgPathVec);
482impl_vec_partialord!(SvgPath, SvgPathVec);
483
484impl_vec!(SvgMultiPolygon, SvgMultiPolygonVec, SvgMultiPolygonVecDestructor, SvgMultiPolygonVecDestructorType, SvgMultiPolygonVecSlice, OptionSvgMultiPolygon);
485impl_vec_debug!(SvgMultiPolygon, SvgMultiPolygonVec);
486impl_vec_clone!(
487    SvgMultiPolygon,
488    SvgMultiPolygonVec,
489    SvgMultiPolygonVecDestructor
490);
491impl_vec_partialeq!(SvgMultiPolygon, SvgMultiPolygonVec);
492impl_vec_partialord!(SvgMultiPolygon, SvgMultiPolygonVec);
493
494/// One `SvgNode` corresponds to one SVG `<path></path>` element
495#[derive(Debug, Clone, PartialOrd, PartialEq)]
496#[repr(C, u8)]
497pub enum SvgNode {
498    /// Multiple multipolygons, merged to one CPU buf for efficient drawing
499    MultiPolygonCollection(SvgMultiPolygonVec),
500    MultiPolygon(SvgMultiPolygon),
501    MultiShape(SvgSimpleNodeVec),
502    Path(SvgPath),
503    Circle(SvgCircle),
504    Rect(SvgRect),
505}
506
507/// One `SvgSimpleNode` is either a path, a rect or a circle
508#[derive(Debug, Clone, PartialOrd, PartialEq)]
509#[repr(C, u8)]
510pub enum SvgSimpleNode {
511    Path(SvgPath),
512    Circle(SvgCircle),
513    Rect(SvgRect),
514    CircleHole(SvgCircle),
515    RectHole(SvgRect),
516}
517
518impl_option!(
519    SvgSimpleNode,
520    OptionSvgSimpleNode,
521    copy = false,
522    [Debug, Clone, PartialOrd, PartialEq]
523);
524
525impl_vec!(SvgSimpleNode, SvgSimpleNodeVec, SvgSimpleNodeVecDestructor, SvgSimpleNodeVecDestructorType, SvgSimpleNodeVecSlice, OptionSvgSimpleNode);
526impl_vec_debug!(SvgSimpleNode, SvgSimpleNodeVec);
527impl_vec_clone!(SvgSimpleNode, SvgSimpleNodeVec, SvgSimpleNodeVecDestructor);
528impl_vec_partialeq!(SvgSimpleNode, SvgSimpleNodeVec);
529impl_vec_partialord!(SvgSimpleNode, SvgSimpleNodeVec);
530
531impl SvgSimpleNode {
532    /// Returns the axis-aligned bounding rectangle of this node.
533    // Same-body arms dispatch on differently-typed bindings (SvgPath vs SvgCircle),
534    // so the identical `a.get_bounds()` bodies cannot be combined into one or-pattern.
535    #[allow(clippy::match_same_arms)]
536    #[must_use] pub fn get_bounds(&self) -> SvgRect {
537        match self {
538            Self::Path(a) => a.get_bounds(),
539            Self::Circle(a) => a.get_bounds(),
540            Self::Rect(a) => *a,
541            Self::CircleHole(a) => a.get_bounds(),
542            Self::RectHole(a) => *a,
543        }
544    }
545    /// Returns `true` if this node represents a closed shape.
546    #[must_use] pub fn is_closed(&self) -> bool {
547        match self {
548            Self::Path(a) => a.is_closed(),
549            Self::Circle(_) | Self::Rect(_) | Self::CircleHole(_) | Self::RectHole(_) => true,
550        }
551    }
552}
553
554impl SvgNode {
555    /// Returns the axis-aligned bounding rectangle of this SVG node.
556    #[must_use] pub fn get_bounds(&self) -> SvgRect {
557        match self {
558            Self::MultiPolygonCollection(a) => {
559                let mut first_mp_bounds = match a.get(0) {
560                    Some(s) => s.get_bounds(),
561                    None => return SvgRect::default(),
562                };
563                for mp in a.iter().skip(1) {
564                    let mp_bounds = mp.get_bounds();
565                    first_mp_bounds.union_with(&mp_bounds);
566                }
567
568                first_mp_bounds
569            }
570            Self::MultiPolygon(a) => a.get_bounds(),
571            Self::MultiShape(a) => {
572                let mut first_mp_bounds = match a.get(0) {
573                    Some(s) => s.get_bounds(),
574                    None => return SvgRect::default(),
575                };
576                for mp in a.iter().skip(1) {
577                    let mp_bounds = mp.get_bounds();
578                    first_mp_bounds.union_with(&mp_bounds);
579                }
580
581                first_mp_bounds
582            }
583            Self::Path(a) => a.get_bounds(),
584            Self::Circle(a) => a.get_bounds(),
585            Self::Rect(a) => *a,
586        }
587    }
588    /// Returns `true` if all sub-paths in this node are closed.
589    #[must_use] pub fn is_closed(&self) -> bool {
590        match self {
591            Self::MultiPolygonCollection(a) => {
592                for mp in a {
593                    for p in mp.rings.as_ref() {
594                        if !p.is_closed() {
595                            return false;
596                        }
597                    }
598                }
599
600                true
601            }
602            Self::MultiPolygon(a) => {
603                for p in a.rings.as_ref() {
604                    if !p.is_closed() {
605                        return false;
606                    }
607                }
608
609                true
610            }
611            Self::MultiShape(a) => {
612                for p in a.as_ref() {
613                    if !p.is_closed() {
614                        return false;
615                    }
616                }
617
618                true
619            }
620            Self::Path(a) => a.is_closed(),
621            Self::Circle(_) | Self::Rect(_) => true,
622        }
623    }
624}
625
626/// An SVG node paired with its visual style (fill or stroke).
627#[derive(Debug, Clone, PartialOrd, PartialEq)]
628#[repr(C)]
629pub struct SvgStyledNode {
630    pub geometry: SvgNode,
631    pub style: SvgStyle,
632}
633
634/// A 2D vertex used in tessellated SVG geometry.
635#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
636#[repr(C)]
637pub struct SvgVertex {
638    pub x: f32,
639    pub y: f32,
640}
641
642impl_option!(
643    SvgVertex,
644    OptionSvgVertex,
645    [Debug, Copy, Clone, PartialOrd, PartialEq]
646);
647
648impl VertexLayoutDescription for SvgVertex {
649    fn get_description() -> VertexLayout {
650        VertexLayout {
651            fields: vec![VertexAttribute {
652                va_name: String::from("vAttrXY").into(),
653                layout_location: None.into(),
654                attribute_type: VertexAttributeType::Float,
655                item_count: 2,
656            }]
657            .into(),
658        }
659    }
660}
661
662/// A 3D vertex with per-vertex RGBA color, used in multi-colored SVG tessellation.
663#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
664#[repr(C)]
665pub struct SvgColoredVertex {
666    pub x: f32,
667    pub y: f32,
668    pub z: f32,
669    pub r: f32,
670    pub g: f32,
671    pub b: f32,
672    pub a: f32,
673}
674
675impl_option!(
676    SvgColoredVertex,
677    OptionSvgColoredVertex,
678    [Debug, Copy, Clone, PartialOrd, PartialEq]
679);
680
681impl VertexLayoutDescription for SvgColoredVertex {
682    fn get_description() -> VertexLayout {
683        VertexLayout {
684            fields: vec![
685                VertexAttribute {
686                    va_name: String::from("vAttrXY").into(),
687                    layout_location: None.into(),
688                    attribute_type: VertexAttributeType::Float,
689                    item_count: 3,
690                },
691                VertexAttribute {
692                    va_name: String::from("vColor").into(),
693                    layout_location: None.into(),
694                    attribute_type: VertexAttributeType::Float,
695                    item_count: 4,
696                },
697            ]
698            .into(),
699        }
700    }
701}
702
703/// A circle defined by center coordinates and radius.
704#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
705#[repr(C)]
706pub struct SvgCircle {
707    pub center_x: f32,
708    pub center_y: f32,
709    pub radius: f32,
710}
711
712impl SvgCircle {
713    /// Returns `true` if the given point lies inside the circle.
714    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
715    #[must_use] pub fn contains_point(&self, x: f32, y: f32) -> bool {
716        let x_diff = libm::fabsf(x - self.center_x);
717        let y_diff = libm::fabsf(y - self.center_y);
718        (x_diff * x_diff) + (y_diff * y_diff) < (self.radius * self.radius)
719    }
720    /// Returns the axis-aligned bounding rectangle of this circle.
721    #[must_use] pub fn get_bounds(&self) -> SvgRect {
722        SvgRect {
723            width: self.radius * 2.0,
724            height: self.radius * 2.0,
725            x: self.center_x - self.radius,
726            y: self.center_y - self.radius,
727            radius_top_left: 0.0,
728            radius_top_right: 0.0,
729            radius_bottom_left: 0.0,
730            radius_bottom_right: 0.0,
731        }
732    }
733}
734
735#[derive(Debug, Clone, PartialEq, PartialOrd)]
736#[repr(C)]
737pub struct TessellatedSvgNode {
738    pub vertices: SvgVertexVec,
739    pub indices: U32Vec,
740}
741
742impl_option!(
743    TessellatedSvgNode,
744    OptionTessellatedSvgNode,
745    copy = false,
746    [Debug, Clone, PartialEq, PartialOrd]
747);
748
749impl Default for TessellatedSvgNode {
750    fn default() -> Self {
751        Self {
752            vertices: Vec::new().into(),
753            indices: Vec::new().into(),
754        }
755    }
756}
757
758impl_vec!(TessellatedSvgNode, TessellatedSvgNodeVec, TessellatedSvgNodeVecDestructor, TessellatedSvgNodeVecDestructorType, TessellatedSvgNodeVecSlice, OptionTessellatedSvgNode);
759impl_vec_debug!(TessellatedSvgNode, TessellatedSvgNodeVec);
760impl_vec_partialord!(TessellatedSvgNode, TessellatedSvgNodeVec);
761impl_vec_clone!(
762    TessellatedSvgNode,
763    TessellatedSvgNodeVec,
764    TessellatedSvgNodeVecDestructor
765);
766impl_vec_partialeq!(TessellatedSvgNode, TessellatedSvgNodeVec);
767
768impl TessellatedSvgNode {
769    #[must_use] pub fn empty() -> Self {
770        Self::default()
771    }
772}
773
774impl TessellatedSvgNodeVec {
775    #[must_use] pub fn get_ref(&self) -> TessellatedSvgNodeVecRef {
776        let slice = self.as_ref();
777        TessellatedSvgNodeVecRef {
778            ptr: slice.as_ptr(),
779            len: slice.len(),
780        }
781    }
782}
783
784impl fmt::Debug for TessellatedSvgNodeVecRef {
785    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
786        self.as_slice().fmt(f)
787    }
788}
789
790// C ABI wrapper over &[TessellatedSvgNode]
791#[repr(C)]
792pub struct TessellatedSvgNodeVecRef {
793    pub ptr: *const TessellatedSvgNode,
794    pub len: usize,
795}
796
797impl Clone for TessellatedSvgNodeVecRef {
798    fn clone(&self) -> Self {
799        Self {
800            ptr: self.ptr,
801            len: self.len,
802        }
803    }
804}
805
806impl TessellatedSvgNodeVecRef {
807    #[must_use] pub const fn as_slice(&self) -> &[TessellatedSvgNode] {
808        unsafe { core::slice::from_raw_parts(self.ptr, self.len) }
809    }
810}
811
812#[derive(Debug, Clone, PartialEq, PartialOrd)]
813#[repr(C)]
814pub struct TessellatedColoredSvgNode {
815    pub vertices: SvgColoredVertexVec,
816    pub indices: U32Vec,
817}
818
819impl_option!(
820    TessellatedColoredSvgNode,
821    OptionTessellatedColoredSvgNode,
822    copy = false,
823    [Debug, Clone, PartialEq, PartialOrd]
824);
825
826impl Default for TessellatedColoredSvgNode {
827    fn default() -> Self {
828        Self {
829            vertices: Vec::new().into(),
830            indices: Vec::new().into(),
831        }
832    }
833}
834
835impl_vec!(TessellatedColoredSvgNode, TessellatedColoredSvgNodeVec, TessellatedColoredSvgNodeVecDestructor, TessellatedColoredSvgNodeVecDestructorType, TessellatedColoredSvgNodeVecSlice, OptionTessellatedColoredSvgNode);
836impl_vec_debug!(TessellatedColoredSvgNode, TessellatedColoredSvgNodeVec);
837impl_vec_partialord!(TessellatedColoredSvgNode, TessellatedColoredSvgNodeVec);
838impl_vec_clone!(
839    TessellatedColoredSvgNode,
840    TessellatedColoredSvgNodeVec,
841    TessellatedColoredSvgNodeVecDestructor
842);
843impl_vec_partialeq!(TessellatedColoredSvgNode, TessellatedColoredSvgNodeVec);
844
845impl TessellatedColoredSvgNode {
846    #[must_use] pub fn empty() -> Self {
847        Self::default()
848    }
849}
850
851impl TessellatedColoredSvgNodeVec {
852    #[must_use] pub fn get_ref(&self) -> TessellatedColoredSvgNodeVecRef {
853        let slice = self.as_ref();
854        TessellatedColoredSvgNodeVecRef {
855            ptr: slice.as_ptr(),
856            len: slice.len(),
857        }
858    }
859}
860
861impl fmt::Debug for TessellatedColoredSvgNodeVecRef {
862    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
863        self.as_slice().fmt(f)
864    }
865}
866
867// C ABI wrapper over &[TessellatedColoredSvgNode]
868#[repr(C)]
869pub struct TessellatedColoredSvgNodeVecRef {
870    pub ptr: *const TessellatedColoredSvgNode,
871    pub len: usize,
872}
873
874impl Clone for TessellatedColoredSvgNodeVecRef {
875    fn clone(&self) -> Self {
876        Self {
877            ptr: self.ptr,
878            len: self.len,
879        }
880    }
881}
882
883impl TessellatedColoredSvgNodeVecRef {
884    #[must_use] pub const fn as_slice(&self) -> &[TessellatedColoredSvgNode] {
885        unsafe { core::slice::from_raw_parts(self.ptr, self.len) }
886    }
887}
888
889impl_vec!(SvgVertex, SvgVertexVec, SvgVertexVecDestructor, SvgVertexVecDestructorType, SvgVertexVecSlice, OptionSvgVertex);
890impl_vec_debug!(SvgVertex, SvgVertexVec);
891impl_vec_partialord!(SvgVertex, SvgVertexVec);
892impl_vec_clone!(SvgVertex, SvgVertexVec, SvgVertexVecDestructor);
893impl_vec_partialeq!(SvgVertex, SvgVertexVec);
894
895impl_vec!(SvgColoredVertex, SvgColoredVertexVec, SvgColoredVertexVecDestructor, SvgColoredVertexVecDestructorType, SvgColoredVertexVecSlice, OptionSvgColoredVertex);
896impl_vec_debug!(SvgColoredVertex, SvgColoredVertexVec);
897impl_vec_partialord!(SvgColoredVertex, SvgColoredVertexVec);
898impl_vec_clone!(
899    SvgColoredVertex,
900    SvgColoredVertexVec,
901    SvgColoredVertexVecDestructor
902);
903impl_vec_partialeq!(SvgColoredVertex, SvgColoredVertexVec);
904
905/// Computes the bbox size and transform matrix uniforms shared by SVG draw methods.
906///
907/// Converts `StyleTransform` list into column-major `[f32; 16]` for OpenGL,
908/// and packages it along with the bbox size uniform.
909// target_size is physical pixel dimensions (u32); GL uniforms are f32. Pixel
910// counts are always well within f32's exact-integer range (2^24), so the
911// precision loss the lint warns about cannot occur for any real render target.
912#[allow(clippy::cast_precision_loss)]
913fn compute_svg_transform_uniforms(
914    target_size: PhysicalSizeU32,
915    transforms: &[StyleTransform],
916) -> (Uniform, Uniform) {
917    let transform_origin = StyleTransformOrigin {
918        x: PixelValue::px(target_size.width as f32 / 2.0),
919        y: PixelValue::px(target_size.height as f32 / 2.0),
920    };
921
922    let computed_transform = ComputedTransform3D::from_style_transform_vec(
923        transforms,
924        &transform_origin,
925        target_size.width as f32,
926        target_size.height as f32,
927        RotationMode::ForWebRender,
928    );
929
930    // NOTE: OpenGL draws are column-major, while ComputedTransform3D
931    // is row-major! Need to transpose the matrix!
932    let m = computed_transform.get_column_major().m;
933    let matrix: [f32; 16] = core::array::from_fn(|i| m[i / 4][i % 4]);
934
935    let bbox_uniform = Uniform {
936        uniform_name: "vBboxSize".into(),
937        uniform_type: UniformType::FloatVec2([
938            target_size.width as f32,
939            target_size.height as f32,
940        ]),
941    };
942
943    let transform_uniform = Uniform {
944        uniform_name: "vTransformMatrix".into(),
945        uniform_type: UniformType::Matrix4 {
946            transpose: false,
947            matrix,
948        },
949    };
950
951    (bbox_uniform, transform_uniform)
952}
953
954#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
955#[repr(C)]
956pub struct TessellatedGPUSvgNode {
957    pub vertex_index_buffer: VertexBuffer,
958}
959
960impl TessellatedGPUSvgNode {
961    /// Uploads the tesselated SVG node to GPU memory
962    #[must_use] pub fn new(node: &TessellatedSvgNode, gl: GlContextPtr) -> Self {
963        let svg_shader_id = gl.ptr.svg_shader;
964        Self {
965            vertex_index_buffer: VertexBuffer::new(
966                gl,
967                svg_shader_id,
968                node.vertices.as_ref(),
969                node.indices.as_ref(),
970                IndexBufferFormat::Triangles,
971            ),
972        }
973    }
974
975    /// Draw the vertex buffer to the texture with the given color and transform
976    #[allow(clippy::needless_pass_by_value)] // owned azul value taken by value (public API / ownership-transfer convention)
977    pub fn draw(
978        &self,
979        texture: &mut Texture,
980        target_size: PhysicalSizeU32,
981        color: ColorU,
982        transforms: StyleTransformVec,
983    ) -> bool {
984        let (bbox_uniform, transform_uniform) =
985            compute_svg_transform_uniforms(target_size, transforms.as_ref());
986
987        let color: ColorF = color.into();
988
989        let uniforms = [
990            bbox_uniform,
991            Uniform {
992                uniform_name: "fDrawColor".into(),
993                uniform_type: UniformType::FloatVec4([color.r, color.g, color.b, color.a]),
994            },
995            transform_uniform,
996        ];
997
998        GlShader::draw(
999            texture.gl_context.ptr.svg_shader,
1000            texture,
1001            &[(&self.vertex_index_buffer, &uniforms[..])],
1002        );
1003
1004        true
1005    }
1006}
1007
1008#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
1009#[repr(C)]
1010pub struct TessellatedColoredGPUSvgNode {
1011    pub vertex_index_buffer: VertexBuffer,
1012}
1013
1014impl TessellatedColoredGPUSvgNode {
1015    /// Uploads the tesselated SVG node to GPU memory
1016    #[must_use] pub fn new(node: &TessellatedColoredSvgNode, gl: GlContextPtr) -> Self {
1017        let svg_shader_id = gl.ptr.svg_multicolor_shader;
1018        Self {
1019            vertex_index_buffer: VertexBuffer::new(
1020                gl,
1021                svg_shader_id,
1022                node.vertices.as_ref(),
1023                node.indices.as_ref(),
1024                IndexBufferFormat::Triangles,
1025            ),
1026        }
1027    }
1028
1029    /// Draw the vertex buffer to the texture with the given color and transform
1030    #[allow(clippy::needless_pass_by_value)] // owned azul value taken by value (public API / ownership-transfer convention)
1031    pub fn draw(
1032        &self,
1033        texture: &mut Texture,
1034        target_size: PhysicalSizeU32,
1035        transforms: StyleTransformVec,
1036    ) -> bool {
1037        let (bbox_uniform, transform_uniform) =
1038            compute_svg_transform_uniforms(target_size, transforms.as_ref());
1039
1040        // two separately-named GL uniforms collected into the draw-call array;
1041        // not a tuple->array conversion.
1042        #[allow(clippy::tuple_array_conversions)]
1043        let uniforms = [bbox_uniform, transform_uniform];
1044
1045        GlShader::draw(
1046            texture.gl_context.ptr.svg_multicolor_shader,
1047            texture,
1048            &[(&self.vertex_index_buffer, &uniforms[..])],
1049        );
1050
1051        true
1052    }
1053}
1054
1055#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
1056#[repr(C, u8)]
1057pub enum SvgStyle {
1058    Fill(SvgFillStyle),
1059    Stroke(SvgStrokeStyle),
1060}
1061
1062impl SvgStyle {
1063    #[must_use] pub const fn get_antialias(&self) -> bool {
1064        match self {
1065            Self::Fill(f) => f.anti_alias,
1066            Self::Stroke(s) => s.anti_alias,
1067        }
1068    }
1069    #[must_use] pub const fn get_high_quality_aa(&self) -> bool {
1070        match self {
1071            Self::Fill(f) => f.high_quality_aa,
1072            Self::Stroke(s) => s.high_quality_aa,
1073        }
1074    }
1075    #[must_use] pub const fn get_transform(&self) -> SvgTransform {
1076        match self {
1077            Self::Fill(f) => f.transform,
1078            Self::Stroke(s) => s.transform,
1079        }
1080    }
1081}
1082/// SVG fill rule for determining the interior of a shape.
1083#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
1084#[repr(C)]
1085#[derive(Default)]
1086pub enum SvgFillRule {
1087    #[default]
1088    Winding,
1089    EvenOdd,
1090}
1091
1092
1093#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
1094#[repr(C)]
1095pub struct SvgTransform {
1096    pub sx: f32,
1097    pub kx: f32,
1098    pub ky: f32,
1099    pub sy: f32,
1100    pub tx: f32,
1101    pub ty: f32,
1102}
1103
1104#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
1105#[repr(C)]
1106pub struct SvgFillStyle {
1107    /// See the SVG specification.
1108    ///
1109    /// Default value: `LineJoin::Miter`.
1110    pub line_join: SvgLineJoin,
1111    /// See the SVG specification.
1112    ///
1113    /// Must be greater than or equal to 1.0.
1114    /// Default value: `StrokeOptions::DEFAULT_MITER_LIMIT`.
1115    pub miter_limit: f32,
1116    /// Maximum allowed distance to the path when building an approximation.
1117    ///
1118    /// See [Flattening and tolerance](index.html#flattening-and-tolerance).
1119    /// Default value: `StrokeOptions::DEFAULT_TOLERANCE`.
1120    pub tolerance: f32,
1121    /// Whether to use the "winding" or "even / odd" fill rule when tesselating the path
1122    pub fill_rule: SvgFillRule,
1123    /// Whether to apply a transform to the points in the path (warning: will be done on the CPU -
1124    /// expensive)
1125    pub transform: SvgTransform,
1126    /// Whether the fill is intended to be anti-aliased (default: true)
1127    pub anti_alias: bool,
1128    /// Whether the anti-aliasing has to be of high quality (default: false)
1129    pub high_quality_aa: bool,
1130}
1131
1132impl Default for SvgFillStyle {
1133    fn default() -> Self {
1134        Self {
1135            line_join: SvgLineJoin::Miter,
1136            miter_limit: DEFAULT_MITER_LIMIT,
1137            tolerance: DEFAULT_TOLERANCE,
1138            fill_rule: SvgFillRule::default(),
1139            transform: SvgTransform::default(),
1140            anti_alias: true,
1141            high_quality_aa: false,
1142        }
1143    }
1144}
1145
1146#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
1147#[repr(C)]
1148pub struct SvgStrokeStyle {
1149    /// Dash pattern
1150    pub dash_pattern: OptionSvgDashPattern,
1151    /// Whether to apply a transform to the points in the path (warning: will be done on the CPU -
1152    /// expensive)
1153    pub transform: SvgTransform,
1154    /// What cap to use at the start of each sub-path.
1155    ///
1156    /// Default value: `LineCap::Butt`.
1157    pub start_cap: SvgLineCap,
1158    /// What cap to use at the end of each sub-path.
1159    ///
1160    /// Default value: `LineCap::Butt`.
1161    pub end_cap: SvgLineCap,
1162    /// See the SVG specification.
1163    ///
1164    /// Default value: `LineJoin::Miter`.
1165    pub line_join: SvgLineJoin,
1166    /// Line width
1167    ///
1168    /// Default value: `StrokeOptions::DEFAULT_LINE_WIDTH`.
1169    pub line_width: f32,
1170    /// See the SVG specification.
1171    ///
1172    /// Must be greater than or equal to 1.0.
1173    /// Default value: `StrokeOptions::DEFAULT_MITER_LIMIT`.
1174    pub miter_limit: f32,
1175    /// Maximum allowed distance to the path when building an approximation.
1176    ///
1177    /// See [Flattening and tolerance](index.html#flattening-and-tolerance).
1178    /// Default value: `StrokeOptions::DEFAULT_TOLERANCE`.
1179    pub tolerance: f32,
1180    /// Apply line width
1181    ///
1182    /// When set to false, the generated vertices will all be positioned in the centre
1183    /// of the line. The width can be applied later on (eg in a vertex shader) by adding
1184    /// the vertex normal multiplied by the line with to each vertex position.
1185    ///
1186    /// Default value: `true`. NOTE: currently unused!
1187    pub apply_line_width: bool,
1188    /// Whether the fill is intended to be anti-aliased (default: true)
1189    pub anti_alias: bool,
1190    /// Whether the anti-aliasing has to be of high quality (default: false)
1191    pub high_quality_aa: bool,
1192}
1193
1194impl Default for SvgStrokeStyle {
1195    fn default() -> Self {
1196        Self {
1197            dash_pattern: OptionSvgDashPattern::None,
1198            transform: SvgTransform::default(),
1199            start_cap: SvgLineCap::default(),
1200            end_cap: SvgLineCap::default(),
1201            line_join: SvgLineJoin::default(),
1202            line_width: DEFAULT_LINE_WIDTH,
1203            miter_limit: DEFAULT_MITER_LIMIT,
1204            tolerance: DEFAULT_TOLERANCE,
1205            apply_line_width: true,
1206            anti_alias: true,
1207            high_quality_aa: false,
1208        }
1209    }
1210}
1211
1212#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
1213#[repr(C)]
1214pub struct SvgDashPattern {
1215    pub offset: f32,
1216    pub length_1: f32,
1217    pub gap_1: f32,
1218    pub length_2: f32,
1219    pub gap_2: f32,
1220    pub length_3: f32,
1221    pub gap_3: f32,
1222}
1223
1224impl_option!(
1225    SvgDashPattern,
1226    OptionSvgDashPattern,
1227    [Debug, Copy, Clone, PartialEq, PartialOrd]
1228);
1229
1230/// The shape used at the end of open sub-paths when they are stroked.
1231#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
1232#[repr(C)]
1233#[derive(Default)]
1234pub enum SvgLineCap {
1235    #[default]
1236    Butt,
1237    Square,
1238    Round,
1239}
1240
1241
1242/// The shape used at the corners of stroked paths.
1243#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
1244#[repr(C)]
1245#[derive(Default)]
1246pub enum SvgLineJoin {
1247    #[default]
1248    Miter,
1249    MiterClip,
1250    Round,
1251    Bevel,
1252}
1253
1254
1255pub use core::ffi::c_void;
1256
1257#[derive(Debug, Clone)]
1258#[repr(C)]
1259pub struct SvgXmlNode {
1260    pub node: *const c_void, // usvg::Node
1261    pub run_destructor: bool,
1262}
1263
1264#[derive(Debug, Clone)]
1265#[repr(C)]
1266pub struct Svg {
1267    pub tree: *const c_void, // *mut usvg::Tree,
1268    pub run_destructor: bool,
1269}
1270
1271/// SVG `shape-rendering` property controlling quality vs speed tradeoffs.
1272#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1273#[repr(C)]
1274pub enum ShapeRendering {
1275    OptimizeSpeed,
1276    CrispEdges,
1277    GeometricPrecision,
1278}
1279
1280/// SVG `image-rendering` property controlling image quality vs speed.
1281#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1282#[repr(C)]
1283pub enum ImageRendering {
1284    OptimizeQuality,
1285    OptimizeSpeed,
1286}
1287
1288/// SVG `text-rendering` property controlling text quality vs speed.
1289#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1290#[repr(C)]
1291pub enum TextRendering {
1292    OptimizeSpeed,
1293    OptimizeLegibility,
1294    GeometricPrecision,
1295}
1296
1297/// Font database source for SVG text rendering.
1298#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1299#[repr(C)]
1300pub enum FontDatabase {
1301    Empty,
1302    System,
1303}
1304
1305#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
1306#[repr(C)]
1307pub struct SvgRenderOptions {
1308    pub target_size: OptionLayoutSize,
1309    pub background_color: OptionColorU,
1310    pub fit: SvgFitTo,
1311    pub transform: SvgRenderTransform,
1312}
1313
1314#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
1315#[repr(C)]
1316pub struct SvgRenderTransform {
1317    pub sx: f32,
1318    pub kx: f32,
1319    pub ky: f32,
1320    pub sy: f32,
1321    pub tx: f32,
1322    pub ty: f32,
1323}
1324
1325#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
1326#[repr(C, u8)]
1327#[derive(Default)]
1328pub enum SvgFitTo {
1329    #[default]
1330    Original,
1331    Width(u32),
1332    Height(u32),
1333    Zoom(f32),
1334}
1335
1336
1337#[derive(Debug, Clone, PartialEq, PartialOrd)]
1338#[repr(C)]
1339pub struct SvgParseOptions {
1340    /// SVG image path. Used to resolve relative image paths.
1341    pub relative_image_path: OptionString,
1342    /// Default font family. Will be used when no font-family attribute is set in the SVG. Default:
1343    /// Times New Roman
1344    pub default_font_family: AzString,
1345    /// A list of languages. Will be used to resolve a systemLanguage conditional attribute.
1346    /// Format: en, en-US. Default: [en]
1347    pub languages: StringVec,
1348    /// Target DPI. Impact units conversion. Default: 96.0
1349    pub dpi: f32,
1350    /// A default font size. Will be used when no font-size attribute is set in the SVG. Default:
1351    /// 12
1352    pub font_size: f32,
1353    /// Specifies the default shape rendering method. Will be used when an SVG element's
1354    /// shape-rendering property is set to auto. Default: `GeometricPrecision`
1355    pub shape_rendering: ShapeRendering,
1356    /// Specifies the default text rendering method. Will be used when an SVG element's
1357    /// text-rendering property is set to auto. Default: `OptimizeLegibility`
1358    pub text_rendering: TextRendering,
1359    /// Specifies the default image rendering method. Will be used when an SVG element's
1360    /// image-rendering property is set to auto. Default: `OptimizeQuality`
1361    pub image_rendering: ImageRendering,
1362    /// When empty, text elements will be skipped. Default: `System`
1363    pub fontdb: FontDatabase,
1364    /// Keep named groups. If set to true, all non-empty groups with id attribute will not be
1365    /// removed. Default: false
1366    pub keep_named_groups: bool,
1367}
1368
1369impl Default for SvgParseOptions {
1370    fn default() -> Self {
1371        let lang_vec: Vec<AzString> = vec![String::from("en").into()];
1372        Self {
1373            relative_image_path: OptionString::None,
1374            default_font_family: "Times New Roman".to_string().into(),
1375            languages: lang_vec.into(),
1376            dpi: 96.0,
1377            font_size: 12.0,
1378            shape_rendering: ShapeRendering::GeometricPrecision,
1379            text_rendering: TextRendering::OptimizeLegibility,
1380            image_rendering: ImageRendering::OptimizeQuality,
1381            fontdb: FontDatabase::System,
1382            keep_named_groups: false,
1383        }
1384    }
1385}
1386
1387#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
1388#[repr(C)]
1389pub struct SvgXmlOptions {
1390    pub use_single_quote: bool,
1391    pub indent: Indent,
1392    pub attributes_indent: Indent,
1393}
1394
1395impl Default for SvgXmlOptions {
1396    fn default() -> Self {
1397        Self {
1398            use_single_quote: false,
1399            indent: Indent::Spaces(2),
1400            attributes_indent: Indent::Spaces(2),
1401        }
1402    }
1403}
1404#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
1405#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1406#[repr(C, u8)]
1407pub enum SvgParseError {
1408    NoParserAvailable,
1409    ElementsLimitReached,
1410    NotAnUtf8Str,
1411    MalformedGZip,
1412    InvalidSize,
1413    ParsingFailed(XmlError),
1414}
1415
1416impl fmt::Display for SvgParseError {
1417    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1418        use self::SvgParseError::{NoParserAvailable, ElementsLimitReached, NotAnUtf8Str, MalformedGZip, InvalidSize, ParsingFailed};
1419        match self {
1420            NoParserAvailable => write!(
1421                f,
1422                "Library was compiled without SVG support (no parser available)"
1423            ),
1424            ElementsLimitReached => write!(f, "Error parsing SVG: Elements limit reached"),
1425            NotAnUtf8Str => write!(f, "Error parsing SVG: Not an UTF-8 String"),
1426            MalformedGZip => write!(
1427                f,
1428                "Error parsing SVG: SVG is compressed with a malformed GZIP compression"
1429            ),
1430            InvalidSize => write!(f, "Error parsing SVG: Invalid size"),
1431            ParsingFailed(e) => write!(f, "Error parsing SVG: Parsing SVG as XML failed: {e}"),
1432        }
1433    }
1434}
1435
1436impl_result!(
1437    SvgXmlNode,
1438    SvgParseError,
1439    ResultSvgXmlNodeSvgParseError,
1440    copy = false,
1441    [Debug, Clone]
1442);
1443impl_result!(
1444    Svg,
1445    SvgParseError,
1446    ResultSvgSvgParseError,
1447    copy = false,
1448    [Debug, Clone]
1449);
1450
1451/// Indentation style for SVG XML serialization.
1452#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
1453#[repr(C, u8)]
1454pub enum Indent {
1455    None,
1456    Spaces(u8),
1457    Tabs,
1458}
1459
1460#[cfg(test)]
1461#[allow(clippy::pedantic, clippy::nursery, clippy::float_cmp)]
1462mod autotest_generated {
1463    use super::*;
1464
1465    // ---------------------------------------------------------------- helpers
1466
1467    fn pt(x: f32, y: f32) -> SvgPoint {
1468        SvgPoint { x, y }
1469    }
1470
1471    fn line_el(x1: f32, y1: f32, x2: f32, y2: f32) -> SvgPathElement {
1472        SvgPathElement::line(SvgLine::new(pt(x1, y1), pt(x2, y2)))
1473    }
1474
1475    fn make_path(items: Vec<SvgPathElement>) -> SvgPath {
1476        SvgPath::create(SvgPathElementVec::from_vec(items))
1477    }
1478
1479    fn quad() -> SvgQuadraticCurve {
1480        SvgQuadraticCurve {
1481            start: pt(0.0, 0.0),
1482            ctrl: pt(5.0, 10.0),
1483            end: pt(10.0, 0.0),
1484        }
1485    }
1486
1487    fn cubic() -> SvgCubicCurve {
1488        SvgCubicCurve {
1489            start: pt(0.0, 0.0),
1490            ctrl_1: pt(0.0, 10.0),
1491            ctrl_2: pt(10.0, 10.0),
1492            end: pt(10.0, 0.0),
1493        }
1494    }
1495
1496    /// `true` if `outer` fully contains `inner` (used to check bounding-box invariants).
1497    fn rect_contains(outer: &SvgRect, inner: &SvgRect) -> bool {
1498        outer.x <= inner.x
1499            && outer.y <= inner.y
1500            && outer.x + outer.width >= inner.x + inner.width
1501            && outer.y + outer.height >= inner.y + inner.height
1502    }
1503
1504    // ------------------------------------------------------- SvgLine :: basic
1505
1506    #[test]
1507    fn svgline_new_keeps_fields_including_extreme_values() {
1508        let l = SvgLine::new(pt(1.0, 2.0), pt(3.0, 4.0));
1509        assert_eq!(l.get_start(), pt(1.0, 2.0));
1510        assert_eq!(l.get_end(), pt(3.0, 4.0));
1511
1512        let extreme = SvgLine::new(pt(f32::MIN, f32::MAX), pt(f32::MAX, f32::MIN));
1513        assert_eq!(extreme.start.x, f32::MIN);
1514        assert_eq!(extreme.end.x, f32::MAX);
1515
1516        // NaN survives construction untouched (no normalization happens)
1517        let nan = SvgLine::new(pt(f32::NAN, 0.0), pt(0.0, f32::NAN));
1518        assert!(nan.get_start().x.is_nan());
1519        assert!(nan.get_end().y.is_nan());
1520    }
1521
1522    #[test]
1523    fn svgline_reverse_is_an_involution() {
1524        let orig = SvgLine::new(pt(-1.5, 2.5), pt(7.0, -9.0));
1525        let mut l = orig;
1526        l.reverse();
1527        assert_eq!(l.get_start(), orig.get_end());
1528        assert_eq!(l.get_end(), orig.get_start());
1529        l.reverse();
1530        assert_eq!(l, orig);
1531    }
1532
1533    #[test]
1534    fn svgline_reverse_does_not_panic_on_extreme_values() {
1535        let mut l = SvgLine::new(pt(f32::INFINITY, f32::NAN), pt(f32::NEG_INFINITY, f32::MAX));
1536        l.reverse();
1537        assert!(l.get_start().x.is_infinite() && l.get_start().x < 0.0);
1538        assert!(l.get_end().y.is_nan());
1539    }
1540
1541    // ----------------------------------------------------- SvgLine :: normals
1542
1543    #[test]
1544    fn svgline_inwards_normal_is_unit_length_and_90deg_right() {
1545        // horizontal line pointing +x -> normal points -y (90deg to the right in SVG coords)
1546        let l = SvgLine::new(pt(0.0, 0.0), pt(10.0, 0.0));
1547        let n = l.inwards_normal().expect("non-degenerate line has a normal");
1548        assert!((n.x - 0.0).abs() < 1e-6);
1549        assert!((n.y - 1.0).abs() < 1e-6);
1550        let len = (n.x * n.x + n.y * n.y).sqrt();
1551        assert!((len - 1.0).abs() < 1e-6, "normal must be unit length");
1552    }
1553
1554    #[test]
1555    fn svgline_outwards_normal_is_the_negated_inwards_normal() {
1556        let l = SvgLine::new(pt(3.0, -4.0), pt(-7.0, 11.0));
1557        let i = l.inwards_normal().expect("non-degenerate line has a normal");
1558        let o = l.outwards_normal().expect("non-degenerate line has a normal");
1559        assert!((i.x + o.x).abs() < 1e-6);
1560        assert!((i.y + o.y).abs() < 1e-6);
1561    }
1562
1563    #[test]
1564    fn svgline_normals_are_none_for_zero_length_line() {
1565        // division by a zero edge length must not produce a bogus point
1566        let l = SvgLine::new(pt(5.0, 5.0), pt(5.0, 5.0));
1567        assert_eq!(l.inwards_normal(), None);
1568        assert_eq!(l.outwards_normal(), None);
1569    }
1570
1571    #[test]
1572    fn svgline_normals_are_none_for_nan_and_infinite_coords() {
1573        let nan = SvgLine::new(pt(f32::NAN, 0.0), pt(1.0, 1.0));
1574        assert_eq!(nan.inwards_normal(), None);
1575        assert_eq!(nan.outwards_normal(), None);
1576
1577        // dy/hypot == inf/inf == NaN -> not finite -> None
1578        let inf = SvgLine::new(pt(f32::NEG_INFINITY, f32::NEG_INFINITY), pt(1.0, 1.0));
1579        assert_eq!(inf.inwards_normal(), None);
1580        assert_eq!(inf.outwards_normal(), None);
1581    }
1582
1583    #[test]
1584    fn svgline_inwards_normal_on_overflowing_line_stays_defined() {
1585        // dx/dy overflow f32 -> hypot is +inf; the result must be either None
1586        // or finite, never a silent NaN/inf leaking into the point.
1587        let l = SvgLine::new(pt(-f32::MAX, -f32::MAX), pt(f32::MAX, f32::MAX));
1588        match l.inwards_normal() {
1589            None => {}
1590            Some(n) => assert!(
1591                n.x.is_finite() && n.y.is_finite(),
1592                "inwards_normal returned a non-finite point: {n:?}"
1593            ),
1594        }
1595    }
1596
1597    // ---------------------------------------------------- SvgLine :: numerics
1598
1599    #[test]
1600    fn svgline_get_x_y_at_t_hit_the_endpoints_exactly() {
1601        let l = SvgLine::new(pt(2.0, -3.0), pt(12.0, 17.0));
1602        assert_eq!(l.get_x_at_t(0.0), 2.0);
1603        assert_eq!(l.get_y_at_t(0.0), -3.0);
1604        assert_eq!(l.get_x_at_t(1.0), 12.0);
1605        assert_eq!(l.get_y_at_t(1.0), 17.0);
1606        assert!((l.get_x_at_t(0.5) - 7.0).abs() < 1e-9);
1607        assert!((l.get_y_at_t(0.5) - 7.0).abs() < 1e-9);
1608    }
1609
1610    #[test]
1611    fn svgline_get_x_at_t_extrapolates_for_out_of_range_t() {
1612        // t is NOT clamped to [0, 1] - document the extrapolating behaviour
1613        let l = SvgLine::new(pt(0.0, 0.0), pt(10.0, 10.0));
1614        assert!((l.get_x_at_t(-1.0) - -10.0).abs() < 1e-9);
1615        assert!((l.get_y_at_t(2.0) - 20.0).abs() < 1e-9);
1616    }
1617
1618    #[test]
1619    fn svgline_get_x_y_at_t_nan_and_inf_do_not_panic() {
1620        let l = SvgLine::new(pt(0.0, 0.0), pt(10.0, 10.0));
1621        assert!(l.get_x_at_t(f64::NAN).is_nan());
1622        assert!(l.get_y_at_t(f64::NAN).is_nan());
1623        assert!(l.get_x_at_t(f64::INFINITY).is_infinite());
1624        assert!(l.get_y_at_t(f64::NEG_INFINITY).is_infinite());
1625
1626        // 0-length in x: (end.x - start.x) == 0, so 0 * inf == NaN
1627        let vertical = SvgLine::new(pt(4.0, 0.0), pt(4.0, 10.0));
1628        assert!(vertical.get_x_at_t(f64::INFINITY).is_nan());
1629    }
1630
1631    #[test]
1632    fn svgline_get_x_at_t_at_f32_extremes_saturates_to_inf_not_panic() {
1633        let l = SvgLine::new(pt(-f32::MAX, 0.0), pt(f32::MAX, 0.0));
1634        // f64 has the range to hold 2 * f32::MAX, so this must stay finite
1635        assert!(l.get_x_at_t(0.5).abs() < 1e-9);
1636        assert!(l.get_x_at_t(1.0).is_finite());
1637        assert!(l.get_x_at_t(f64::MAX).is_infinite());
1638    }
1639
1640    #[test]
1641    fn svgline_get_length_is_euclidean_and_direction_independent() {
1642        let l = SvgLine::new(pt(0.0, 0.0), pt(3.0, 4.0));
1643        assert!((l.get_length() - 5.0).abs() < 1e-6);
1644        let mut r = l;
1645        r.reverse();
1646        assert!((r.get_length() - l.get_length()).abs() < 1e-9);
1647        assert_eq!(SvgLine::new(pt(1.0, 1.0), pt(1.0, 1.0)).get_length(), 0.0);
1648    }
1649
1650    #[test]
1651    fn svgline_get_length_overflow_and_nan_are_defined() {
1652        // dx overflows f32 -> +inf, hypot(+inf, 0) == +inf
1653        let huge = SvgLine::new(pt(-f32::MAX, 0.0), pt(f32::MAX, 0.0));
1654        let len = huge.get_length();
1655        assert!(len.is_infinite() && len > 0.0);
1656
1657        let nan = SvgLine::new(pt(f32::NAN, 0.0), pt(0.0, 0.0));
1658        assert!(nan.get_length().is_nan());
1659    }
1660
1661    #[test]
1662    fn svgline_get_t_at_offset_maps_arc_length_to_t() {
1663        let l = SvgLine::new(pt(0.0, 0.0), pt(10.0, 0.0));
1664        assert_eq!(l.get_t_at_offset(0.0), 0.0);
1665        assert!((l.get_t_at_offset(5.0) - 0.5).abs() < 1e-6);
1666        assert!((l.get_t_at_offset(10.0) - 1.0).abs() < 1e-6);
1667        // negative + past-the-end offsets are NOT clamped
1668        assert!((l.get_t_at_offset(-5.0) + 0.5).abs() < 1e-6);
1669        assert!((l.get_t_at_offset(20.0) - 2.0).abs() < 1e-6);
1670    }
1671
1672    #[test]
1673    fn svgline_get_t_at_offset_on_zero_length_line_is_nan_or_inf_not_a_panic() {
1674        // offset / 0.0 -- must not panic; 0/0 is NaN, x/0 is +-inf
1675        let l = SvgLine::new(pt(1.0, 1.0), pt(1.0, 1.0));
1676        assert!(l.get_t_at_offset(0.0).is_nan());
1677        assert!(l.get_t_at_offset(5.0).is_infinite());
1678        assert!(l.get_t_at_offset(-5.0).is_infinite());
1679        assert!(l.get_t_at_offset(f64::NAN).is_nan());
1680    }
1681
1682    #[test]
1683    fn svgline_get_t_at_offset_round_trips_through_get_x_at_t() {
1684        let l = SvgLine::new(pt(2.0, 2.0), pt(2.0, 12.0));
1685        let t = l.get_t_at_offset(l.get_length());
1686        assert!((l.get_y_at_t(t) - 12.0).abs() < 1e-6);
1687        assert!((l.get_x_at_t(t) - 2.0).abs() < 1e-6);
1688    }
1689
1690    #[test]
1691    fn svgline_tangent_vector_is_normalized_and_zero_for_degenerate_lines() {
1692        let l = SvgLine::new(pt(0.0, 0.0), pt(0.0, 5.0));
1693        let t = l.get_tangent_vector_at_t();
1694        assert!((t.x - 0.0).abs() < 1e-9);
1695        assert!((t.y - 1.0).abs() < 1e-9);
1696
1697        // normalize() defines the zero-length case as the zero vector
1698        let degenerate = SvgLine::new(pt(3.0, 3.0), pt(3.0, 3.0));
1699        let t = degenerate.get_tangent_vector_at_t();
1700        assert_eq!(t, SvgVector { x: 0.0, y: 0.0 });
1701    }
1702
1703    // ------------------------------------------------------ SvgLine :: bounds
1704
1705    #[test]
1706    fn svgline_get_bounds_is_orientation_independent() {
1707        let l = SvgLine::new(pt(10.0, 20.0), pt(-5.0, 4.0));
1708        let mut r = l;
1709        r.reverse();
1710        assert_eq!(l.get_bounds(), r.get_bounds());
1711
1712        let b = l.get_bounds();
1713        assert_eq!(b.x, -5.0);
1714        assert_eq!(b.y, 4.0);
1715        assert_eq!(b.width, 15.0);
1716        assert_eq!(b.height, 16.0);
1717        assert_eq!(b.radius_top_left, 0.0);
1718    }
1719
1720    #[test]
1721    fn svgline_get_bounds_of_degenerate_line_is_zero_sized() {
1722        let b = SvgLine::new(pt(7.0, 8.0), pt(7.0, 8.0)).get_bounds();
1723        assert_eq!((b.x, b.y, b.width, b.height), (7.0, 8.0, 0.0, 0.0));
1724    }
1725
1726    #[test]
1727    fn svgline_get_bounds_overflows_to_inf_width_without_panicking() {
1728        // max_x - min_x overflows f32 -> +inf rather than a wrapped/negative width
1729        let b = SvgLine::new(pt(-f32::MAX, 0.0), pt(f32::MAX, 1.0)).get_bounds();
1730        assert!(b.width.is_infinite() && b.width > 0.0);
1731        assert_eq!(b.x, -f32::MAX);
1732        assert_eq!(b.height, 1.0);
1733    }
1734
1735    // ------------------------------------------------- SvgPathElement :: ctors
1736
1737    #[test]
1738    fn svgpathelement_constructors_wrap_the_right_variant() {
1739        let l = SvgLine::new(pt(0.0, 0.0), pt(1.0, 1.0));
1740        assert!(matches!(SvgPathElement::line(l), SvgPathElement::Line(_)));
1741        assert!(matches!(
1742            SvgPathElement::quadratic_curve(quad()),
1743            SvgPathElement::QuadraticCurve(_)
1744        ));
1745        assert!(matches!(
1746            SvgPathElement::cubic_curve(cubic()),
1747            SvgPathElement::CubicCurve(_)
1748        ));
1749    }
1750
1751    #[test]
1752    fn svgpathelement_constructors_accept_extreme_geometry() {
1753        let l = SvgLine::new(pt(f32::NAN, f32::INFINITY), pt(f32::MIN, f32::MAX));
1754        let el = SvgPathElement::line(l);
1755        assert!(el.get_start().x.is_nan());
1756        assert_eq!(el.get_end().x, f32::MIN);
1757    }
1758
1759    // ------------------------------------------- SvgPathElement :: set / get
1760
1761    #[test]
1762    fn svgpathelement_set_first_last_round_trip_for_every_variant() {
1763        let a = pt(-1.0, -2.0);
1764        let b = pt(3.0, 4.0);
1765
1766        for mut el in [
1767            SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(1.0, 1.0))),
1768            SvgPathElement::quadratic_curve(quad()),
1769            SvgPathElement::cubic_curve(cubic()),
1770        ] {
1771            el.set_first(a);
1772            el.set_last(b);
1773            assert_eq!(el.get_start(), a);
1774            assert_eq!(el.get_end(), b);
1775        }
1776    }
1777
1778    #[test]
1779    fn svgpathelement_set_first_last_accept_zero_min_max_and_nan() {
1780        let mut el = SvgPathElement::cubic_curve(cubic());
1781
1782        el.set_first(pt(0.0, 0.0));
1783        el.set_last(pt(0.0, 0.0));
1784        assert_eq!(el.get_start(), pt(0.0, 0.0));
1785        assert_eq!(el.get_end(), pt(0.0, 0.0));
1786
1787        el.set_first(pt(f32::MIN, f32::MIN));
1788        el.set_last(pt(f32::MAX, f32::MAX));
1789        assert_eq!(el.get_start().x, f32::MIN);
1790        assert_eq!(el.get_end().x, f32::MAX);
1791
1792        el.set_first(pt(f32::NAN, f32::NEG_INFINITY));
1793        assert!(el.get_start().x.is_nan());
1794        assert!(el.get_start().y.is_infinite());
1795    }
1796
1797    #[test]
1798    fn svgpathelement_reverse_is_an_involution_for_every_variant() {
1799        for el in [
1800            SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(1.0, 1.0))),
1801            SvgPathElement::quadratic_curve(quad()),
1802            SvgPathElement::cubic_curve(cubic()),
1803        ] {
1804            let mut r = el;
1805            r.reverse();
1806            assert_eq!(r.get_start(), el.get_end());
1807            assert_eq!(r.get_end(), el.get_start());
1808            r.reverse();
1809            assert_eq!(r, el, "reverse() applied twice must be the identity");
1810        }
1811    }
1812
1813    // --------------------------------------------- SvgPathElement :: numerics
1814
1815    #[test]
1816    fn svgpathelement_get_length_is_non_negative_for_every_variant() {
1817        for el in [
1818            SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(3.0, 4.0))),
1819            SvgPathElement::quadratic_curve(quad()),
1820            SvgPathElement::cubic_curve(cubic()),
1821        ] {
1822            let len = el.get_length();
1823            assert!(len.is_finite() && len >= 0.0, "bad length: {len}");
1824        }
1825    }
1826
1827    #[test]
1828    fn svgpathelement_get_x_y_at_t_hit_endpoints_for_every_variant() {
1829        for el in [
1830            SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(10.0, 0.0))),
1831            SvgPathElement::quadratic_curve(quad()),
1832            SvgPathElement::cubic_curve(cubic()),
1833        ] {
1834            assert!((el.get_x_at_t(0.0) - f64::from(el.get_start().x)).abs() < 1e-6);
1835            assert!((el.get_y_at_t(0.0) - f64::from(el.get_start().y)).abs() < 1e-6);
1836            assert!((el.get_x_at_t(1.0) - f64::from(el.get_end().x)).abs() < 1e-6);
1837            assert!((el.get_y_at_t(1.0) - f64::from(el.get_end().y)).abs() < 1e-6);
1838        }
1839    }
1840
1841    #[test]
1842    fn svgpathelement_get_x_y_at_t_nan_inf_do_not_panic() {
1843        for el in [
1844            SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(10.0, 0.0))),
1845            SvgPathElement::quadratic_curve(quad()),
1846            SvgPathElement::cubic_curve(cubic()),
1847        ] {
1848            assert!(el.get_x_at_t(f64::NAN).is_nan());
1849            assert!(el.get_y_at_t(f64::NAN).is_nan());
1850            // +-inf must not panic; any non-panicking value is acceptable here
1851            let _ = el.get_x_at_t(f64::INFINITY);
1852            let _ = el.get_y_at_t(f64::NEG_INFINITY);
1853            let _ = el.get_x_at_t(f64::MIN);
1854            let _ = el.get_y_at_t(f64::MAX);
1855        }
1856    }
1857
1858    #[test]
1859    fn svgpathelement_line_tangent_ignores_t_even_when_t_is_nan() {
1860        // SvgLine has a constant tangent, so the `t` argument is discarded.
1861        let el = SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(0.0, 8.0)));
1862        let at_half = el.get_tangent_vector_at_t(0.5);
1863        assert_eq!(el.get_tangent_vector_at_t(f64::NAN), at_half);
1864        assert_eq!(el.get_tangent_vector_at_t(f64::INFINITY), at_half);
1865        assert_eq!(at_half, SvgVector { x: 0.0, y: 1.0 });
1866    }
1867
1868    #[test]
1869    fn svgpathelement_curve_tangent_at_nan_is_nan_not_a_panic() {
1870        let el = SvgPathElement::quadratic_curve(quad());
1871        let v = el.get_tangent_vector_at_t(f64::NAN);
1872        assert!(v.x.is_nan() && v.y.is_nan());
1873    }
1874
1875    #[test]
1876    fn svgpathelement_curve_tangent_is_unit_length_in_range() {
1877        for el in [
1878            SvgPathElement::quadratic_curve(quad()),
1879            SvgPathElement::cubic_curve(cubic()),
1880        ] {
1881            for t in [0.0_f64, 0.25, 0.5, 0.75, 1.0] {
1882                let v = el.get_tangent_vector_at_t(t);
1883                let len = (v.x * v.x + v.y * v.y).sqrt();
1884                // either the zero vector (degenerate derivative) or unit length
1885                assert!(
1886                    len.abs() < 1e-9 || (len - 1.0).abs() < 1e-6,
1887                    "tangent at t={t} has length {len}"
1888                );
1889            }
1890        }
1891    }
1892
1893    #[test]
1894    fn svgpathelement_curve_t_at_offset_saturates_at_1_past_the_end() {
1895        // the sampling loop never triggers for an out-of-range offset,
1896        // so it must fall through to the final t (== 1.0), not overshoot
1897        for el in [
1898            SvgPathElement::quadratic_curve(quad()),
1899            SvgPathElement::cubic_curve(cubic()),
1900        ] {
1901            assert!((el.get_t_at_offset(f64::MAX) - 1.0).abs() < 1e-9);
1902            assert!((el.get_t_at_offset(1.0e30) - 1.0).abs() < 1e-9);
1903            // NaN never compares greater, so it also falls through
1904            assert!((el.get_t_at_offset(f64::NAN) - 1.0).abs() < 1e-9);
1905        }
1906    }
1907
1908    #[test]
1909    fn svgpathelement_curve_t_at_offset_is_monotonic_and_in_range() {
1910        let el = SvgPathElement::cubic_curve(cubic());
1911        let len = el.get_length();
1912        let t_quarter = el.get_t_at_offset(len * 0.25);
1913        let t_half = el.get_t_at_offset(len * 0.5);
1914        assert!(t_quarter.is_finite() && t_half.is_finite());
1915        assert!((0.0..=1.0).contains(&t_quarter), "t={t_quarter}");
1916        assert!((0.0..=1.0).contains(&t_half), "t={t_half}");
1917        assert!(t_quarter <= t_half, "t must not decrease with offset");
1918    }
1919
1920    #[test]
1921    fn svgpathelement_curve_t_at_offset_negative_offset_is_non_positive() {
1922        let el = SvgPathElement::cubic_curve(cubic());
1923        let t = el.get_t_at_offset(-10.0);
1924        assert!(t.is_finite(), "negative offset produced {t}");
1925        assert!(t <= 0.0, "negative offset must not map to a forward t: {t}");
1926    }
1927
1928    #[test]
1929    fn svgpathelement_get_bounds_contains_both_endpoints() {
1930        for el in [
1931            SvgPathElement::line(SvgLine::new(pt(-3.0, 2.0), pt(9.0, -4.0))),
1932            SvgPathElement::quadratic_curve(quad()),
1933            SvgPathElement::cubic_curve(cubic()),
1934        ] {
1935            let b = el.get_bounds();
1936            let (s, e) = (el.get_start(), el.get_end());
1937            assert!(b.width >= 0.0 && b.height >= 0.0);
1938            assert!(s.x >= b.x && s.x <= b.x + b.width);
1939            assert!(e.x >= b.x && e.x <= b.x + b.width);
1940            assert!(s.y >= b.y && s.y <= b.y + b.height);
1941            assert!(e.y >= b.y && e.y <= b.y + b.height);
1942        }
1943    }
1944
1945    // -------------------------------------------------------------- SvgPath
1946
1947    #[test]
1948    fn svgpath_empty_getters_are_none_and_bounds_are_default() {
1949        let p = make_path(Vec::new());
1950        assert_eq!(p.get_start(), None);
1951        assert_eq!(p.get_end(), None);
1952        assert!(!p.is_closed(), "an empty path is not closed");
1953        assert_eq!(p.get_bounds(), SvgRect::default());
1954    }
1955
1956    #[test]
1957    fn svgpath_start_and_end_come_from_first_and_last_element() {
1958        let p = make_path(vec![
1959            line_el(0.0, 0.0, 1.0, 1.0),
1960            line_el(1.0, 1.0, 5.0, 5.0),
1961            line_el(5.0, 5.0, 9.0, 2.0),
1962        ]);
1963        assert_eq!(p.get_start(), Some(pt(0.0, 0.0)));
1964        assert_eq!(p.get_end(), Some(pt(9.0, 2.0)));
1965    }
1966
1967    #[test]
1968    fn svgpath_close_makes_is_closed_true_and_is_idempotent() {
1969        let mut p = make_path(vec![
1970            line_el(0.0, 0.0, 10.0, 0.0),
1971            line_el(10.0, 0.0, 10.0, 10.0),
1972        ]);
1973        assert!(!p.is_closed());
1974        p.close();
1975        assert!(p.is_closed(), "close() must establish is_closed()");
1976        assert_eq!(p.items.len(), 3);
1977        assert_eq!(p.get_end(), p.get_start());
1978
1979        // closing an already-closed path must not append anything
1980        p.close();
1981        assert_eq!(p.items.len(), 3);
1982    }
1983
1984    #[test]
1985    fn svgpath_close_on_empty_path_is_a_noop() {
1986        let mut p = make_path(Vec::new());
1987        p.close();
1988        assert_eq!(p.items.len(), 0);
1989        assert!(!p.is_closed());
1990    }
1991
1992    #[test]
1993    fn svgpath_close_with_nan_coords_appends_once_and_does_not_panic() {
1994        // NaN != NaN, so the "already closed?" check can never be satisfied.
1995        // close() must still terminate and append exactly one element.
1996        let mut p = make_path(vec![line_el(f32::NAN, 0.0, 10.0, 0.0)]);
1997        p.close();
1998        assert_eq!(p.items.len(), 2);
1999        assert!(!p.is_closed(), "a NaN start point can never compare equal");
2000    }
2001
2002    #[test]
2003    fn svgpath_is_closed_for_single_degenerate_element() {
2004        let p = make_path(vec![line_el(4.0, 4.0, 4.0, 4.0)]);
2005        assert!(p.is_closed(), "start == end for the only element");
2006
2007        let open = make_path(vec![line_el(4.0, 4.0, 5.0, 4.0)]);
2008        assert!(!open.is_closed());
2009    }
2010
2011    #[test]
2012    fn svgpath_reverse_is_an_involution_and_swaps_endpoints() {
2013        let orig = make_path(vec![
2014            line_el(0.0, 0.0, 1.0, 1.0),
2015            SvgPathElement::cubic_curve(cubic()),
2016            line_el(10.0, 0.0, 20.0, 5.0),
2017        ]);
2018
2019        let mut p = orig.clone();
2020        p.reverse();
2021        assert_eq!(p.get_start(), orig.get_end());
2022        assert_eq!(p.get_end(), orig.get_start());
2023        assert_eq!(p.items.len(), orig.items.len());
2024
2025        p.reverse();
2026        assert_eq!(p, orig, "reverse() applied twice must be the identity");
2027    }
2028
2029    #[test]
2030    fn svgpath_reverse_on_empty_path_does_not_panic() {
2031        let mut p = make_path(Vec::new());
2032        p.reverse();
2033        assert_eq!(p.items.len(), 0);
2034    }
2035
2036    #[test]
2037    fn svgpath_join_with_interpolates_the_join_point() {
2038        let mut a = make_path(vec![line_el(0.0, 0.0, 10.0, 0.0)]);
2039        let b = make_path(vec![line_el(20.0, 10.0, 30.0, 10.0)]);
2040
2041        assert_eq!(a.join_with(b), Some(()));
2042        assert_eq!(a.items.len(), 2);
2043
2044        // join point is the midpoint of (10,0) and (20,10)
2045        let mid = pt(15.0, 5.0);
2046        assert_eq!(a.items.as_ref()[0].get_end(), mid);
2047        assert_eq!(a.items.as_ref()[1].get_start(), mid);
2048        assert_eq!(a.get_start(), Some(pt(0.0, 0.0)));
2049        assert_eq!(a.get_end(), Some(pt(30.0, 10.0)));
2050    }
2051
2052    #[test]
2053    fn svgpath_join_with_empty_other_returns_none_and_leaves_self_intact() {
2054        let mut a = make_path(vec![line_el(0.0, 0.0, 10.0, 0.0)]);
2055        let before = a.clone();
2056        assert_eq!(a.join_with(make_path(Vec::new())), None);
2057        assert_eq!(a, before, "a failed join must not corrupt the receiver");
2058    }
2059
2060    #[test]
2061    fn svgpath_join_with_on_empty_self_returns_none_without_underflow() {
2062        // `vec.len() - 1` would underflow on an empty receiver; the `?` on
2063        // `last()` must short-circuit first.
2064        let mut a = make_path(Vec::new());
2065        let b = make_path(vec![line_el(0.0, 0.0, 1.0, 1.0)]);
2066        assert_eq!(a.join_with(b), None);
2067        assert_eq!(a.items.len(), 0);
2068    }
2069
2070    #[test]
2071    fn svgpath_join_with_extreme_coords_does_not_panic() {
2072        let mut a = make_path(vec![line_el(0.0, 0.0, f32::MAX, f32::MAX)]);
2073        let b = make_path(vec![line_el(-f32::MAX, -f32::MAX, 0.0, 0.0)]);
2074        assert_eq!(a.join_with(b), Some(()));
2075        // midpoint of MAX and -MAX must not overflow to inf
2076        let join = a.items.as_ref()[0].get_end();
2077        assert!(join.x.is_finite() && join.y.is_finite(), "join: {join:?}");
2078    }
2079
2080    #[test]
2081    fn svgpath_get_bounds_unions_every_element() {
2082        let p = make_path(vec![
2083            line_el(0.0, 0.0, 10.0, 0.0),
2084            line_el(10.0, 0.0, 10.0, 20.0),
2085            line_el(10.0, 20.0, -5.0, -8.0),
2086        ]);
2087        let b = p.get_bounds();
2088        assert_eq!(b.x, -5.0);
2089        assert_eq!(b.y, -8.0);
2090        assert_eq!(b.width, 15.0);
2091        assert_eq!(b.height, 28.0);
2092
2093        for el in p.items.as_ref() {
2094            assert!(
2095                rect_contains(&b, &el.get_bounds()),
2096                "path bounds must contain every element's bounds"
2097            );
2098        }
2099    }
2100
2101    // ------------------------------------------------------ SvgMultiPolygon
2102
2103    #[test]
2104    fn svgmultipolygon_empty_has_default_bounds() {
2105        let mp = SvgMultiPolygon::create(SvgPathVec::from_vec(Vec::new()));
2106        assert_eq!(mp.get_bounds(), SvgRect::default());
2107    }
2108
2109    #[test]
2110    fn svgmultipolygon_bounds_union_all_rings() {
2111        let mp = SvgMultiPolygon::create(SvgPathVec::from_vec(vec![
2112            make_path(vec![line_el(0.0, 0.0, 10.0, 10.0)]),
2113            make_path(vec![line_el(-4.0, 30.0, 2.0, 33.0)]),
2114        ]));
2115        let b = mp.get_bounds();
2116        assert_eq!(b.x, -4.0);
2117        assert_eq!(b.y, 0.0);
2118        assert_eq!(b.width, 14.0);
2119        assert_eq!(b.height, 33.0);
2120    }
2121
2122    #[test]
2123    fn svgmultipolygon_bounds_must_contain_geometry_after_an_empty_first_ring() {
2124        // BUG: get_bounds() seeds from rings[0].items[0]; when the FIRST ring is
2125        // empty it bails out to SvgRect::default() and silently drops every
2126        // later ring's geometry.
2127        let mp = SvgMultiPolygon::create(SvgPathVec::from_vec(vec![
2128            make_path(Vec::new()),
2129            make_path(vec![line_el(100.0, 100.0, 200.0, 200.0)]),
2130        ]));
2131        let b = mp.get_bounds();
2132        let expected = SvgRect {
2133            width: 100.0,
2134            height: 100.0,
2135            x: 100.0,
2136            y: 100.0,
2137            ..SvgRect::default()
2138        };
2139        assert!(
2140            rect_contains(&b, &expected),
2141            "bounds {b:?} must contain the geometry of the non-empty ring {expected:?}"
2142        );
2143    }
2144
2145    // ------------------------------------------------------- SvgSimpleNode
2146
2147    #[test]
2148    fn svgsimplenode_is_closed_per_variant() {
2149        let circle = SvgCircle {
2150            center_x: 0.0,
2151            center_y: 0.0,
2152            radius: 5.0,
2153        };
2154        assert!(SvgSimpleNode::Circle(circle).is_closed());
2155        assert!(SvgSimpleNode::CircleHole(circle).is_closed());
2156        assert!(SvgSimpleNode::Rect(SvgRect::default()).is_closed());
2157        assert!(SvgSimpleNode::RectHole(SvgRect::default()).is_closed());
2158
2159        assert!(!SvgSimpleNode::Path(make_path(vec![line_el(0.0, 0.0, 1.0, 0.0)])).is_closed());
2160        assert!(SvgSimpleNode::Path(make_path(vec![line_el(2.0, 2.0, 2.0, 2.0)])).is_closed());
2161        // an empty path is not a closed shape
2162        assert!(!SvgSimpleNode::Path(make_path(Vec::new())).is_closed());
2163    }
2164
2165    #[test]
2166    fn svgsimplenode_get_bounds_per_variant() {
2167        let circle = SvgCircle {
2168            center_x: 10.0,
2169            center_y: 10.0,
2170            radius: 2.0,
2171        };
2172        let b = SvgSimpleNode::Circle(circle).get_bounds();
2173        assert_eq!((b.x, b.y, b.width, b.height), (8.0, 8.0, 4.0, 4.0));
2174        assert_eq!(SvgSimpleNode::CircleHole(circle).get_bounds(), b);
2175
2176        let rect = SvgRect {
2177            width: 3.0,
2178            height: 4.0,
2179            x: 1.0,
2180            y: 2.0,
2181            ..SvgRect::default()
2182        };
2183        assert_eq!(SvgSimpleNode::Rect(rect).get_bounds(), rect);
2184        assert_eq!(SvgSimpleNode::RectHole(rect).get_bounds(), rect);
2185
2186        // empty path -> default bounds, no panic
2187        assert_eq!(
2188            SvgSimpleNode::Path(make_path(Vec::new())).get_bounds(),
2189            SvgRect::default()
2190        );
2191    }
2192
2193    // ------------------------------------------------------------- SvgNode
2194
2195    #[test]
2196    fn svgnode_get_bounds_of_empty_collections_is_default() {
2197        assert_eq!(
2198            SvgNode::MultiPolygonCollection(SvgMultiPolygonVec::from_vec(Vec::new())).get_bounds(),
2199            SvgRect::default()
2200        );
2201        assert_eq!(
2202            SvgNode::MultiShape(SvgSimpleNodeVec::from_vec(Vec::new())).get_bounds(),
2203            SvgRect::default()
2204        );
2205        assert_eq!(
2206            SvgNode::Path(make_path(Vec::new())).get_bounds(),
2207            SvgRect::default()
2208        );
2209        assert_eq!(
2210            SvgNode::MultiPolygon(SvgMultiPolygon::create(SvgPathVec::from_vec(Vec::new())))
2211                .get_bounds(),
2212            SvgRect::default()
2213        );
2214    }
2215
2216    #[test]
2217    fn svgnode_is_closed_is_vacuously_true_for_empty_collections() {
2218        assert!(SvgNode::MultiPolygonCollection(SvgMultiPolygonVec::from_vec(Vec::new())).is_closed());
2219        assert!(SvgNode::MultiShape(SvgSimpleNodeVec::from_vec(Vec::new())).is_closed());
2220        assert!(
2221            SvgNode::MultiPolygon(SvgMultiPolygon::create(SvgPathVec::from_vec(Vec::new())))
2222                .is_closed()
2223        );
2224        // ... but an empty *path* is still open
2225        assert!(!SvgNode::Path(make_path(Vec::new())).is_closed());
2226    }
2227
2228    #[test]
2229    fn svgnode_is_closed_false_when_any_subpath_is_open() {
2230        let open = make_path(vec![line_el(0.0, 0.0, 1.0, 0.0)]);
2231        let mut closed = make_path(vec![
2232            line_el(0.0, 0.0, 1.0, 0.0),
2233            line_el(1.0, 0.0, 1.0, 1.0),
2234        ]);
2235        closed.close();
2236
2237        let mp = SvgMultiPolygon::create(SvgPathVec::from_vec(vec![closed.clone(), open.clone()]));
2238        assert!(!SvgNode::MultiPolygon(mp.clone()).is_closed());
2239        assert!(!SvgNode::MultiPolygonCollection(SvgMultiPolygonVec::from_vec(vec![mp])).is_closed());
2240        assert!(!SvgNode::MultiShape(SvgSimpleNodeVec::from_vec(vec![
2241            SvgSimpleNode::Path(open),
2242            SvgSimpleNode::Circle(SvgCircle {
2243                center_x: 0.0,
2244                center_y: 0.0,
2245                radius: 1.0,
2246            }),
2247        ]))
2248        .is_closed());
2249
2250        let all_closed = SvgMultiPolygon::create(SvgPathVec::from_vec(vec![closed]));
2251        assert!(SvgNode::MultiPolygon(all_closed).is_closed());
2252    }
2253
2254    #[test]
2255    fn svgnode_get_bounds_contains_all_children() {
2256        let a = make_path(vec![line_el(0.0, 0.0, 10.0, 10.0)]);
2257        let b = make_path(vec![line_el(100.0, 100.0, 200.0, 200.0)]);
2258        let node = SvgNode::MultiShape(SvgSimpleNodeVec::from_vec(vec![
2259            SvgSimpleNode::Path(a.clone()),
2260            SvgSimpleNode::Path(b.clone()),
2261        ]));
2262        let bounds = node.get_bounds();
2263        assert!(rect_contains(&bounds, &a.get_bounds()));
2264        assert!(rect_contains(&bounds, &b.get_bounds()));
2265    }
2266
2267    #[test]
2268    fn svgnode_rect_and_circle_bounds_are_passthrough() {
2269        let rect = SvgRect {
2270            width: 5.0,
2271            height: 6.0,
2272            x: -1.0,
2273            y: -2.0,
2274            ..SvgRect::default()
2275        };
2276        assert_eq!(SvgNode::Rect(rect).get_bounds(), rect);
2277        assert!(SvgNode::Rect(rect).is_closed());
2278
2279        let circle = SvgCircle {
2280            center_x: 0.0,
2281            center_y: 0.0,
2282            radius: 1.0,
2283        };
2284        assert_eq!(SvgNode::Circle(circle).get_bounds(), circle.get_bounds());
2285        assert!(SvgNode::Circle(circle).is_closed());
2286    }
2287
2288    // ----------------------------------------------------------- SvgCircle
2289
2290    #[test]
2291    fn svgcircle_contains_point_is_strict_and_correct() {
2292        let c = SvgCircle {
2293            center_x: 0.0,
2294            center_y: 0.0,
2295            radius: 1.0,
2296        };
2297        assert!(c.contains_point(0.0, 0.0));
2298        assert!(c.contains_point(0.5, 0.5));
2299        // exactly on the perimeter -> NOT contained (strict `<`)
2300        assert!(!c.contains_point(1.0, 0.0));
2301        assert!(!c.contains_point(0.0, -1.0));
2302        assert!(!c.contains_point(2.0, 0.0));
2303        assert!(!c.contains_point(-0.8, -0.8));
2304    }
2305
2306    #[test]
2307    fn svgcircle_zero_radius_contains_nothing_not_even_its_center() {
2308        let c = SvgCircle {
2309            center_x: 3.0,
2310            center_y: 3.0,
2311            radius: 0.0,
2312        };
2313        assert!(!c.contains_point(3.0, 3.0));
2314        assert!(!c.contains_point(0.0, 0.0));
2315    }
2316
2317    #[test]
2318    fn svgcircle_negative_radius_behaves_like_its_absolute_value() {
2319        // r*r discards the sign, so a negative radius is NOT treated as empty.
2320        // Documented here so a future fix has to update this test deliberately.
2321        let neg = SvgCircle {
2322            center_x: 0.0,
2323            center_y: 0.0,
2324            radius: -2.0,
2325        };
2326        let pos = SvgCircle {
2327            center_x: 0.0,
2328            center_y: 0.0,
2329            radius: 2.0,
2330        };
2331        assert_eq!(neg.contains_point(0.0, 0.0), pos.contains_point(0.0, 0.0));
2332        assert_eq!(neg.contains_point(1.9, 0.0), pos.contains_point(1.9, 0.0));
2333        assert_eq!(neg.contains_point(5.0, 0.0), pos.contains_point(5.0, 0.0));
2334    }
2335
2336    #[test]
2337    fn svgcircle_contains_point_nan_and_inf_are_false_not_a_panic() {
2338        let c = SvgCircle {
2339            center_x: 0.0,
2340            center_y: 0.0,
2341            radius: 1.0,
2342        };
2343        // every comparison against NaN is false
2344        assert!(!c.contains_point(f32::NAN, 0.0));
2345        assert!(!c.contains_point(0.0, f32::NAN));
2346        assert!(!c.contains_point(f32::INFINITY, 0.0));
2347        assert!(!c.contains_point(f32::NEG_INFINITY, f32::NEG_INFINITY));
2348
2349        let nan_r = SvgCircle {
2350            center_x: 0.0,
2351            center_y: 0.0,
2352            radius: f32::NAN,
2353        };
2354        assert!(!nan_r.contains_point(0.0, 0.0));
2355    }
2356
2357    #[test]
2358    fn svgcircle_contains_point_at_f32_extremes_does_not_panic() {
2359        let c = SvgCircle {
2360            center_x: 0.0,
2361            center_y: 0.0,
2362            radius: f32::MAX,
2363        };
2364        // x_diff*x_diff overflows to +inf, which is never < radius^2 (+inf)
2365        assert!(!c.contains_point(f32::MAX, f32::MAX));
2366        assert!(c.contains_point(1.0, 1.0));
2367        assert!(!c.contains_point(f32::MIN, 0.0));
2368    }
2369
2370    #[test]
2371    fn svgcircle_get_bounds_is_the_enclosing_square() {
2372        let c = SvgCircle {
2373            center_x: 5.0,
2374            center_y: -5.0,
2375            radius: 2.5,
2376        };
2377        let b = c.get_bounds();
2378        assert_eq!((b.x, b.y, b.width, b.height), (2.5, -7.5, 5.0, 5.0));
2379        // a point just inside the bbox's left edge is still outside the circle
2380        assert!(!c.contains_point(b.x + 0.1, b.y + b.height / 2.0));
2381        assert!(c.contains_point(c.center_x, c.center_y));
2382    }
2383
2384    #[test]
2385    fn svgcircle_get_bounds_with_nan_and_huge_radius_does_not_panic() {
2386        let nan = SvgCircle {
2387            center_x: 0.0,
2388            center_y: 0.0,
2389            radius: f32::NAN,
2390        };
2391        let b = nan.get_bounds();
2392        assert!(b.width.is_nan() && b.x.is_nan());
2393
2394        let huge = SvgCircle {
2395            center_x: 0.0,
2396            center_y: 0.0,
2397            radius: f32::MAX,
2398        };
2399        let b = huge.get_bounds();
2400        // radius * 2.0 overflows f32 -> +inf (saturates, does not wrap negative)
2401        assert!(b.width.is_infinite() && b.width > 0.0);
2402        assert_eq!(b.x, -f32::MAX);
2403    }
2404
2405    // ------------------------------------------------ Tessellated* wrappers
2406
2407    #[test]
2408    fn tessellated_svg_node_empty_is_a_neutral_value() {
2409        let e = TessellatedSvgNode::empty();
2410        assert!(e.vertices.as_ref().is_empty());
2411        assert!(e.indices.as_ref().is_empty());
2412        assert_eq!(e, TessellatedSvgNode::default());
2413    }
2414
2415    #[test]
2416    fn tessellated_colored_svg_node_empty_is_a_neutral_value() {
2417        let e = TessellatedColoredSvgNode::empty();
2418        assert!(e.vertices.as_ref().is_empty());
2419        assert!(e.indices.as_ref().is_empty());
2420        assert_eq!(e, TessellatedColoredSvgNode::default());
2421    }
2422
2423    #[test]
2424    fn tessellated_svg_node_vec_ref_round_trips_through_as_slice() {
2425        let node = TessellatedSvgNode {
2426            vertices: vec![SvgVertex { x: 1.0, y: 2.0 }].into(),
2427            indices: vec![0_u32].into(),
2428        };
2429        let v = TessellatedSvgNodeVec::from_vec(vec![node.clone(), TessellatedSvgNode::empty()]);
2430        let r = v.get_ref();
2431        assert_eq!(r.len, 2);
2432        let slice = r.as_slice();
2433        assert_eq!(slice.len(), 2);
2434        assert_eq!(slice[0], node);
2435        assert_eq!(slice[1], TessellatedSvgNode::empty());
2436    }
2437
2438    #[test]
2439    fn tessellated_svg_node_vec_ref_on_empty_vec_yields_an_empty_slice() {
2440        // as_slice() calls slice::from_raw_parts - an empty vec must still
2441        // produce a valid (dangling but aligned) pointer, never a null deref.
2442        let v = TessellatedSvgNodeVec::from_vec(Vec::new());
2443        let r = v.get_ref();
2444        assert_eq!(r.len, 0);
2445        assert!(r.as_slice().is_empty());
2446        assert!(!r.ptr.is_null(), "raw ptr must never be null");
2447    }
2448
2449    #[test]
2450    fn tessellated_colored_svg_node_vec_ref_round_trips_through_as_slice() {
2451        let node = TessellatedColoredSvgNode {
2452            vertices: vec![SvgColoredVertex {
2453                x: 1.0,
2454                y: 2.0,
2455                z: 3.0,
2456                r: 1.0,
2457                g: 0.5,
2458                b: 0.25,
2459                a: 1.0,
2460            }]
2461            .into(),
2462            indices: vec![0_u32, 1, 2].into(),
2463        };
2464        let v = TessellatedColoredSvgNodeVec::from_vec(vec![node.clone()]);
2465        let r = v.get_ref();
2466        assert_eq!(r.len, 1);
2467        assert_eq!(r.as_slice().len(), 1);
2468        assert_eq!(r.as_slice()[0], node);
2469    }
2470
2471    #[test]
2472    fn tessellated_colored_svg_node_vec_ref_on_empty_vec_yields_an_empty_slice() {
2473        let v = TessellatedColoredSvgNodeVec::from_vec(Vec::new());
2474        let r = v.get_ref();
2475        assert_eq!(r.len, 0);
2476        assert!(r.as_slice().is_empty());
2477        assert!(!r.ptr.is_null(), "raw ptr must never be null");
2478    }
2479
2480    // ------------------------------------- compute_svg_transform_uniforms
2481
2482    fn matrix_of(u: &Uniform) -> [f32; 16] {
2483        match u.uniform_type {
2484            UniformType::Matrix4 { transpose, matrix } => {
2485                assert!(!transpose, "SVG shaders expect a pre-transposed matrix");
2486                matrix
2487            }
2488            _ => panic!("expected a Matrix4 uniform"),
2489        }
2490    }
2491
2492    fn bbox_of(u: &Uniform) -> [f32; 2] {
2493        match u.uniform_type {
2494            UniformType::FloatVec2(v) => v,
2495            _ => panic!("expected a FloatVec2 uniform"),
2496        }
2497    }
2498
2499    #[test]
2500    fn compute_svg_transform_uniforms_zero_size_no_transforms_is_finite() {
2501        let (bbox, tf) = compute_svg_transform_uniforms(
2502            PhysicalSizeU32 {
2503                width: 0,
2504                height: 0,
2505            },
2506            &[],
2507        );
2508        assert_eq!(bbox.uniform_name.as_str(), "vBboxSize");
2509        assert_eq!(bbox_of(&bbox), [0.0, 0.0]);
2510
2511        assert_eq!(tf.uniform_name.as_str(), "vTransformMatrix");
2512        let m = matrix_of(&tf);
2513        assert!(
2514            m.iter().all(|f| f.is_finite()),
2515            "a zero-sized target must not produce NaN/inf in the matrix: {m:?}"
2516        );
2517    }
2518
2519    #[test]
2520    fn compute_svg_transform_uniforms_at_u32_max_does_not_panic() {
2521        let (bbox, tf) = compute_svg_transform_uniforms(
2522            PhysicalSizeU32 {
2523                width: u32::MAX,
2524                height: u32::MAX,
2525            },
2526            &[],
2527        );
2528        let b = bbox_of(&bbox);
2529        assert!(b[0].is_finite() && b[0] > 0.0);
2530        assert_eq!(b[0], u32::MAX as f32);
2531        assert_eq!(b[1], u32::MAX as f32);
2532        let m = matrix_of(&tf);
2533        assert!(m.iter().all(|f| f.is_finite()), "matrix: {m:?}");
2534    }
2535
2536    #[test]
2537    fn compute_svg_transform_uniforms_translation_changes_the_matrix() {
2538        let size = PhysicalSizeU32 {
2539            width: 800,
2540            height: 600,
2541        };
2542        let identity = matrix_of(&compute_svg_transform_uniforms(size, &[]).1);
2543        let translated = matrix_of(
2544            &compute_svg_transform_uniforms(
2545                size,
2546                &[
2547                    StyleTransform::TranslateX(PixelValue::px(10.0)),
2548                    StyleTransform::TranslateY(PixelValue::px(-20.0)),
2549                ],
2550            )
2551            .1,
2552        );
2553        assert!(
2554            translated.iter().all(|f| f.is_finite()),
2555            "matrix: {translated:?}"
2556        );
2557        assert!(
2558            identity != translated,
2559            "a translation must actually alter the transform matrix"
2560        );
2561    }
2562
2563    #[test]
2564    fn compute_svg_transform_uniforms_extreme_translation_does_not_panic() {
2565        let size = PhysicalSizeU32 {
2566            width: 1,
2567            height: 1,
2568        };
2569        let (bbox, tf) = compute_svg_transform_uniforms(
2570            size,
2571            &[
2572                StyleTransform::TranslateX(PixelValue::px(f32::MAX)),
2573                StyleTransform::TranslateY(PixelValue::px(-f32::MAX)),
2574            ],
2575        );
2576        assert_eq!(bbox_of(&bbox), [1.0, 1.0]);
2577        // no assertion on finiteness here: the transform itself is degenerate,
2578        // we only require that building the uniform does not panic
2579        let _ = matrix_of(&tf);
2580    }
2581
2582    // ------------------------------------------------------------ SvgStyle
2583
2584    #[test]
2585    fn svgstyle_getters_read_through_to_both_variants() {
2586        let fill = SvgStyle::Fill(SvgFillStyle::default());
2587        assert!(fill.get_antialias());
2588        assert!(!fill.get_high_quality_aa());
2589        assert_eq!(fill.get_transform(), SvgTransform::default());
2590
2591        let stroke = SvgStyle::Stroke(SvgStrokeStyle::default());
2592        assert!(stroke.get_antialias());
2593        assert!(!stroke.get_high_quality_aa());
2594        assert_eq!(stroke.get_transform(), SvgTransform::default());
2595    }
2596
2597    #[test]
2598    fn svgstyle_getters_reflect_non_default_fields() {
2599        let transform = SvgTransform {
2600            sx: 2.0,
2601            kx: 0.5,
2602            ky: -0.5,
2603            sy: 3.0,
2604            tx: 10.0,
2605            ty: -10.0,
2606        };
2607        let fill = SvgStyle::Fill(SvgFillStyle {
2608            anti_alias: false,
2609            high_quality_aa: true,
2610            transform,
2611            ..SvgFillStyle::default()
2612        });
2613        assert!(!fill.get_antialias());
2614        assert!(fill.get_high_quality_aa());
2615        assert_eq!(fill.get_transform(), transform);
2616
2617        let stroke = SvgStyle::Stroke(SvgStrokeStyle {
2618            anti_alias: false,
2619            high_quality_aa: true,
2620            transform,
2621            ..SvgStrokeStyle::default()
2622        });
2623        assert!(!stroke.get_antialias());
2624        assert!(stroke.get_high_quality_aa());
2625        assert_eq!(stroke.get_transform(), transform);
2626    }
2627
2628    // ------------------------------------------------------ SvgParseError
2629
2630    #[test]
2631    fn svgparseerror_display_is_non_empty_for_every_variant() {
2632        let variants = [
2633            SvgParseError::NoParserAvailable,
2634            SvgParseError::ElementsLimitReached,
2635            SvgParseError::NotAnUtf8Str,
2636            SvgParseError::MalformedGZip,
2637            SvgParseError::InvalidSize,
2638            SvgParseError::ParsingFailed(XmlError::NoRootNode),
2639        ];
2640
2641        for v in &variants {
2642            let s = v.to_string();
2643            assert!(!s.is_empty(), "empty Display output for {v:?}");
2644            assert!(
2645                !s.contains("\u{0}"),
2646                "Display output must not contain NUL bytes"
2647            );
2648        }
2649    }
2650
2651    #[test]
2652    fn svgparseerror_display_of_parsing_failed_embeds_the_inner_error() {
2653        let inner = XmlError::NoRootNode;
2654        let s = SvgParseError::ParsingFailed(inner.clone()).to_string();
2655        assert!(s.starts_with("Error parsing SVG:"), "got: {s}");
2656        assert!(
2657            s.contains(&inner.to_string()),
2658            "outer message {s:?} must embed the inner XmlError message"
2659        );
2660    }
2661}