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