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

            
14
use alloc::{
15
    string::{String, ToString},
16
    vec::Vec,
17
};
18
use core::fmt;
19

            
20
use 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

            
31
use 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)
42
const DEFAULT_MITER_LIMIT: f32 = 4.0;
43
/// Default stroke width in pixels
44
const DEFAULT_LINE_WIDTH: f32 = 1.0;
45
/// Default tessellation tolerance in pixels (smaller = more vertices, higher quality)
46
const 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)]
51
pub 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)]
61
pub struct SvgLine {
62
    /// Start point of the line
63
    pub start: SvgPoint,
64
    /// End point of the line
65
    pub end: SvgPoint,
66
}
67

            
68
impl SvgLine {
69
    /// Creates a new line segment from start to end point
70
    #[inline]
71
25771
    #[must_use] pub const fn new(start: SvgPoint, end: SvgPoint) -> Self {
72
25771
        Self { start, end }
73
25771
    }
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
736
    #[must_use] pub fn inwards_normal(&self) -> Option<SvgPoint> {
80
736
        let dx = self.end.x - self.start.x;
81
736
        let dy = self.end.y - self.start.y;
82
736
        let edge_length = dx.hypot(dy);
83
736
        let x = -dy / edge_length;
84
736
        let y = dx / edge_length;
85

            
86
736
        if x.is_finite() && y.is_finite() {
87
707
            Some(SvgPoint { x, y })
88
        } else {
89
29
            None
90
        }
91
736
    }
92

            
93
    /// Computes the outward-facing normal vector for this line (opposite of `inwards_normal`).
94
730
    #[must_use] pub fn outwards_normal(&self) -> Option<SvgPoint> {
95
730
        let inwards = self.inwards_normal()?;
96
705
        Some(SvgPoint {
97
705
            x: -inwards.x,
98
705
            y: -inwards.y,
99
705
        })
100
730
    }
101

            
102
    /// Reverses the direction of the line by swapping start and end points.
103
11
    pub const fn reverse(&mut self) {
104
11
        core::mem::swap(&mut self.start, &mut self.end);
105
11
    }
106
    /// Returns the start point of the line.
107
1326
    #[must_use] pub const fn get_start(&self) -> SvgPoint {
108
1326
        self.start
109
1326
    }
110
    /// Returns the end point of the line.
111
582
    #[must_use] pub const fn get_end(&self) -> SvgPoint {
112
582
        self.end
113
582
    }
114

            
115
    /// Returns the parametric `t` value (0.0–1.0) at the given arc-length offset.
116
10
    #[must_use] pub fn get_t_at_offset(&self, offset: f64) -> f64 {
117
10
        offset / self.get_length()
118
10
    }
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
5
    #[must_use] pub fn get_tangent_vector_at_t(&self) -> SvgVector {
124
5
        let dx = self.end.x - self.start.x;
125
5
        let dy = self.end.y - self.start.y;
126
5
        SvgVector {
127
5
            x: f64::from(dx),
128
5
            y: f64::from(dy),
129
5
        }
130
5
        .normalize()
131
5
    }
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
16
    #[must_use] pub fn get_x_at_t(&self, t: f64) -> f64 {
136
16
        f64::from(self.start.x) + (f64::from(self.end.x) - f64::from(self.start.x)) * t
137
16
    }
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
12
    #[must_use] pub fn get_y_at_t(&self, t: f64) -> f64 {
142
12
        f64::from(self.start.y) + (f64::from(self.end.y) - f64::from(self.start.y)) * t
143
12
    }
144

            
145
    /// Returns the Euclidean length of the line segment.
146
18
    #[must_use] pub fn get_length(&self) -> f64 {
147
18
        let dx = self.end.x - self.start.x;
148
18
        let dy = self.end.y - self.start.y;
149
18
        f64::from(libm::hypotf(dx, dy))
150
18
    }
151

            
152
    /// Returns the axis-aligned bounding rectangle of this line segment.
153
459
    #[must_use] pub fn get_bounds(&self) -> SvgRect {
154
459
        let min_x = self.start.x.min(self.end.x);
155
459
        let max_x = self.start.x.max(self.end.x);
156

            
157
459
        let min_y = self.start.y.min(self.end.y);
158
459
        let max_y = self.start.y.max(self.end.y);
159

            
160
459
        let width = (max_x - min_x).abs();
161
459
        let height = (max_y - min_y).abs();
162

            
163
459
        SvgRect {
164
459
            width,
165
459
            height,
166
459
            x: min_x,
167
459
            y: min_y,
168
459
            radius_top_left: 0.0,
169
459
            radius_top_right: 0.0,
170
459
            radius_bottom_left: 0.0,
171
459
            radius_bottom_right: 0.0,
172
459
        }
173
459
    }
174
}
175

            
176
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
177
#[repr(C, u8)]
178
pub enum SvgPathElement {
179
    Line(SvgLine),
180
    QuadraticCurve(SvgQuadraticCurve),
181
    CubicCurve(SvgCubicCurve),
182
}
183

            
184
impl_option!(
185
    SvgPathElement,
186
    OptionSvgPathElement,
187
    [Debug, Copy, Clone, PartialEq, PartialOrd]
188
);
189

            
190
impl SvgPathElement {
191
    /// Creates a line path element from a `SvgLine`
192
    #[inline]
193
38
    #[must_use] pub const fn line(l: SvgLine) -> Self {
194
38
        Self::Line(l)
195
38
    }
196

            
197
    /// Creates a quadratic curve path element from a `SvgQuadraticCurve`
198
    #[inline]
199
10
    #[must_use] pub const fn quadratic_curve(qc: SvgQuadraticCurve) -> Self {
200
10
        Self::QuadraticCurve(qc)
201
10
    }
202

            
203
    /// Creates a cubic curve path element from a `SvgCubicCurve`
204
    #[inline]
205
13
    #[must_use] pub const fn cubic_curve(cc: SvgCubicCurve) -> Self {
206
13
        Self::CubicCurve(cc)
207
13
    }
208

            
209
    /// Sets the end point of this path element.
210
183
    pub const fn set_last(&mut self, point: SvgPoint) {
211
183
        match self {
212
168
            Self::Line(l) => l.end = point,
213
12
            Self::QuadraticCurve(qc) => qc.end = point,
214
3
            Self::CubicCurve(cc) => cc.end = point,
215
        }
216
183
    }
217

            
218
    /// Sets the start point of this path element.
219
184
    pub const fn set_first(&mut self, point: SvgPoint) {
220
184
        match self {
221
168
            Self::Line(l) => l.start = point,
222
1
            Self::QuadraticCurve(qc) => qc.start = point,
223
15
            Self::CubicCurve(cc) => cc.start = point,
224
        }
225
184
    }
226

            
227
    /// Reverses the direction of this path element.
228
12
    pub const fn reverse(&mut self) {
229
12
        match self {
230
6
            Self::Line(l) => l.reverse(),
231
2
            Self::QuadraticCurve(qc) => qc.reverse(),
232
4
            Self::CubicCurve(cc) => cc.reverse(),
233
        }
234
12
    }
235
    /// Returns the start point of this path element.
236
1467
    #[must_use] pub const fn get_start(&self) -> SvgPoint {
237
1467
        match self {
238
1320
            Self::Line(l) => l.get_start(),
239
8
            Self::QuadraticCurve(qc) => qc.get_start(),
240
139
            Self::CubicCurve(cc) => cc.get_start(),
241
        }
242
1467
    }
243
    /// Returns the end point of this path element.
244
723
    #[must_use] pub const fn get_end(&self) -> SvgPoint {
245
723
        match self {
246
577
            Self::Line(l) => l.get_end(),
247
8
            Self::QuadraticCurve(qc) => qc.get_end(),
248
138
            Self::CubicCurve(cc) => cc.get_end(),
249
        }
250
723
    }
251
    /// Returns the axis-aligned bounding rectangle of this path element.
252
456
    #[must_use] pub fn get_bounds(&self) -> SvgRect {
253
456
        match self {
254
454
            Self::Line(l) => l.get_bounds(),
255
1
            Self::QuadraticCurve(qc) => qc.get_bounds(),
256
1
            Self::CubicCurve(cc) => cc.get_bounds(),
257
        }
258
456
    }
259
    /// Returns the arc length of this path element.
260
4
    #[must_use] pub fn get_length(&self) -> f64 {
261
4
        match self {
262
1
            Self::Line(l) => l.get_length(),
263
1
            Self::QuadraticCurve(qc) => qc.get_length(),
264
2
            Self::CubicCurve(cc) => cc.get_length(),
265
        }
266
4
    }
267
    /// Returns the parametric `t` value at the given arc-length offset.
268
9
    #[must_use] pub fn get_t_at_offset(&self, offset: f64) -> f64 {
269
9
        match self {
270
            Self::Line(l) => l.get_t_at_offset(offset),
271
3
            Self::QuadraticCurve(qc) => qc.get_t_at_offset(offset),
272
6
            Self::CubicCurve(cc) => cc.get_t_at_offset(offset),
273
        }
274
9
    }
275
    /// Returns the normalized tangent vector at parametric position `t`.
276
14
    #[must_use] pub fn get_tangent_vector_at_t(&self, t: f64) -> SvgVector {
277
14
        match self {
278
3
            Self::Line(l) => l.get_tangent_vector_at_t(),
279
6
            Self::QuadraticCurve(qc) => qc.get_tangent_vector_at_t(t),
280
5
            Self::CubicCurve(cc) => cc.get_tangent_vector_at_t(t),
281
        }
282
14
    }
283
    /// Returns the X coordinate at parametric position `t`.
284
15
    #[must_use] pub fn get_x_at_t(&self, t: f64) -> f64 {
285
15
        match self {
286
5
            Self::Line(l) => l.get_x_at_t(t),
287
5
            Self::QuadraticCurve(qc) => qc.get_x_at_t(t),
288
5
            Self::CubicCurve(cc) => cc.get_x_at_t(t),
289
        }
290
15
    }
291
    /// Returns the Y coordinate at parametric position `t`.
292
15
    #[must_use] pub fn get_y_at_t(&self, t: f64) -> f64 {
293
15
        match self {
294
5
            Self::Line(l) => l.get_y_at_t(t),
295
5
            Self::QuadraticCurve(qc) => qc.get_y_at_t(t),
296
5
            Self::CubicCurve(cc) => cc.get_y_at_t(t),
297
        }
298
15
    }
299
}
300

            
301
impl_vec!(SvgPathElement, SvgPathElementVec, SvgPathElementVecDestructor, SvgPathElementVecDestructorType, SvgPathElementVecSlice, OptionSvgPathElement);
302
impl_vec_debug!(SvgPathElement, SvgPathElementVec);
303
impl_vec_clone!(
304
    SvgPathElement,
305
    SvgPathElementVec,
306
    SvgPathElementVecDestructor
307
);
308
impl_vec_partialeq!(SvgPathElement, SvgPathElementVec);
309
impl_vec_partialord!(SvgPathElement, SvgPathElementVec);
310

            
311
#[derive(Debug, Clone, PartialEq, PartialOrd)]
312
#[repr(C)]
313
pub struct SvgPath {
314
    pub items: SvgPathElementVec,
315
}
316

            
317
impl_option!(
318
    SvgPath,
319
    OptionSvgPath,
320
    copy = false,
321
    [Debug, Clone, PartialEq, PartialOrd]
322
);
323

            
324
impl SvgPath {
325
    /// Creates a new `SvgPath` from a vector of path elements
326
    #[inline]
327
32
    #[must_use] pub const fn create(items: SvgPathElementVec) -> Self {
328
32
        Self { items }
329
32
    }
330

            
331
    /// Returns the start point of the first element, or `None` if the path is empty.
332
6
    #[must_use] pub fn get_start(&self) -> Option<SvgPoint> {
333
6
        self.items.as_ref().first().map(SvgPathElement::get_start)
334
6
    }
335

            
336
    /// Returns the end point of the last element, or `None` if the path is empty.
337
6
    #[must_use] pub fn get_end(&self) -> Option<SvgPoint> {
338
6
        self.items.as_ref().last().map(SvgPathElement::get_end)
339
6
    }
340

            
341
    /// Closes the path by appending a line from the last point to the first point, if needed.
342
5
    pub fn close(&mut self) {
343
5
        let Some(first) = self.items.as_ref().first() else {
344
1
            return;
345
        };
346
4
        let Some(last) = self.items.as_ref().last() else {
347
            return;
348
        };
349
4
        if first.get_start() != last.get_end() {
350
3
            let mut elements = self.items.as_slice().to_vec();
351
3
            elements.push(SvgPathElement::Line(SvgLine {
352
3
                start: last.get_end(),
353
3
                end: first.get_start(),
354
3
            }));
355
3
            self.items = elements.into();
356
3
        }
357
5
    }
358

            
359
    /// Returns `true` if the path's first start point equals its last end point.
360
424
    #[must_use] pub fn is_closed(&self) -> bool {
361
424
        let first = self.items.as_ref().first();
362
424
        let last = self.items.as_ref().last();
363
424
        match (first, last) {
364
409
            (Some(f), Some(l)) => (f.get_start() == l.get_end()),
365
15
            _ => false,
366
        }
367
424
    }
368

            
369
    /// Reverses the order and direction of all elements in the path.
370
3
    pub fn reverse(&mut self) {
371
        // swap self.items with a default vec
372
3
        let mut vec = SvgPathElementVec::from_const_slice(&[]);
373
3
        core::mem::swap(&mut vec, &mut self.items);
374
3
        let mut vec = vec.into_library_owned_vec();
375

            
376
        // reverse the order of items in the vec
377
3
        vec.reverse();
378

            
379
        // reverse the order inside the item itself
380
        // i.e. swap line.start and line.end
381
9
        for item in &mut vec {
382
6
            item.reverse();
383
6
        }
384

            
385
        // swap back
386
3
        let mut vec = SvgPathElementVec::from_vec(vec);
387
3
        core::mem::swap(&mut vec, &mut self.items);
388
3
    }
389

            
390
    /// Joins another path onto the end of this one, interpolating the join point.
391
4
    pub fn join_with(&mut self, mut path: Self) -> Option<()> {
392
4
        let self_last_point = self.items.as_ref().last()?.get_end();
393
3
        let other_start_point = path.items.as_ref().first()?.get_start();
394
2
        let interpolated_join_point = SvgPoint {
395
2
            x: f32::midpoint(self_last_point.x, other_start_point.x),
396
2
            y: f32::midpoint(self_last_point.y, other_start_point.y),
397
2
        };
398

            
399
        // swap self.items with a default vec
400
2
        let mut vec = SvgPathElementVec::from_const_slice(&[]);
401
2
        core::mem::swap(&mut vec, &mut self.items);
402
2
        let mut vec = vec.into_library_owned_vec();
403

            
404
2
        let mut other = SvgPathElementVec::from_const_slice(&[]);
405
2
        core::mem::swap(&mut other, &mut path.items);
406
2
        let mut other = other.into_library_owned_vec();
407

            
408
2
        let vec_len = vec.len() - 1;
409
2
        vec.get_mut(vec_len)?.set_last(interpolated_join_point);
410
2
        other.get_mut(0)?.set_first(interpolated_join_point);
411
2
        vec.append(&mut other);
412

            
413
        // swap back
414
2
        let mut vec = SvgPathElementVec::from_vec(vec);
415
2
        core::mem::swap(&mut vec, &mut self.items);
416

            
417
2
        Some(())
418
4
    }
419
    /// Returns the axis-aligned bounding rectangle of the entire path.
420
8
    #[must_use] pub fn get_bounds(&self) -> SvgRect {
421
8
        let mut first_bounds = match self.items.as_ref().first() {
422
5
            Some(s) => s.get_bounds(),
423
3
            None => return SvgRect::default(),
424
        };
425

            
426
5
        for mp in self.items.as_ref().iter().skip(1) {
427
2
            let mp_bounds = mp.get_bounds();
428
2
            first_bounds.union_with(&mp_bounds);
429
2
        }
430

            
431
5
        first_bounds
432
8
    }
433
}
434

            
435
#[derive(Debug, Clone, PartialEq, PartialOrd)]
436
#[repr(C)]
437
pub struct SvgMultiPolygon {
438
    /// NOTE: If a ring represents a hole, simply reverse the order of points
439
    pub rings: SvgPathVec,
440
}
441

            
442
impl_option!(
443
    SvgMultiPolygon,
444
    OptionSvgMultiPolygon,
445
    copy = false,
446
    [Debug, Clone, PartialEq, PartialOrd]
447
);
448

            
449
impl 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
8
    #[must_use] pub const fn create(rings: SvgPathVec) -> Self {
454
8
        Self { rings }
455
8
    }
456

            
457
    /// Returns the axis-aligned bounding rectangle of all rings in this multi-polygon.
458
15
    #[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
15
        let mut bounds: Option<SvgRect> = None;
463
129
        for ring in &self.rings {
464
557
            for item in &ring.items {
465
443
                let item_bounds = item.get_bounds();
466
443
                match &mut bounds {
467
430
                    Some(b) => b.union_with(&item_bounds),
468
13
                    None => bounds = Some(item_bounds),
469
                }
470
            }
471
        }
472
        // Empty polygon (no items in any ring) has zero-sized bounds at origin.
473
15
        bounds.unwrap_or_default()
474
15
    }
475
}
476

            
477
impl_vec!(SvgPath, SvgPathVec, SvgPathVecDestructor, SvgPathVecDestructorType, SvgPathVecSlice, OptionSvgPath);
478
impl_vec_debug!(SvgPath, SvgPathVec);
479
impl_vec_clone!(SvgPath, SvgPathVec, SvgPathVecDestructor);
480
impl_vec_partialeq!(SvgPath, SvgPathVec);
481
impl_vec_partialord!(SvgPath, SvgPathVec);
482

            
483
impl_vec!(SvgMultiPolygon, SvgMultiPolygonVec, SvgMultiPolygonVecDestructor, SvgMultiPolygonVecDestructorType, SvgMultiPolygonVecSlice, OptionSvgMultiPolygon);
484
impl_vec_debug!(SvgMultiPolygon, SvgMultiPolygonVec);
485
impl_vec_clone!(
486
    SvgMultiPolygon,
487
    SvgMultiPolygonVec,
488
    SvgMultiPolygonVecDestructor
489
);
490
impl_vec_partialeq!(SvgMultiPolygon, SvgMultiPolygonVec);
491
impl_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)]
496
pub 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)]
509
pub enum SvgSimpleNode {
510
    Path(SvgPath),
511
    Circle(SvgCircle),
512
    Rect(SvgRect),
513
    CircleHole(SvgCircle),
514
    RectHole(SvgRect),
515
}
516

            
517
impl_option!(
518
    SvgSimpleNode,
519
    OptionSvgSimpleNode,
520
    copy = false,
521
    [Debug, Clone, PartialOrd, PartialEq]
522
);
523

            
524
impl_vec!(SvgSimpleNode, SvgSimpleNodeVec, SvgSimpleNodeVecDestructor, SvgSimpleNodeVecDestructorType, SvgSimpleNodeVecSlice, OptionSvgSimpleNode);
525
impl_vec_debug!(SvgSimpleNode, SvgSimpleNodeVec);
526
impl_vec_clone!(SvgSimpleNode, SvgSimpleNodeVec, SvgSimpleNodeVecDestructor);
527
impl_vec_partialeq!(SvgSimpleNode, SvgSimpleNodeVec);
528
impl_vec_partialord!(SvgSimpleNode, SvgSimpleNodeVec);
529

            
530
impl 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
7
    #[must_use] pub fn get_bounds(&self) -> SvgRect {
536
7
        match self {
537
3
            Self::Path(a) => a.get_bounds(),
538
1
            Self::Circle(a) => a.get_bounds(),
539
1
            Self::Rect(a) => *a,
540
1
            Self::CircleHole(a) => a.get_bounds(),
541
1
            Self::RectHole(a) => *a,
542
        }
543
7
    }
544
    /// Returns `true` if this node represents a closed shape.
545
8
    #[must_use] pub fn is_closed(&self) -> bool {
546
8
        match self {
547
4
            Self::Path(a) => a.is_closed(),
548
4
            Self::Circle(_) | Self::Rect(_) | Self::CircleHole(_) | Self::RectHole(_) => true,
549
        }
550
8
    }
551
}
552

            
553
impl SvgNode {
554
    /// Returns the axis-aligned bounding rectangle of this SVG node.
555
7
    #[must_use] pub fn get_bounds(&self) -> SvgRect {
556
7
        match self {
557
1
            Self::MultiPolygonCollection(a) => {
558
1
                let mut first_mp_bounds = match a.get(0) {
559
                    Some(s) => s.get_bounds(),
560
1
                    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
1
            Self::MultiPolygon(a) => a.get_bounds(),
570
2
            Self::MultiShape(a) => {
571
2
                let mut first_mp_bounds = match a.get(0) {
572
1
                    Some(s) => s.get_bounds(),
573
1
                    None => return SvgRect::default(),
574
                };
575
1
                for mp in a.iter().skip(1) {
576
1
                    let mp_bounds = mp.get_bounds();
577
1
                    first_mp_bounds.union_with(&mp_bounds);
578
1
                }
579

            
580
1
                first_mp_bounds
581
            }
582
1
            Self::Path(a) => a.get_bounds(),
583
1
            Self::Circle(a) => a.get_bounds(),
584
1
            Self::Rect(a) => *a,
585
        }
586
7
    }
587
    /// Returns `true` if all sub-paths in this node are closed.
588
10
    #[must_use] pub fn is_closed(&self) -> bool {
589
10
        match self {
590
2
            Self::MultiPolygonCollection(a) => {
591
2
                for mp in a {
592
2
                    for p in mp.rings.as_ref() {
593
2
                        if !p.is_closed() {
594
1
                            return false;
595
1
                        }
596
                    }
597
                }
598

            
599
1
                true
600
            }
601
3
            Self::MultiPolygon(a) => {
602
3
                for p in a.rings.as_ref() {
603
3
                    if !p.is_closed() {
604
1
                        return false;
605
2
                    }
606
                }
607

            
608
2
                true
609
            }
610
2
            Self::MultiShape(a) => {
611
2
                for p in a.as_ref() {
612
1
                    if !p.is_closed() {
613
1
                        return false;
614
                    }
615
                }
616

            
617
1
                true
618
            }
619
1
            Self::Path(a) => a.is_closed(),
620
2
            Self::Circle(_) | Self::Rect(_) => true,
621
        }
622
10
    }
623
}
624

            
625
/// An SVG node paired with its visual style (fill or stroke).
626
#[derive(Debug, Clone, PartialOrd, PartialEq)]
627
#[repr(C)]
628
pub 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)]
636
pub struct SvgVertex {
637
    pub x: f32,
638
    pub y: f32,
639
}
640

            
641
impl_option!(
642
    SvgVertex,
643
    OptionSvgVertex,
644
    [Debug, Copy, Clone, PartialOrd, PartialEq]
645
);
646

            
647
impl 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)]
664
pub 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

            
674
impl_option!(
675
    SvgColoredVertex,
676
    OptionSvgColoredVertex,
677
    [Debug, Copy, Clone, PartialOrd, PartialEq]
678
);
679

            
680
impl 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)]
705
pub struct SvgCircle {
706
    pub center_x: f32,
707
    pub center_y: f32,
708
    pub radius: f32,
709
}
710

            
711
impl 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
124
    #[must_use] pub fn contains_point(&self, x: f32, y: f32) -> bool {
715
124
        let x_diff = libm::fabsf(x - self.center_x);
716
124
        let y_diff = libm::fabsf(y - self.center_y);
717
124
        (x_diff * x_diff) + (y_diff * y_diff) < (self.radius * self.radius)
718
124
    }
719
    /// Returns the axis-aligned bounding rectangle of this circle.
720
7
    #[must_use] pub fn get_bounds(&self) -> SvgRect {
721
7
        SvgRect {
722
7
            width: self.radius * 2.0,
723
7
            height: self.radius * 2.0,
724
7
            x: self.center_x - self.radius,
725
7
            y: self.center_y - self.radius,
726
7
            radius_top_left: 0.0,
727
7
            radius_top_right: 0.0,
728
7
            radius_bottom_left: 0.0,
729
7
            radius_bottom_right: 0.0,
730
7
        }
731
7
    }
732
}
733

            
734
#[derive(Debug, Clone, PartialEq, PartialOrd)]
735
#[repr(C)]
736
pub struct TessellatedSvgNode {
737
    pub vertices: SvgVertexVec,
738
    pub indices: U32Vec,
739
}
740

            
741
impl_option!(
742
    TessellatedSvgNode,
743
    OptionTessellatedSvgNode,
744
    copy = false,
745
    [Debug, Clone, PartialEq, PartialOrd]
746
);
747

            
748
impl Default for TessellatedSvgNode {
749
4
    fn default() -> Self {
750
4
        Self {
751
4
            vertices: Vec::new().into(),
752
4
            indices: Vec::new().into(),
753
4
        }
754
4
    }
755
}
756

            
757
impl_vec!(TessellatedSvgNode, TessellatedSvgNodeVec, TessellatedSvgNodeVecDestructor, TessellatedSvgNodeVecDestructorType, TessellatedSvgNodeVecSlice, OptionTessellatedSvgNode);
758
impl_vec_debug!(TessellatedSvgNode, TessellatedSvgNodeVec);
759
impl_vec_partialord!(TessellatedSvgNode, TessellatedSvgNodeVec);
760
impl_vec_clone!(
761
    TessellatedSvgNode,
762
    TessellatedSvgNodeVec,
763
    TessellatedSvgNodeVecDestructor
764
);
765
impl_vec_partialeq!(TessellatedSvgNode, TessellatedSvgNodeVec);
766

            
767
impl TessellatedSvgNode {
768
3
    #[must_use] pub fn empty() -> Self {
769
3
        Self::default()
770
3
    }
771
}
772

            
773
impl TessellatedSvgNodeVec {
774
2
    #[must_use] pub fn get_ref(&self) -> TessellatedSvgNodeVecRef {
775
2
        let slice = self.as_ref();
776
2
        TessellatedSvgNodeVecRef {
777
2
            ptr: slice.as_ptr(),
778
2
            len: slice.len(),
779
2
        }
780
2
    }
781
}
782

            
783
impl 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)]
791
pub struct TessellatedSvgNodeVecRef {
792
    pub ptr: *const TessellatedSvgNode,
793
    pub len: usize,
794
}
795

            
796
impl Clone for TessellatedSvgNodeVecRef {
797
    fn clone(&self) -> Self {
798
        Self {
799
            ptr: self.ptr,
800
            len: self.len,
801
        }
802
    }
803
}
804

            
805
impl TessellatedSvgNodeVecRef {
806
2
    #[must_use] pub const fn as_slice(&self) -> &[TessellatedSvgNode] {
807
2
        unsafe { core::slice::from_raw_parts(self.ptr, self.len) }
808
2
    }
809
}
810

            
811
#[derive(Debug, Clone, PartialEq, PartialOrd)]
812
#[repr(C)]
813
pub struct TessellatedColoredSvgNode {
814
    pub vertices: SvgColoredVertexVec,
815
    pub indices: U32Vec,
816
}
817

            
818
impl_option!(
819
    TessellatedColoredSvgNode,
820
    OptionTessellatedColoredSvgNode,
821
    copy = false,
822
    [Debug, Clone, PartialEq, PartialOrd]
823
);
824

            
825
impl Default for TessellatedColoredSvgNode {
826
2
    fn default() -> Self {
827
2
        Self {
828
2
            vertices: Vec::new().into(),
829
2
            indices: Vec::new().into(),
830
2
        }
831
2
    }
832
}
833

            
834
impl_vec!(TessellatedColoredSvgNode, TessellatedColoredSvgNodeVec, TessellatedColoredSvgNodeVecDestructor, TessellatedColoredSvgNodeVecDestructorType, TessellatedColoredSvgNodeVecSlice, OptionTessellatedColoredSvgNode);
835
impl_vec_debug!(TessellatedColoredSvgNode, TessellatedColoredSvgNodeVec);
836
impl_vec_partialord!(TessellatedColoredSvgNode, TessellatedColoredSvgNodeVec);
837
impl_vec_clone!(
838
    TessellatedColoredSvgNode,
839
    TessellatedColoredSvgNodeVec,
840
    TessellatedColoredSvgNodeVecDestructor
841
);
842
impl_vec_partialeq!(TessellatedColoredSvgNode, TessellatedColoredSvgNodeVec);
843

            
844
impl TessellatedColoredSvgNode {
845
1
    #[must_use] pub fn empty() -> Self {
846
1
        Self::default()
847
1
    }
848
}
849

            
850
impl TessellatedColoredSvgNodeVec {
851
2
    #[must_use] pub fn get_ref(&self) -> TessellatedColoredSvgNodeVecRef {
852
2
        let slice = self.as_ref();
853
2
        TessellatedColoredSvgNodeVecRef {
854
2
            ptr: slice.as_ptr(),
855
2
            len: slice.len(),
856
2
        }
857
2
    }
858
}
859

            
860
impl 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)]
868
pub struct TessellatedColoredSvgNodeVecRef {
869
    pub ptr: *const TessellatedColoredSvgNode,
870
    pub len: usize,
871
}
872

            
873
impl Clone for TessellatedColoredSvgNodeVecRef {
874
    fn clone(&self) -> Self {
875
        Self {
876
            ptr: self.ptr,
877
            len: self.len,
878
        }
879
    }
880
}
881

            
882
impl TessellatedColoredSvgNodeVecRef {
883
3
    #[must_use] pub const fn as_slice(&self) -> &[TessellatedColoredSvgNode] {
884
3
        unsafe { core::slice::from_raw_parts(self.ptr, self.len) }
885
3
    }
886
}
887

            
888
impl_vec!(SvgVertex, SvgVertexVec, SvgVertexVecDestructor, SvgVertexVecDestructorType, SvgVertexVecSlice, OptionSvgVertex);
889
impl_vec_debug!(SvgVertex, SvgVertexVec);
890
impl_vec_partialord!(SvgVertex, SvgVertexVec);
891
impl_vec_clone!(SvgVertex, SvgVertexVec, SvgVertexVecDestructor);
892
impl_vec_partialeq!(SvgVertex, SvgVertexVec);
893

            
894
impl_vec!(SvgColoredVertex, SvgColoredVertexVec, SvgColoredVertexVecDestructor, SvgColoredVertexVecDestructorType, SvgColoredVertexVecSlice, OptionSvgColoredVertex);
895
impl_vec_debug!(SvgColoredVertex, SvgColoredVertexVec);
896
impl_vec_partialord!(SvgColoredVertex, SvgColoredVertexVec);
897
impl_vec_clone!(
898
    SvgColoredVertex,
899
    SvgColoredVertexVec,
900
    SvgColoredVertexVecDestructor
901
);
902
impl_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)]
912
5
fn compute_svg_transform_uniforms(
913
5
    target_size: PhysicalSizeU32,
914
5
    transforms: &[StyleTransform],
915
5
) -> (Uniform, Uniform) {
916
5
    let transform_origin = StyleTransformOrigin {
917
5
        x: PixelValue::px(target_size.width as f32 / 2.0),
918
5
        y: PixelValue::px(target_size.height as f32 / 2.0),
919
5
    };
920

            
921
5
    let computed_transform = ComputedTransform3D::from_style_transform_vec(
922
5
        transforms,
923
5
        &transform_origin,
924
5
        target_size.width as f32,
925
5
        target_size.height as f32,
926
5
        RotationMode::ForWebRender,
927
    );
928

            
929
    // NOTE: OpenGL draws are column-major, while ComputedTransform3D
930
    // is row-major! Need to transpose the matrix!
931
5
    let m = computed_transform.get_column_major().m;
932
80
    let matrix: [f32; 16] = core::array::from_fn(|i| m[i / 4][i % 4]);
933

            
934
5
    let bbox_uniform = Uniform {
935
5
        uniform_name: "vBboxSize".into(),
936
5
        uniform_type: UniformType::FloatVec2([
937
5
            target_size.width as f32,
938
5
            target_size.height as f32,
939
5
        ]),
940
5
    };
941

            
942
5
    let transform_uniform = Uniform {
943
5
        uniform_name: "vTransformMatrix".into(),
944
5
        uniform_type: UniformType::Matrix4 {
945
5
            transpose: false,
946
5
            matrix,
947
5
        },
948
5
    };
949

            
950
5
    (bbox_uniform, transform_uniform)
951
5
}
952

            
953
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
954
#[repr(C)]
955
pub struct TessellatedGPUSvgNode {
956
    pub vertex_index_buffer: VertexBuffer,
957
}
958

            
959
impl 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,
            &[(&self.vertex_index_buffer, &uniforms[..])],
        );
        true
    }
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
#[repr(C)]
pub struct TessellatedColoredGPUSvgNode {
    pub vertex_index_buffer: VertexBuffer,
}
impl TessellatedColoredGPUSvgNode {
    /// Uploads the tesselated SVG node to GPU memory
    #[must_use] pub fn new(node: &TessellatedColoredSvgNode, gl: GlContextPtr) -> Self {
        let svg_shader_id = gl.ptr.svg_multicolor_shader;
        Self {
            vertex_index_buffer: VertexBuffer::new(
                gl,
                svg_shader_id,
                node.vertices.as_ref(),
                node.indices.as_ref(),
                IndexBufferFormat::Triangles,
            ),
        }
    }
    /// Draw the vertex buffer to the texture with the given color and transform
    #[allow(clippy::needless_pass_by_value)] // owned azul value taken by value (public API / ownership-transfer convention)
    pub fn draw(
        &self,
        texture: &mut Texture,
        target_size: PhysicalSizeU32,
        transforms: StyleTransformVec,
    ) -> bool {
        let (bbox_uniform, transform_uniform) =
            compute_svg_transform_uniforms(target_size, transforms.as_ref());
        // two separately-named GL uniforms collected into the draw-call array;
        // not a tuple->array conversion.
        #[allow(clippy::tuple_array_conversions)]
        let uniforms = [bbox_uniform, transform_uniform];
        GlShader::draw(
            texture.gl_context.ptr.svg_multicolor_shader,
            texture,
            &[(&self.vertex_index_buffer, &uniforms[..])],
        );
        true
    }
}
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
#[repr(C, u8)]
pub enum SvgStyle {
    Fill(SvgFillStyle),
    Stroke(SvgStrokeStyle),
}
impl SvgStyle {
4
    #[must_use] pub const fn get_antialias(&self) -> bool {
4
        match self {
2
            Self::Fill(f) => f.anti_alias,
2
            Self::Stroke(s) => s.anti_alias,
        }
4
    }
4
    #[must_use] pub const fn get_high_quality_aa(&self) -> bool {
4
        match self {
2
            Self::Fill(f) => f.high_quality_aa,
2
            Self::Stroke(s) => s.high_quality_aa,
        }
4
    }
94
    #[must_use] pub const fn get_transform(&self) -> SvgTransform {
94
        match self {
82
            Self::Fill(f) => f.transform,
12
            Self::Stroke(s) => s.transform,
        }
94
    }
}
/// SVG fill rule for determining the interior of a shape.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
#[repr(C)]
#[derive(Default)]
pub enum SvgFillRule {
    #[default]
    Winding,
    EvenOdd,
}
#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
#[repr(C)]
pub struct SvgTransform {
    pub sx: f32,
    pub kx: f32,
    pub ky: f32,
    pub sy: f32,
    pub tx: f32,
    pub ty: f32,
}
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
#[repr(C)]
pub struct SvgFillStyle {
    /// See the SVG specification.
    ///
    /// Default value: `LineJoin::Miter`.
    pub line_join: SvgLineJoin,
    /// See the SVG specification.
    ///
    /// Must be greater than or equal to 1.0.
    /// Default value: `StrokeOptions::DEFAULT_MITER_LIMIT`.
    pub miter_limit: f32,
    /// Maximum allowed distance to the path when building an approximation.
    ///
    /// See [Flattening and tolerance](index.html#flattening-and-tolerance).
    /// Default value: `StrokeOptions::DEFAULT_TOLERANCE`.
    pub tolerance: f32,
    /// Whether to use the "winding" or "even / odd" fill rule when tesselating the path
    pub fill_rule: SvgFillRule,
    /// Whether to apply a transform to the points in the path (warning: will be done on the CPU -
    /// expensive)
    pub transform: SvgTransform,
    /// Whether the fill is intended to be anti-aliased (default: true)
    pub anti_alias: bool,
    /// Whether the anti-aliasing has to be of high quality (default: false)
    pub high_quality_aa: bool,
}
impl Default for SvgFillStyle {
78
    fn default() -> Self {
78
        Self {
78
            line_join: SvgLineJoin::Miter,
78
            miter_limit: DEFAULT_MITER_LIMIT,
78
            tolerance: DEFAULT_TOLERANCE,
78
            fill_rule: SvgFillRule::default(),
78
            transform: SvgTransform::default(),
78
            anti_alias: true,
78
            high_quality_aa: false,
78
        }
78
    }
}
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
#[repr(C)]
pub struct SvgStrokeStyle {
    /// Dash pattern
    pub dash_pattern: OptionSvgDashPattern,
    /// Whether to apply a transform to the points in the path (warning: will be done on the CPU -
    /// expensive)
    pub transform: SvgTransform,
    /// What cap to use at the start of each sub-path.
    ///
    /// Default value: `LineCap::Butt`.
    pub start_cap: SvgLineCap,
    /// What cap to use at the end of each sub-path.
    ///
    /// Default value: `LineCap::Butt`.
    pub end_cap: SvgLineCap,
    /// See the SVG specification.
    ///
    /// Default value: `LineJoin::Miter`.
    pub line_join: SvgLineJoin,
    /// Line width
    ///
    /// Default value: `StrokeOptions::DEFAULT_LINE_WIDTH`.
    pub line_width: f32,
    /// See the SVG specification.
    ///
    /// Must be greater than or equal to 1.0.
    /// Default value: `StrokeOptions::DEFAULT_MITER_LIMIT`.
    pub miter_limit: f32,
    /// Maximum allowed distance to the path when building an approximation.
    ///
    /// See [Flattening and tolerance](index.html#flattening-and-tolerance).
    /// Default value: `StrokeOptions::DEFAULT_TOLERANCE`.
    pub tolerance: f32,
    /// Apply line width
    ///
    /// When set to false, the generated vertices will all be positioned in the centre
    /// of the line. The width can be applied later on (eg in a vertex shader) by adding
    /// the vertex normal multiplied by the line with to each vertex position.
    ///
    /// Default value: `true`. NOTE: currently unused!
    pub apply_line_width: bool,
    /// Whether the fill is intended to be anti-aliased (default: true)
    pub anti_alias: bool,
    /// Whether the anti-aliasing has to be of high quality (default: false)
    pub high_quality_aa: bool,
}
impl Default for SvgStrokeStyle {
32
    fn default() -> Self {
32
        Self {
32
            dash_pattern: OptionSvgDashPattern::None,
32
            transform: SvgTransform::default(),
32
            start_cap: SvgLineCap::default(),
32
            end_cap: SvgLineCap::default(),
32
            line_join: SvgLineJoin::default(),
32
            line_width: DEFAULT_LINE_WIDTH,
32
            miter_limit: DEFAULT_MITER_LIMIT,
32
            tolerance: DEFAULT_TOLERANCE,
32
            apply_line_width: true,
32
            anti_alias: true,
32
            high_quality_aa: false,
32
        }
32
    }
}
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
#[repr(C)]
pub struct SvgDashPattern {
    pub offset: f32,
    pub length_1: f32,
    pub gap_1: f32,
    pub length_2: f32,
    pub gap_2: f32,
    pub length_3: f32,
    pub gap_3: f32,
}
impl_option!(
    SvgDashPattern,
    OptionSvgDashPattern,
    [Debug, Copy, Clone, PartialEq, PartialOrd]
);
/// The shape used at the end of open sub-paths when they are stroked.
#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
#[repr(C)]
#[derive(Default)]
pub enum SvgLineCap {
    #[default]
    Butt,
    Square,
    Round,
}
/// The shape used at the corners of stroked paths.
#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
#[repr(C)]
#[derive(Default)]
pub enum SvgLineJoin {
    #[default]
    Miter,
    MiterClip,
    Round,
    Bevel,
}
pub use core::ffi::c_void;
#[derive(Debug, Clone)]
#[repr(C)]
pub struct SvgXmlNode {
    pub node: *const c_void, // usvg::Node
    pub run_destructor: bool,
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct Svg {
    pub tree: *const c_void, // *mut usvg::Tree,
    pub run_destructor: bool,
}
/// SVG `shape-rendering` property controlling quality vs speed tradeoffs.
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[repr(C)]
pub enum ShapeRendering {
    OptimizeSpeed,
    CrispEdges,
    GeometricPrecision,
}
/// SVG `image-rendering` property controlling image quality vs speed.
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[repr(C)]
pub enum ImageRendering {
    OptimizeQuality,
    OptimizeSpeed,
}
/// SVG `text-rendering` property controlling text quality vs speed.
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[repr(C)]
pub enum TextRendering {
    OptimizeSpeed,
    OptimizeLegibility,
    GeometricPrecision,
}
/// Font database source for SVG text rendering.
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[repr(C)]
pub enum FontDatabase {
    Empty,
    System,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
#[repr(C)]
pub struct SvgRenderOptions {
    pub target_size: OptionLayoutSize,
    pub background_color: OptionColorU,
    pub fit: SvgFitTo,
    pub transform: SvgRenderTransform,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
#[repr(C)]
pub struct SvgRenderTransform {
    pub sx: f32,
    pub kx: f32,
    pub ky: f32,
    pub sy: f32,
    pub tx: f32,
    pub ty: f32,
}
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
#[repr(C, u8)]
#[derive(Default)]
pub enum SvgFitTo {
    #[default]
    Original,
    Width(u32),
    Height(u32),
    Zoom(f32),
}
#[derive(Debug, Clone, PartialEq, PartialOrd)]
#[repr(C)]
pub struct SvgParseOptions {
    /// SVG image path. Used to resolve relative image paths.
    pub relative_image_path: OptionString,
    /// Default font family. Will be used when no font-family attribute is set in the SVG. Default:
    /// Times New Roman
    pub default_font_family: AzString,
    /// A list of languages. Will be used to resolve a systemLanguage conditional attribute.
    /// Format: en, en-US. Default: [en]
    pub languages: StringVec,
    /// Target DPI. Impact units conversion. Default: 96.0
    pub dpi: f32,
    /// A default font size. Will be used when no font-size attribute is set in the SVG. Default:
    /// 12
    pub font_size: f32,
    /// Specifies the default shape rendering method. Will be used when an SVG element's
    /// shape-rendering property is set to auto. Default: `GeometricPrecision`
    pub shape_rendering: ShapeRendering,
    /// Specifies the default text rendering method. Will be used when an SVG element's
    /// text-rendering property is set to auto. Default: `OptimizeLegibility`
    pub text_rendering: TextRendering,
    /// Specifies the default image rendering method. Will be used when an SVG element's
    /// image-rendering property is set to auto. Default: `OptimizeQuality`
    pub image_rendering: ImageRendering,
    /// When empty, text elements will be skipped. Default: `System`
    pub fontdb: FontDatabase,
    /// Keep named groups. If set to true, all non-empty groups with id attribute will not be
    /// removed. Default: false
    pub keep_named_groups: bool,
}
impl Default for SvgParseOptions {
506
    fn default() -> Self {
506
        let lang_vec: Vec<AzString> = vec![String::from("en").into()];
506
        Self {
506
            relative_image_path: OptionString::None,
506
            default_font_family: "Times New Roman".to_string().into(),
506
            languages: lang_vec.into(),
506
            dpi: 96.0,
506
            font_size: 12.0,
506
            shape_rendering: ShapeRendering::GeometricPrecision,
506
            text_rendering: TextRendering::OptimizeLegibility,
506
            image_rendering: ImageRendering::OptimizeQuality,
506
            fontdb: FontDatabase::System,
506
            keep_named_groups: false,
506
        }
506
    }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
#[repr(C)]
pub struct SvgXmlOptions {
    pub use_single_quote: bool,
    pub indent: Indent,
    pub attributes_indent: Indent,
}
impl Default for SvgXmlOptions {
13
    fn default() -> Self {
13
        Self {
13
            use_single_quote: false,
13
            indent: Indent::Spaces(2),
13
            attributes_indent: Indent::Spaces(2),
13
        }
13
    }
}
#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
#[repr(C, u8)]
pub enum SvgParseError {
    NoParserAvailable,
    ElementsLimitReached,
    NotAnUtf8Str,
    MalformedGZip,
    InvalidSize,
    ParsingFailed(XmlError),
}
impl fmt::Display for SvgParseError {
7
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use self::SvgParseError::{NoParserAvailable, ElementsLimitReached, NotAnUtf8Str, MalformedGZip, InvalidSize, ParsingFailed};
7
        match self {
1
            NoParserAvailable => write!(
1
                f,
1
                "Library was compiled without SVG support (no parser available)"
            ),
1
            ElementsLimitReached => write!(f, "Error parsing SVG: Elements limit reached"),
1
            NotAnUtf8Str => write!(f, "Error parsing SVG: Not an UTF-8 String"),
1
            MalformedGZip => write!(
1
                f,
1
                "Error parsing SVG: SVG is compressed with a malformed GZIP compression"
            ),
1
            InvalidSize => write!(f, "Error parsing SVG: Invalid size"),
2
            ParsingFailed(e) => write!(f, "Error parsing SVG: Parsing SVG as XML failed: {e}"),
        }
7
    }
}
impl_result!(
    SvgXmlNode,
    SvgParseError,
    ResultSvgXmlNodeSvgParseError,
    copy = false,
    [Debug, Clone]
);
impl_result!(
    Svg,
    SvgParseError,
    ResultSvgSvgParseError,
    copy = false,
    [Debug, Clone]
);
/// Indentation style for SVG XML serialization.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
#[repr(C, u8)]
pub enum Indent {
    None,
    Spaces(u8),
    Tabs,
}
#[cfg(test)]
#[allow(clippy::pedantic, clippy::nursery, clippy::float_cmp)]
mod autotest_generated {
    use super::*;
    // ---------------------------------------------------------------- helpers
    fn pt(x: f32, y: f32) -> SvgPoint {
        SvgPoint { x, y }
    }
    fn line_el(x1: f32, y1: f32, x2: f32, y2: f32) -> SvgPathElement {
        SvgPathElement::line(SvgLine::new(pt(x1, y1), pt(x2, y2)))
    }
    fn make_path(items: Vec<SvgPathElement>) -> SvgPath {
        SvgPath::create(SvgPathElementVec::from_vec(items))
    }
    fn quad() -> SvgQuadraticCurve {
        SvgQuadraticCurve {
            start: pt(0.0, 0.0),
            ctrl: pt(5.0, 10.0),
            end: pt(10.0, 0.0),
        }
    }
    fn cubic() -> SvgCubicCurve {
        SvgCubicCurve {
            start: pt(0.0, 0.0),
            ctrl_1: pt(0.0, 10.0),
            ctrl_2: pt(10.0, 10.0),
            end: pt(10.0, 0.0),
        }
    }
    /// `true` if `outer` fully contains `inner` (used to check bounding-box invariants).
    fn rect_contains(outer: &SvgRect, inner: &SvgRect) -> bool {
        outer.x <= inner.x
            && outer.y <= inner.y
            && outer.x + outer.width >= inner.x + inner.width
            && outer.y + outer.height >= inner.y + inner.height
    }
    // ------------------------------------------------------- SvgLine :: basic
    #[test]
    fn svgline_new_keeps_fields_including_extreme_values() {
        let l = SvgLine::new(pt(1.0, 2.0), pt(3.0, 4.0));
        assert_eq!(l.get_start(), pt(1.0, 2.0));
        assert_eq!(l.get_end(), pt(3.0, 4.0));
        let extreme = SvgLine::new(pt(f32::MIN, f32::MAX), pt(f32::MAX, f32::MIN));
        assert_eq!(extreme.start.x, f32::MIN);
        assert_eq!(extreme.end.x, f32::MAX);
        // NaN survives construction untouched (no normalization happens)
        let nan = SvgLine::new(pt(f32::NAN, 0.0), pt(0.0, f32::NAN));
        assert!(nan.get_start().x.is_nan());
        assert!(nan.get_end().y.is_nan());
    }
    #[test]
    fn svgline_reverse_is_an_involution() {
        let orig = SvgLine::new(pt(-1.5, 2.5), pt(7.0, -9.0));
        let mut l = orig;
        l.reverse();
        assert_eq!(l.get_start(), orig.get_end());
        assert_eq!(l.get_end(), orig.get_start());
        l.reverse();
        assert_eq!(l, orig);
    }
    #[test]
    fn svgline_reverse_does_not_panic_on_extreme_values() {
        let mut l = SvgLine::new(pt(f32::INFINITY, f32::NAN), pt(f32::NEG_INFINITY, f32::MAX));
        l.reverse();
        assert!(l.get_start().x.is_infinite() && l.get_start().x < 0.0);
        assert!(l.get_end().y.is_nan());
    }
    // ----------------------------------------------------- SvgLine :: normals
    #[test]
    fn svgline_inwards_normal_is_unit_length_and_90deg_right() {
        // horizontal line pointing +x -> normal points -y (90deg to the right in SVG coords)
        let l = SvgLine::new(pt(0.0, 0.0), pt(10.0, 0.0));
        let n = l.inwards_normal().expect("non-degenerate line has a normal");
        assert!((n.x - 0.0).abs() < 1e-6);
        assert!((n.y - 1.0).abs() < 1e-6);
        let len = (n.x * n.x + n.y * n.y).sqrt();
        assert!((len - 1.0).abs() < 1e-6, "normal must be unit length");
    }
    #[test]
    fn svgline_outwards_normal_is_the_negated_inwards_normal() {
        let l = SvgLine::new(pt(3.0, -4.0), pt(-7.0, 11.0));
        let i = l.inwards_normal().expect("non-degenerate line has a normal");
        let o = l.outwards_normal().expect("non-degenerate line has a normal");
        assert!((i.x + o.x).abs() < 1e-6);
        assert!((i.y + o.y).abs() < 1e-6);
    }
    #[test]
    fn svgline_normals_are_none_for_zero_length_line() {
        // division by a zero edge length must not produce a bogus point
        let l = SvgLine::new(pt(5.0, 5.0), pt(5.0, 5.0));
        assert_eq!(l.inwards_normal(), None);
        assert_eq!(l.outwards_normal(), None);
    }
    #[test]
    fn svgline_normals_are_none_for_nan_and_infinite_coords() {
        let nan = SvgLine::new(pt(f32::NAN, 0.0), pt(1.0, 1.0));
        assert_eq!(nan.inwards_normal(), None);
        assert_eq!(nan.outwards_normal(), None);
        // dy/hypot == inf/inf == NaN -> not finite -> None
        let inf = SvgLine::new(pt(f32::NEG_INFINITY, f32::NEG_INFINITY), pt(1.0, 1.0));
        assert_eq!(inf.inwards_normal(), None);
        assert_eq!(inf.outwards_normal(), None);
    }
    #[test]
    fn svgline_inwards_normal_on_overflowing_line_stays_defined() {
        // dx/dy overflow f32 -> hypot is +inf; the result must be either None
        // or finite, never a silent NaN/inf leaking into the point.
        let l = SvgLine::new(pt(-f32::MAX, -f32::MAX), pt(f32::MAX, f32::MAX));
        match l.inwards_normal() {
            None => {}
            Some(n) => assert!(
                n.x.is_finite() && n.y.is_finite(),
                "inwards_normal returned a non-finite point: {n:?}"
            ),
        }
    }
    // ---------------------------------------------------- SvgLine :: numerics
    #[test]
    fn svgline_get_x_y_at_t_hit_the_endpoints_exactly() {
        let l = SvgLine::new(pt(2.0, -3.0), pt(12.0, 17.0));
        assert_eq!(l.get_x_at_t(0.0), 2.0);
        assert_eq!(l.get_y_at_t(0.0), -3.0);
        assert_eq!(l.get_x_at_t(1.0), 12.0);
        assert_eq!(l.get_y_at_t(1.0), 17.0);
        assert!((l.get_x_at_t(0.5) - 7.0).abs() < 1e-9);
        assert!((l.get_y_at_t(0.5) - 7.0).abs() < 1e-9);
    }
    #[test]
    fn svgline_get_x_at_t_extrapolates_for_out_of_range_t() {
        // t is NOT clamped to [0, 1] - document the extrapolating behaviour
        let l = SvgLine::new(pt(0.0, 0.0), pt(10.0, 10.0));
        assert!((l.get_x_at_t(-1.0) - -10.0).abs() < 1e-9);
        assert!((l.get_y_at_t(2.0) - 20.0).abs() < 1e-9);
    }
    #[test]
    fn svgline_get_x_y_at_t_nan_and_inf_do_not_panic() {
        let l = SvgLine::new(pt(0.0, 0.0), pt(10.0, 10.0));
        assert!(l.get_x_at_t(f64::NAN).is_nan());
        assert!(l.get_y_at_t(f64::NAN).is_nan());
        assert!(l.get_x_at_t(f64::INFINITY).is_infinite());
        assert!(l.get_y_at_t(f64::NEG_INFINITY).is_infinite());
        // 0-length in x: (end.x - start.x) == 0, so 0 * inf == NaN
        let vertical = SvgLine::new(pt(4.0, 0.0), pt(4.0, 10.0));
        assert!(vertical.get_x_at_t(f64::INFINITY).is_nan());
    }
    #[test]
    fn svgline_get_x_at_t_at_f32_extremes_saturates_to_inf_not_panic() {
        let l = SvgLine::new(pt(-f32::MAX, 0.0), pt(f32::MAX, 0.0));
        // f64 has the range to hold 2 * f32::MAX, so this must stay finite
        assert!(l.get_x_at_t(0.5).abs() < 1e-9);
        assert!(l.get_x_at_t(1.0).is_finite());
        assert!(l.get_x_at_t(f64::MAX).is_infinite());
    }
    #[test]
    fn svgline_get_length_is_euclidean_and_direction_independent() {
        let l = SvgLine::new(pt(0.0, 0.0), pt(3.0, 4.0));
        assert!((l.get_length() - 5.0).abs() < 1e-6);
        let mut r = l;
        r.reverse();
        assert!((r.get_length() - l.get_length()).abs() < 1e-9);
        assert_eq!(SvgLine::new(pt(1.0, 1.0), pt(1.0, 1.0)).get_length(), 0.0);
    }
    #[test]
    fn svgline_get_length_overflow_and_nan_are_defined() {
        // dx overflows f32 -> +inf, hypot(+inf, 0) == +inf
        let huge = SvgLine::new(pt(-f32::MAX, 0.0), pt(f32::MAX, 0.0));
        let len = huge.get_length();
        assert!(len.is_infinite() && len > 0.0);
        let nan = SvgLine::new(pt(f32::NAN, 0.0), pt(0.0, 0.0));
        assert!(nan.get_length().is_nan());
    }
    #[test]
    fn svgline_get_t_at_offset_maps_arc_length_to_t() {
        let l = SvgLine::new(pt(0.0, 0.0), pt(10.0, 0.0));
        assert_eq!(l.get_t_at_offset(0.0), 0.0);
        assert!((l.get_t_at_offset(5.0) - 0.5).abs() < 1e-6);
        assert!((l.get_t_at_offset(10.0) - 1.0).abs() < 1e-6);
        // negative + past-the-end offsets are NOT clamped
        assert!((l.get_t_at_offset(-5.0) + 0.5).abs() < 1e-6);
        assert!((l.get_t_at_offset(20.0) - 2.0).abs() < 1e-6);
    }
    #[test]
    fn svgline_get_t_at_offset_on_zero_length_line_is_nan_or_inf_not_a_panic() {
        // offset / 0.0 -- must not panic; 0/0 is NaN, x/0 is +-inf
        let l = SvgLine::new(pt(1.0, 1.0), pt(1.0, 1.0));
        assert!(l.get_t_at_offset(0.0).is_nan());
        assert!(l.get_t_at_offset(5.0).is_infinite());
        assert!(l.get_t_at_offset(-5.0).is_infinite());
        assert!(l.get_t_at_offset(f64::NAN).is_nan());
    }
    #[test]
    fn svgline_get_t_at_offset_round_trips_through_get_x_at_t() {
        let l = SvgLine::new(pt(2.0, 2.0), pt(2.0, 12.0));
        let t = l.get_t_at_offset(l.get_length());
        assert!((l.get_y_at_t(t) - 12.0).abs() < 1e-6);
        assert!((l.get_x_at_t(t) - 2.0).abs() < 1e-6);
    }
    #[test]
    fn svgline_tangent_vector_is_normalized_and_zero_for_degenerate_lines() {
        let l = SvgLine::new(pt(0.0, 0.0), pt(0.0, 5.0));
        let t = l.get_tangent_vector_at_t();
        assert!((t.x - 0.0).abs() < 1e-9);
        assert!((t.y - 1.0).abs() < 1e-9);
        // normalize() defines the zero-length case as the zero vector
        let degenerate = SvgLine::new(pt(3.0, 3.0), pt(3.0, 3.0));
        let t = degenerate.get_tangent_vector_at_t();
        assert_eq!(t, SvgVector { x: 0.0, y: 0.0 });
    }
    // ------------------------------------------------------ SvgLine :: bounds
    #[test]
    fn svgline_get_bounds_is_orientation_independent() {
        let l = SvgLine::new(pt(10.0, 20.0), pt(-5.0, 4.0));
        let mut r = l;
        r.reverse();
        assert_eq!(l.get_bounds(), r.get_bounds());
        let b = l.get_bounds();
        assert_eq!(b.x, -5.0);
        assert_eq!(b.y, 4.0);
        assert_eq!(b.width, 15.0);
        assert_eq!(b.height, 16.0);
        assert_eq!(b.radius_top_left, 0.0);
    }
    #[test]
    fn svgline_get_bounds_of_degenerate_line_is_zero_sized() {
        let b = SvgLine::new(pt(7.0, 8.0), pt(7.0, 8.0)).get_bounds();
        assert_eq!((b.x, b.y, b.width, b.height), (7.0, 8.0, 0.0, 0.0));
    }
    #[test]
    fn svgline_get_bounds_overflows_to_inf_width_without_panicking() {
        // max_x - min_x overflows f32 -> +inf rather than a wrapped/negative width
        let b = SvgLine::new(pt(-f32::MAX, 0.0), pt(f32::MAX, 1.0)).get_bounds();
        assert!(b.width.is_infinite() && b.width > 0.0);
        assert_eq!(b.x, -f32::MAX);
        assert_eq!(b.height, 1.0);
    }
    // ------------------------------------------------- SvgPathElement :: ctors
    #[test]
    fn svgpathelement_constructors_wrap_the_right_variant() {
        let l = SvgLine::new(pt(0.0, 0.0), pt(1.0, 1.0));
        assert!(matches!(SvgPathElement::line(l), SvgPathElement::Line(_)));
        assert!(matches!(
            SvgPathElement::quadratic_curve(quad()),
            SvgPathElement::QuadraticCurve(_)
        ));
        assert!(matches!(
            SvgPathElement::cubic_curve(cubic()),
            SvgPathElement::CubicCurve(_)
        ));
    }
    #[test]
    fn svgpathelement_constructors_accept_extreme_geometry() {
        let l = SvgLine::new(pt(f32::NAN, f32::INFINITY), pt(f32::MIN, f32::MAX));
        let el = SvgPathElement::line(l);
        assert!(el.get_start().x.is_nan());
        assert_eq!(el.get_end().x, f32::MIN);
    }
    // ------------------------------------------- SvgPathElement :: set / get
    #[test]
    fn svgpathelement_set_first_last_round_trip_for_every_variant() {
        let a = pt(-1.0, -2.0);
        let b = pt(3.0, 4.0);
        for mut el in [
            SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(1.0, 1.0))),
            SvgPathElement::quadratic_curve(quad()),
            SvgPathElement::cubic_curve(cubic()),
        ] {
            el.set_first(a);
            el.set_last(b);
            assert_eq!(el.get_start(), a);
            assert_eq!(el.get_end(), b);
        }
    }
    #[test]
    fn svgpathelement_set_first_last_accept_zero_min_max_and_nan() {
        let mut el = SvgPathElement::cubic_curve(cubic());
        el.set_first(pt(0.0, 0.0));
        el.set_last(pt(0.0, 0.0));
        assert_eq!(el.get_start(), pt(0.0, 0.0));
        assert_eq!(el.get_end(), pt(0.0, 0.0));
        el.set_first(pt(f32::MIN, f32::MIN));
        el.set_last(pt(f32::MAX, f32::MAX));
        assert_eq!(el.get_start().x, f32::MIN);
        assert_eq!(el.get_end().x, f32::MAX);
        el.set_first(pt(f32::NAN, f32::NEG_INFINITY));
        assert!(el.get_start().x.is_nan());
        assert!(el.get_start().y.is_infinite());
    }
    #[test]
    fn svgpathelement_reverse_is_an_involution_for_every_variant() {
        for el in [
            SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(1.0, 1.0))),
            SvgPathElement::quadratic_curve(quad()),
            SvgPathElement::cubic_curve(cubic()),
        ] {
            let mut r = el;
            r.reverse();
            assert_eq!(r.get_start(), el.get_end());
            assert_eq!(r.get_end(), el.get_start());
            r.reverse();
            assert_eq!(r, el, "reverse() applied twice must be the identity");
        }
    }
    // --------------------------------------------- SvgPathElement :: numerics
    #[test]
    fn svgpathelement_get_length_is_non_negative_for_every_variant() {
        for el in [
            SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(3.0, 4.0))),
            SvgPathElement::quadratic_curve(quad()),
            SvgPathElement::cubic_curve(cubic()),
        ] {
            let len = el.get_length();
            assert!(len.is_finite() && len >= 0.0, "bad length: {len}");
        }
    }
    #[test]
    fn svgpathelement_get_x_y_at_t_hit_endpoints_for_every_variant() {
        for el in [
            SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(10.0, 0.0))),
            SvgPathElement::quadratic_curve(quad()),
            SvgPathElement::cubic_curve(cubic()),
        ] {
            assert!((el.get_x_at_t(0.0) - f64::from(el.get_start().x)).abs() < 1e-6);
            assert!((el.get_y_at_t(0.0) - f64::from(el.get_start().y)).abs() < 1e-6);
            assert!((el.get_x_at_t(1.0) - f64::from(el.get_end().x)).abs() < 1e-6);
            assert!((el.get_y_at_t(1.0) - f64::from(el.get_end().y)).abs() < 1e-6);
        }
    }
    #[test]
    fn svgpathelement_get_x_y_at_t_nan_inf_do_not_panic() {
        for el in [
            SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(10.0, 0.0))),
            SvgPathElement::quadratic_curve(quad()),
            SvgPathElement::cubic_curve(cubic()),
        ] {
            assert!(el.get_x_at_t(f64::NAN).is_nan());
            assert!(el.get_y_at_t(f64::NAN).is_nan());
            // +-inf must not panic; any non-panicking value is acceptable here
            let _ = el.get_x_at_t(f64::INFINITY);
            let _ = el.get_y_at_t(f64::NEG_INFINITY);
            let _ = el.get_x_at_t(f64::MIN);
            let _ = el.get_y_at_t(f64::MAX);
        }
    }
    #[test]
    fn svgpathelement_line_tangent_ignores_t_even_when_t_is_nan() {
        // SvgLine has a constant tangent, so the `t` argument is discarded.
        let el = SvgPathElement::line(SvgLine::new(pt(0.0, 0.0), pt(0.0, 8.0)));
        let at_half = el.get_tangent_vector_at_t(0.5);
        assert_eq!(el.get_tangent_vector_at_t(f64::NAN), at_half);
        assert_eq!(el.get_tangent_vector_at_t(f64::INFINITY), at_half);
        assert_eq!(at_half, SvgVector { x: 0.0, y: 1.0 });
    }
    #[test]
    fn svgpathelement_curve_tangent_at_nan_is_nan_not_a_panic() {
        let el = SvgPathElement::quadratic_curve(quad());
        let v = el.get_tangent_vector_at_t(f64::NAN);
        assert!(v.x.is_nan() && v.y.is_nan());
    }
    #[test]
    fn svgpathelement_curve_tangent_is_unit_length_in_range() {
        for el in [
            SvgPathElement::quadratic_curve(quad()),
            SvgPathElement::cubic_curve(cubic()),
        ] {
            for t in [0.0_f64, 0.25, 0.5, 0.75, 1.0] {
                let v = el.get_tangent_vector_at_t(t);
                let len = (v.x * v.x + v.y * v.y).sqrt();
                // either the zero vector (degenerate derivative) or unit length
                assert!(
                    len.abs() < 1e-9 || (len - 1.0).abs() < 1e-6,
                    "tangent at t={t} has length {len}"
                );
            }
        }
    }
    #[test]
    fn svgpathelement_curve_t_at_offset_saturates_at_1_past_the_end() {
        // the sampling loop never triggers for an out-of-range offset,
        // so it must fall through to the final t (== 1.0), not overshoot
        for el in [
            SvgPathElement::quadratic_curve(quad()),
            SvgPathElement::cubic_curve(cubic()),
        ] {
            assert!((el.get_t_at_offset(f64::MAX) - 1.0).abs() < 1e-9);
            assert!((el.get_t_at_offset(1.0e30) - 1.0).abs() < 1e-9);
            // NaN never compares greater, so it also falls through
            assert!((el.get_t_at_offset(f64::NAN) - 1.0).abs() < 1e-9);
        }
    }
    #[test]
    fn svgpathelement_curve_t_at_offset_is_monotonic_and_in_range() {
        let el = SvgPathElement::cubic_curve(cubic());
        let len = el.get_length();
        let t_quarter = el.get_t_at_offset(len * 0.25);
        let t_half = el.get_t_at_offset(len * 0.5);
        assert!(t_quarter.is_finite() && t_half.is_finite());
        assert!((0.0..=1.0).contains(&t_quarter), "t={t_quarter}");
        assert!((0.0..=1.0).contains(&t_half), "t={t_half}");
        assert!(t_quarter <= t_half, "t must not decrease with offset");
    }
    #[test]
    fn svgpathelement_curve_t_at_offset_negative_offset_is_non_positive() {
        let el = SvgPathElement::cubic_curve(cubic());
        let t = el.get_t_at_offset(-10.0);
        assert!(t.is_finite(), "negative offset produced {t}");
        assert!(t <= 0.0, "negative offset must not map to a forward t: {t}");
    }
    #[test]
    fn svgpathelement_get_bounds_contains_both_endpoints() {
        for el in [
            SvgPathElement::line(SvgLine::new(pt(-3.0, 2.0), pt(9.0, -4.0))),
            SvgPathElement::quadratic_curve(quad()),
            SvgPathElement::cubic_curve(cubic()),
        ] {
            let b = el.get_bounds();
            let (s, e) = (el.get_start(), el.get_end());
            assert!(b.width >= 0.0 && b.height >= 0.0);
            assert!(s.x >= b.x && s.x <= b.x + b.width);
            assert!(e.x >= b.x && e.x <= b.x + b.width);
            assert!(s.y >= b.y && s.y <= b.y + b.height);
            assert!(e.y >= b.y && e.y <= b.y + b.height);
        }
    }
    // -------------------------------------------------------------- SvgPath
    #[test]
    fn svgpath_empty_getters_are_none_and_bounds_are_default() {
        let p = make_path(Vec::new());
        assert_eq!(p.get_start(), None);
        assert_eq!(p.get_end(), None);
        assert!(!p.is_closed(), "an empty path is not closed");
        assert_eq!(p.get_bounds(), SvgRect::default());
    }
    #[test]
    fn svgpath_start_and_end_come_from_first_and_last_element() {
        let p = make_path(vec![
            line_el(0.0, 0.0, 1.0, 1.0),
            line_el(1.0, 1.0, 5.0, 5.0),
            line_el(5.0, 5.0, 9.0, 2.0),
        ]);
        assert_eq!(p.get_start(), Some(pt(0.0, 0.0)));
        assert_eq!(p.get_end(), Some(pt(9.0, 2.0)));
    }
    #[test]
    fn svgpath_close_makes_is_closed_true_and_is_idempotent() {
        let mut p = make_path(vec![
            line_el(0.0, 0.0, 10.0, 0.0),
            line_el(10.0, 0.0, 10.0, 10.0),
        ]);
        assert!(!p.is_closed());
        p.close();
        assert!(p.is_closed(), "close() must establish is_closed()");
        assert_eq!(p.items.len(), 3);
        assert_eq!(p.get_end(), p.get_start());
        // closing an already-closed path must not append anything
        p.close();
        assert_eq!(p.items.len(), 3);
    }
    #[test]
    fn svgpath_close_on_empty_path_is_a_noop() {
        let mut p = make_path(Vec::new());
        p.close();
        assert_eq!(p.items.len(), 0);
        assert!(!p.is_closed());
    }
    #[test]
    fn svgpath_close_with_nan_coords_appends_once_and_does_not_panic() {
        // NaN != NaN, so the "already closed?" check can never be satisfied.
        // close() must still terminate and append exactly one element.
        let mut p = make_path(vec![line_el(f32::NAN, 0.0, 10.0, 0.0)]);
        p.close();
        assert_eq!(p.items.len(), 2);
        assert!(!p.is_closed(), "a NaN start point can never compare equal");
    }
    #[test]
    fn svgpath_is_closed_for_single_degenerate_element() {
        let p = make_path(vec![line_el(4.0, 4.0, 4.0, 4.0)]);
        assert!(p.is_closed(), "start == end for the only element");
        let open = make_path(vec![line_el(4.0, 4.0, 5.0, 4.0)]);
        assert!(!open.is_closed());
    }
    #[test]
    fn svgpath_reverse_is_an_involution_and_swaps_endpoints() {
        let orig = make_path(vec![
            line_el(0.0, 0.0, 1.0, 1.0),
            SvgPathElement::cubic_curve(cubic()),
            line_el(10.0, 0.0, 20.0, 5.0),
        ]);
        let mut p = orig.clone();
        p.reverse();
        assert_eq!(p.get_start(), orig.get_end());
        assert_eq!(p.get_end(), orig.get_start());
        assert_eq!(p.items.len(), orig.items.len());
        p.reverse();
        assert_eq!(p, orig, "reverse() applied twice must be the identity");
    }
    #[test]
    fn svgpath_reverse_on_empty_path_does_not_panic() {
        let mut p = make_path(Vec::new());
        p.reverse();
        assert_eq!(p.items.len(), 0);
    }
    #[test]
    fn svgpath_join_with_interpolates_the_join_point() {
        let mut a = make_path(vec![line_el(0.0, 0.0, 10.0, 0.0)]);
        let b = make_path(vec![line_el(20.0, 10.0, 30.0, 10.0)]);
        assert_eq!(a.join_with(b), Some(()));
        assert_eq!(a.items.len(), 2);
        // join point is the midpoint of (10,0) and (20,10)
        let mid = pt(15.0, 5.0);
        assert_eq!(a.items.as_ref()[0].get_end(), mid);
        assert_eq!(a.items.as_ref()[1].get_start(), mid);
        assert_eq!(a.get_start(), Some(pt(0.0, 0.0)));
        assert_eq!(a.get_end(), Some(pt(30.0, 10.0)));
    }
    #[test]
    fn svgpath_join_with_empty_other_returns_none_and_leaves_self_intact() {
        let mut a = make_path(vec![line_el(0.0, 0.0, 10.0, 0.0)]);
        let before = a.clone();
        assert_eq!(a.join_with(make_path(Vec::new())), None);
        assert_eq!(a, before, "a failed join must not corrupt the receiver");
    }
    #[test]
    fn svgpath_join_with_on_empty_self_returns_none_without_underflow() {
        // `vec.len() - 1` would underflow on an empty receiver; the `?` on
        // `last()` must short-circuit first.
        let mut a = make_path(Vec::new());
        let b = make_path(vec![line_el(0.0, 0.0, 1.0, 1.0)]);
        assert_eq!(a.join_with(b), None);
        assert_eq!(a.items.len(), 0);
    }
    #[test]
    fn svgpath_join_with_extreme_coords_does_not_panic() {
        let mut a = make_path(vec![line_el(0.0, 0.0, f32::MAX, f32::MAX)]);
        let b = make_path(vec![line_el(-f32::MAX, -f32::MAX, 0.0, 0.0)]);
        assert_eq!(a.join_with(b), Some(()));
        // midpoint of MAX and -MAX must not overflow to inf
        let join = a.items.as_ref()[0].get_end();
        assert!(join.x.is_finite() && join.y.is_finite(), "join: {join:?}");
    }
    #[test]
    fn svgpath_get_bounds_unions_every_element() {
        let p = make_path(vec![
            line_el(0.0, 0.0, 10.0, 0.0),
            line_el(10.0, 0.0, 10.0, 20.0),
            line_el(10.0, 20.0, -5.0, -8.0),
        ]);
        let b = p.get_bounds();
        assert_eq!(b.x, -5.0);
        assert_eq!(b.y, -8.0);
        assert_eq!(b.width, 15.0);
        assert_eq!(b.height, 28.0);
        for el in p.items.as_ref() {
            assert!(
                rect_contains(&b, &el.get_bounds()),
                "path bounds must contain every element's bounds"
            );
        }
    }
    // ------------------------------------------------------ SvgMultiPolygon
    #[test]
    fn svgmultipolygon_empty_has_default_bounds() {
        let mp = SvgMultiPolygon::create(SvgPathVec::from_vec(Vec::new()));
        assert_eq!(mp.get_bounds(), SvgRect::default());
    }
    #[test]
    fn svgmultipolygon_bounds_union_all_rings() {
        let mp = SvgMultiPolygon::create(SvgPathVec::from_vec(vec![
            make_path(vec![line_el(0.0, 0.0, 10.0, 10.0)]),
            make_path(vec![line_el(-4.0, 30.0, 2.0, 33.0)]),
        ]));
        let b = mp.get_bounds();
        assert_eq!(b.x, -4.0);
        assert_eq!(b.y, 0.0);
        assert_eq!(b.width, 14.0);
        assert_eq!(b.height, 33.0);
    }
    #[test]
    fn svgmultipolygon_bounds_must_contain_geometry_after_an_empty_first_ring() {
        // BUG: get_bounds() seeds from rings[0].items[0]; when the FIRST ring is
        // empty it bails out to SvgRect::default() and silently drops every
        // later ring's geometry.
        let mp = SvgMultiPolygon::create(SvgPathVec::from_vec(vec![
            make_path(Vec::new()),
            make_path(vec![line_el(100.0, 100.0, 200.0, 200.0)]),
        ]));
        let b = mp.get_bounds();
        let expected = SvgRect {
            width: 100.0,
            height: 100.0,
            x: 100.0,
            y: 100.0,
            ..SvgRect::default()
        };
        assert!(
            rect_contains(&b, &expected),
            "bounds {b:?} must contain the geometry of the non-empty ring {expected:?}"
        );
    }
    // ------------------------------------------------------- SvgSimpleNode
    #[test]
    fn svgsimplenode_is_closed_per_variant() {
        let circle = SvgCircle {
            center_x: 0.0,
            center_y: 0.0,
            radius: 5.0,
        };
        assert!(SvgSimpleNode::Circle(circle).is_closed());
        assert!(SvgSimpleNode::CircleHole(circle).is_closed());
        assert!(SvgSimpleNode::Rect(SvgRect::default()).is_closed());
        assert!(SvgSimpleNode::RectHole(SvgRect::default()).is_closed());
        assert!(!SvgSimpleNode::Path(make_path(vec![line_el(0.0, 0.0, 1.0, 0.0)])).is_closed());
        assert!(SvgSimpleNode::Path(make_path(vec![line_el(2.0, 2.0, 2.0, 2.0)])).is_closed());
        // an empty path is not a closed shape
        assert!(!SvgSimpleNode::Path(make_path(Vec::new())).is_closed());
    }
    #[test]
    fn svgsimplenode_get_bounds_per_variant() {
        let circle = SvgCircle {
            center_x: 10.0,
            center_y: 10.0,
            radius: 2.0,
        };
        let b = SvgSimpleNode::Circle(circle).get_bounds();
        assert_eq!((b.x, b.y, b.width, b.height), (8.0, 8.0, 4.0, 4.0));
        assert_eq!(SvgSimpleNode::CircleHole(circle).get_bounds(), b);
        let rect = SvgRect {
            width: 3.0,
            height: 4.0,
            x: 1.0,
            y: 2.0,
            ..SvgRect::default()
        };
        assert_eq!(SvgSimpleNode::Rect(rect).get_bounds(), rect);
        assert_eq!(SvgSimpleNode::RectHole(rect).get_bounds(), rect);
        // empty path -> default bounds, no panic
        assert_eq!(
            SvgSimpleNode::Path(make_path(Vec::new())).get_bounds(),
            SvgRect::default()
        );
    }
    // ------------------------------------------------------------- SvgNode
    #[test]
    fn svgnode_get_bounds_of_empty_collections_is_default() {
        assert_eq!(
            SvgNode::MultiPolygonCollection(SvgMultiPolygonVec::from_vec(Vec::new())).get_bounds(),
            SvgRect::default()
        );
        assert_eq!(
            SvgNode::MultiShape(SvgSimpleNodeVec::from_vec(Vec::new())).get_bounds(),
            SvgRect::default()
        );
        assert_eq!(
            SvgNode::Path(make_path(Vec::new())).get_bounds(),
            SvgRect::default()
        );
        assert_eq!(
            SvgNode::MultiPolygon(SvgMultiPolygon::create(SvgPathVec::from_vec(Vec::new())))
                .get_bounds(),
            SvgRect::default()
        );
    }
    #[test]
    fn svgnode_is_closed_is_vacuously_true_for_empty_collections() {
        assert!(SvgNode::MultiPolygonCollection(SvgMultiPolygonVec::from_vec(Vec::new())).is_closed());
        assert!(SvgNode::MultiShape(SvgSimpleNodeVec::from_vec(Vec::new())).is_closed());
        assert!(
            SvgNode::MultiPolygon(SvgMultiPolygon::create(SvgPathVec::from_vec(Vec::new())))
                .is_closed()
        );
        // ... but an empty *path* is still open
        assert!(!SvgNode::Path(make_path(Vec::new())).is_closed());
    }
    #[test]
    fn svgnode_is_closed_false_when_any_subpath_is_open() {
        let open = make_path(vec![line_el(0.0, 0.0, 1.0, 0.0)]);
        let mut closed = make_path(vec![
            line_el(0.0, 0.0, 1.0, 0.0),
            line_el(1.0, 0.0, 1.0, 1.0),
        ]);
        closed.close();
        let mp = SvgMultiPolygon::create(SvgPathVec::from_vec(vec![closed.clone(), open.clone()]));
        assert!(!SvgNode::MultiPolygon(mp.clone()).is_closed());
        assert!(!SvgNode::MultiPolygonCollection(SvgMultiPolygonVec::from_vec(vec![mp])).is_closed());
        assert!(!SvgNode::MultiShape(SvgSimpleNodeVec::from_vec(vec![
            SvgSimpleNode::Path(open),
            SvgSimpleNode::Circle(SvgCircle {
                center_x: 0.0,
                center_y: 0.0,
                radius: 1.0,
            }),
        ]))
        .is_closed());
        let all_closed = SvgMultiPolygon::create(SvgPathVec::from_vec(vec![closed]));
        assert!(SvgNode::MultiPolygon(all_closed).is_closed());
    }
    #[test]
    fn svgnode_get_bounds_contains_all_children() {
        let a = make_path(vec![line_el(0.0, 0.0, 10.0, 10.0)]);
        let b = make_path(vec![line_el(100.0, 100.0, 200.0, 200.0)]);
        let node = SvgNode::MultiShape(SvgSimpleNodeVec::from_vec(vec![
            SvgSimpleNode::Path(a.clone()),
            SvgSimpleNode::Path(b.clone()),
        ]));
        let bounds = node.get_bounds();
        assert!(rect_contains(&bounds, &a.get_bounds()));
        assert!(rect_contains(&bounds, &b.get_bounds()));
    }
    #[test]
    fn svgnode_rect_and_circle_bounds_are_passthrough() {
        let rect = SvgRect {
            width: 5.0,
            height: 6.0,
            x: -1.0,
            y: -2.0,
            ..SvgRect::default()
        };
        assert_eq!(SvgNode::Rect(rect).get_bounds(), rect);
        assert!(SvgNode::Rect(rect).is_closed());
        let circle = SvgCircle {
            center_x: 0.0,
            center_y: 0.0,
            radius: 1.0,
        };
        assert_eq!(SvgNode::Circle(circle).get_bounds(), circle.get_bounds());
        assert!(SvgNode::Circle(circle).is_closed());
    }
    // ----------------------------------------------------------- SvgCircle
    #[test]
    fn svgcircle_contains_point_is_strict_and_correct() {
        let c = SvgCircle {
            center_x: 0.0,
            center_y: 0.0,
            radius: 1.0,
        };
        assert!(c.contains_point(0.0, 0.0));
        assert!(c.contains_point(0.5, 0.5));
        // exactly on the perimeter -> NOT contained (strict `<`)
        assert!(!c.contains_point(1.0, 0.0));
        assert!(!c.contains_point(0.0, -1.0));
        assert!(!c.contains_point(2.0, 0.0));
        assert!(!c.contains_point(-0.8, -0.8));
    }
    #[test]
    fn svgcircle_zero_radius_contains_nothing_not_even_its_center() {
        let c = SvgCircle {
            center_x: 3.0,
            center_y: 3.0,
            radius: 0.0,
        };
        assert!(!c.contains_point(3.0, 3.0));
        assert!(!c.contains_point(0.0, 0.0));
    }
    #[test]
    fn svgcircle_negative_radius_behaves_like_its_absolute_value() {
        // r*r discards the sign, so a negative radius is NOT treated as empty.
        // Documented here so a future fix has to update this test deliberately.
        let neg = SvgCircle {
            center_x: 0.0,
            center_y: 0.0,
            radius: -2.0,
        };
        let pos = SvgCircle {
            center_x: 0.0,
            center_y: 0.0,
            radius: 2.0,
        };
        assert_eq!(neg.contains_point(0.0, 0.0), pos.contains_point(0.0, 0.0));
        assert_eq!(neg.contains_point(1.9, 0.0), pos.contains_point(1.9, 0.0));
        assert_eq!(neg.contains_point(5.0, 0.0), pos.contains_point(5.0, 0.0));
    }
    #[test]
    fn svgcircle_contains_point_nan_and_inf_are_false_not_a_panic() {
        let c = SvgCircle {
            center_x: 0.0,
            center_y: 0.0,
            radius: 1.0,
        };
        // every comparison against NaN is false
        assert!(!c.contains_point(f32::NAN, 0.0));
        assert!(!c.contains_point(0.0, f32::NAN));
        assert!(!c.contains_point(f32::INFINITY, 0.0));
        assert!(!c.contains_point(f32::NEG_INFINITY, f32::NEG_INFINITY));
        let nan_r = SvgCircle {
            center_x: 0.0,
            center_y: 0.0,
            radius: f32::NAN,
        };
        assert!(!nan_r.contains_point(0.0, 0.0));
    }
    #[test]
    fn svgcircle_contains_point_at_f32_extremes_does_not_panic() {
        let c = SvgCircle {
            center_x: 0.0,
            center_y: 0.0,
            radius: f32::MAX,
        };
        // x_diff*x_diff overflows to +inf, which is never < radius^2 (+inf)
        assert!(!c.contains_point(f32::MAX, f32::MAX));
        assert!(c.contains_point(1.0, 1.0));
        assert!(!c.contains_point(f32::MIN, 0.0));
    }
    #[test]
    fn svgcircle_get_bounds_is_the_enclosing_square() {
        let c = SvgCircle {
            center_x: 5.0,
            center_y: -5.0,
            radius: 2.5,
        };
        let b = c.get_bounds();
        assert_eq!((b.x, b.y, b.width, b.height), (2.5, -7.5, 5.0, 5.0));
        // A bbox CORNER is outside the inscribed circle. (Mid-edge is not: the
        // previous probe sat on the horizontal diameter, 2.4 from the centre, i.e.
        // strictly inside the radius-2.5 circle.)
        assert!(!c.contains_point(b.x + 0.1, b.y + 0.1));
        assert!(c.contains_point(c.center_x, c.center_y));
    }
    #[test]
    fn svgcircle_get_bounds_with_nan_and_huge_radius_does_not_panic() {
        let nan = SvgCircle {
            center_x: 0.0,
            center_y: 0.0,
            radius: f32::NAN,
        };
        let b = nan.get_bounds();
        assert!(b.width.is_nan() && b.x.is_nan());
        let huge = SvgCircle {
            center_x: 0.0,
            center_y: 0.0,
            radius: f32::MAX,
        };
        let b = huge.get_bounds();
        // radius * 2.0 overflows f32 -> +inf (saturates, does not wrap negative)
        assert!(b.width.is_infinite() && b.width > 0.0);
        assert_eq!(b.x, -f32::MAX);
    }
    // ------------------------------------------------ Tessellated* wrappers
    #[test]
    fn tessellated_svg_node_empty_is_a_neutral_value() {
        let e = TessellatedSvgNode::empty();
        assert!(e.vertices.as_ref().is_empty());
        assert!(e.indices.as_ref().is_empty());
        assert_eq!(e, TessellatedSvgNode::default());
    }
    #[test]
    fn tessellated_colored_svg_node_empty_is_a_neutral_value() {
        let e = TessellatedColoredSvgNode::empty();
        assert!(e.vertices.as_ref().is_empty());
        assert!(e.indices.as_ref().is_empty());
        assert_eq!(e, TessellatedColoredSvgNode::default());
    }
    #[test]
    fn tessellated_svg_node_vec_ref_round_trips_through_as_slice() {
        let node = TessellatedSvgNode {
            vertices: vec![SvgVertex { x: 1.0, y: 2.0 }].into(),
            indices: vec![0_u32].into(),
        };
        let v = TessellatedSvgNodeVec::from_vec(vec![node.clone(), TessellatedSvgNode::empty()]);
        let r = v.get_ref();
        assert_eq!(r.len, 2);
        let slice = r.as_slice();
        assert_eq!(slice.len(), 2);
        assert_eq!(slice[0], node);
        assert_eq!(slice[1], TessellatedSvgNode::empty());
    }
    #[test]
    fn tessellated_svg_node_vec_ref_on_empty_vec_yields_an_empty_slice() {
        // as_slice() calls slice::from_raw_parts - an empty vec must still
        // produce a valid (dangling but aligned) pointer, never a null deref.
        let v = TessellatedSvgNodeVec::from_vec(Vec::new());
        let r = v.get_ref();
        assert_eq!(r.len, 0);
        assert!(r.as_slice().is_empty());
        assert!(!r.ptr.is_null(), "raw ptr must never be null");
    }
    #[test]
    fn tessellated_colored_svg_node_vec_ref_round_trips_through_as_slice() {
        let node = TessellatedColoredSvgNode {
            vertices: vec![SvgColoredVertex {
                x: 1.0,
                y: 2.0,
                z: 3.0,
                r: 1.0,
                g: 0.5,
                b: 0.25,
                a: 1.0,
            }]
            .into(),
            indices: vec![0_u32, 1, 2].into(),
        };
        let v = TessellatedColoredSvgNodeVec::from_vec(vec![node.clone()]);
        let r = v.get_ref();
        assert_eq!(r.len, 1);
        assert_eq!(r.as_slice().len(), 1);
        assert_eq!(r.as_slice()[0], node);
    }
    #[test]
    fn tessellated_colored_svg_node_vec_ref_on_empty_vec_yields_an_empty_slice() {
        let v = TessellatedColoredSvgNodeVec::from_vec(Vec::new());
        let r = v.get_ref();
        assert_eq!(r.len, 0);
        assert!(r.as_slice().is_empty());
        assert!(!r.ptr.is_null(), "raw ptr must never be null");
    }
    // ------------------------------------- compute_svg_transform_uniforms
    fn matrix_of(u: &Uniform) -> [f32; 16] {
        match u.uniform_type {
            UniformType::Matrix4 { transpose, matrix } => {
                assert!(!transpose, "SVG shaders expect a pre-transposed matrix");
                matrix
            }
            _ => panic!("expected a Matrix4 uniform"),
        }
    }
    fn bbox_of(u: &Uniform) -> [f32; 2] {
        match u.uniform_type {
            UniformType::FloatVec2(v) => v,
            _ => panic!("expected a FloatVec2 uniform"),
        }
    }
    #[test]
    fn compute_svg_transform_uniforms_zero_size_no_transforms_is_finite() {
        let (bbox, tf) = compute_svg_transform_uniforms(
            PhysicalSizeU32 {
                width: 0,
                height: 0,
            },
            &[],
        );
        assert_eq!(bbox.uniform_name.as_str(), "vBboxSize");
        assert_eq!(bbox_of(&bbox), [0.0, 0.0]);
        assert_eq!(tf.uniform_name.as_str(), "vTransformMatrix");
        let m = matrix_of(&tf);
        assert!(
            m.iter().all(|f| f.is_finite()),
            "a zero-sized target must not produce NaN/inf in the matrix: {m:?}"
        );
    }
    #[test]
    fn compute_svg_transform_uniforms_at_u32_max_does_not_panic() {
        let (bbox, tf) = compute_svg_transform_uniforms(
            PhysicalSizeU32 {
                width: u32::MAX,
                height: u32::MAX,
            },
            &[],
        );
        let b = bbox_of(&bbox);
        assert!(b[0].is_finite() && b[0] > 0.0);
        assert_eq!(b[0], u32::MAX as f32);
        assert_eq!(b[1], u32::MAX as f32);
        let m = matrix_of(&tf);
        assert!(m.iter().all(|f| f.is_finite()), "matrix: {m:?}");
    }
    #[test]
    fn compute_svg_transform_uniforms_translation_changes_the_matrix() {
        let size = PhysicalSizeU32 {
            width: 800,
            height: 600,
        };
        let identity = matrix_of(&compute_svg_transform_uniforms(size, &[]).1);
        let translated = matrix_of(
            &compute_svg_transform_uniforms(
                size,
                &[
                    StyleTransform::TranslateX(PixelValue::px(10.0)),
                    StyleTransform::TranslateY(PixelValue::px(-20.0)),
                ],
            )
            .1,
        );
        assert!(
            translated.iter().all(|f| f.is_finite()),
            "matrix: {translated:?}"
        );
        assert!(
            identity != translated,
            "a translation must actually alter the transform matrix"
        );
    }
    #[test]
    fn compute_svg_transform_uniforms_extreme_translation_does_not_panic() {
        let size = PhysicalSizeU32 {
            width: 1,
            height: 1,
        };
        let (bbox, tf) = compute_svg_transform_uniforms(
            size,
            &[
                StyleTransform::TranslateX(PixelValue::px(f32::MAX)),
                StyleTransform::TranslateY(PixelValue::px(-f32::MAX)),
            ],
        );
        assert_eq!(bbox_of(&bbox), [1.0, 1.0]);
        // no assertion on finiteness here: the transform itself is degenerate,
        // we only require that building the uniform does not panic
        let _ = matrix_of(&tf);
    }
    // ------------------------------------------------------------ SvgStyle
    #[test]
    fn svgstyle_getters_read_through_to_both_variants() {
        let fill = SvgStyle::Fill(SvgFillStyle::default());
        assert!(fill.get_antialias());
        assert!(!fill.get_high_quality_aa());
        assert_eq!(fill.get_transform(), SvgTransform::default());
        let stroke = SvgStyle::Stroke(SvgStrokeStyle::default());
        assert!(stroke.get_antialias());
        assert!(!stroke.get_high_quality_aa());
        assert_eq!(stroke.get_transform(), SvgTransform::default());
    }
    #[test]
    fn svgstyle_getters_reflect_non_default_fields() {
        let transform = SvgTransform {
            sx: 2.0,
            kx: 0.5,
            ky: -0.5,
            sy: 3.0,
            tx: 10.0,
            ty: -10.0,
        };
        let fill = SvgStyle::Fill(SvgFillStyle {
            anti_alias: false,
            high_quality_aa: true,
            transform,
            ..SvgFillStyle::default()
        });
        assert!(!fill.get_antialias());
        assert!(fill.get_high_quality_aa());
        assert_eq!(fill.get_transform(), transform);
        let stroke = SvgStyle::Stroke(SvgStrokeStyle {
            anti_alias: false,
            high_quality_aa: true,
            transform,
            ..SvgStrokeStyle::default()
        });
        assert!(!stroke.get_antialias());
        assert!(stroke.get_high_quality_aa());
        assert_eq!(stroke.get_transform(), transform);
    }
    // ------------------------------------------------------ SvgParseError
    #[test]
    fn svgparseerror_display_is_non_empty_for_every_variant() {
        let variants = [
            SvgParseError::NoParserAvailable,
            SvgParseError::ElementsLimitReached,
            SvgParseError::NotAnUtf8Str,
            SvgParseError::MalformedGZip,
            SvgParseError::InvalidSize,
            SvgParseError::ParsingFailed(XmlError::NoRootNode),
        ];
        for v in &variants {
            let s = v.to_string();
            assert!(!s.is_empty(), "empty Display output for {v:?}");
            assert!(
                !s.contains("\u{0}"),
                "Display output must not contain NUL bytes"
            );
        }
    }
    #[test]
    fn svgparseerror_display_of_parsing_failed_embeds_the_inner_error() {
        let inner = XmlError::NoRootNode;
        let s = SvgParseError::ParsingFailed(inner.clone()).to_string();
        assert!(s.starts_with("Error parsing SVG:"), "got: {s}");
        assert!(
            s.contains(&inner.to_string()),
            "outer message {s:?} must embed the inner XmlError message"
        );
    }
}