Skip to main content

cvkg_render_gpu/renderer/
svg.rs

1use crate::draw::{parse_svg_animations, usvg_to_lyon};
2use crate::renderer::GpuRenderer;
3use crate::types::{SvgAnimation, SvgModel, SvgPath};
4use crate::vertex::{CustomStrokeVertexConstructor, SceneVertexConstructor, Vertex};
5use cvkg_core::Rect;
6use lyon::tessellation::{
7    BuffersBuilder, FillOptions, FillTessellator, StrokeOptions, StrokeTessellator, VertexBuffers,
8};
9
10/// SVG tessellation parameters.
11pub(crate) struct TessellateParams<'a> {
12    pub(crate) fill_tessellator: &'a mut FillTessellator,
13    pub(crate) stroke_tessellator: &'a mut StrokeTessellator,
14    pub(crate) vertices: &'a mut Vec<Vertex>,
15    pub(crate) indices: &'a mut Vec<u32>,
16    pub(crate) parsed_animations: &'a [SvgAnimation],
17    pub(crate) finalized_animations: &'a mut Vec<SvgAnimation>,
18    pub(crate) paths: &'a mut Vec<crate::types::SvgPath>,
19}
20
21impl GpuRenderer {
22    /// load_svg -- Parses an SVG file and tessellates its paths into GPU triangles.
23    pub fn load_svg(&mut self, name: &str, data: &[u8]) {
24        if self.svg.model_cache.contains(name) {
25            return;
26        }
27
28        let mut opt = usvg::Options::default();
29        opt.fontdb_mut().load_system_fonts();
30        let tree = match usvg::Tree::from_data(data, &opt) {
31            Ok(t) => t,
32            Err(e) => {
33                log::error!("Failed to parse SVG '{}': {:?}, skipping load", name, e);
34                return;
35            }
36        };
37
38        // The viewBox is applied as the root group's transform.
39        // Use the tree size as the viewBox (which is the SVG's width/height).
40        let view_box = Rect {
41            x: 0.0,
42            y: 0.0,
43            width: tree.size().width(),
44            height: tree.size().height(),
45        };
46
47        let parsed_animations = parse_svg_animations(data);
48
49        let mut vertices = Vec::new();
50        let mut indices = Vec::new();
51        let mut fill_tessellator = FillTessellator::new();
52        let mut stroke_tessellator = StrokeTessellator::new();
53        let mut finalized_animations = Vec::new();
54        let mut paths = Vec::new();
55
56        for child in tree.root().children() {
57            let mut tess_params = TessellateParams {
58                fill_tessellator: &mut fill_tessellator,
59                stroke_tessellator: &mut stroke_tessellator,
60                vertices: &mut vertices,
61                indices: &mut indices,
62                parsed_animations: &parsed_animations,
63                finalized_animations: &mut finalized_animations,
64                paths: &mut paths,
65            };
66            self.tessellate_node(child, &mut tess_params);
67        }
68
69        self.svg.model_cache.put(
70            name.to_string(),
71            SvgModel {
72                vertices,
73                indices,
74                view_box,
75                paths,
76                animations: finalized_animations,
77            },
78        );
79        self.svg.tree_cache.put(name.to_string(), tree);
80    }
81
82    pub(crate) fn tessellate_node(&self, node: &usvg::Node, params: &mut TessellateParams<'_>) {
83        let start_idx = params.vertices.len();
84        let node_id = match node {
85            usvg::Node::Group(g) => g.id().to_string(),
86            usvg::Node::Path(p) => p.id().to_string(),
87            _ => String::new(),
88        };
89
90        if let usvg::Node::Group(ref group) = *node {
91            for child in group.children() {
92                let mut child_params = TessellateParams {
93                    fill_tessellator: params.fill_tessellator,
94                    stroke_tessellator: params.stroke_tessellator,
95                    vertices: params.vertices,
96                    indices: params.indices,
97                    parsed_animations: params.parsed_animations,
98                    finalized_animations: params.finalized_animations,
99                    paths: params.paths,
100                };
101                self.tessellate_node(child, &mut child_params);
102            }
103        } else if let usvg::Node::Path(ref path) = *node {
104            let has_fill = path.fill().is_some();
105            let has_stroke = path.stroke().is_some();
106
107            // If neither fill nor stroke, log and skip
108            if !has_fill && !has_stroke {
109                log::debug!("SVG path '{}' has no fill or stroke, skipping", node_id);
110                return;
111            }
112
113            let lyon_path = usvg_to_lyon(path, node.abs_transform());
114            let clip = [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY]; // Default clip
115
116            // Tessellate fill if present
117            if has_fill && let Some(fill) = path.fill() {
118                let paint = fill.paint();
119                let fill_opacity = fill.opacity().get();
120                // Convert SVG fill rule to Lyon fill rule
121                let fill_rule = match fill.rule() {
122                    usvg::FillRule::EvenOdd => lyon::tessellation::FillRule::EvenOdd,
123                    usvg::FillRule::NonZero => lyon::tessellation::FillRule::NonZero,
124                };
125
126                match paint {
127                    usvg::Paint::Color(c) => {
128                        let color = [
129                            c.red as f32 / 255.0,
130                            c.green as f32 / 255.0,
131                            c.blue as f32 / 255.0,
132                            fill_opacity,
133                        ];
134                        Self::tessellate_fill_solid(&lyon_path, color, &node_id, params, fill_rule);
135                    }
136                    usvg::Paint::LinearGradient(g) => {
137                        Self::tessellate_fill_gradient(
138                            &lyon_path,
139                            g,
140                            fill_opacity,
141                            &node_id,
142                            params,
143                            fill_rule,
144                        );
145                    }
146                    usvg::Paint::RadialGradient(g) => {
147                        Self::tessellate_fill_radial_gradient(
148                            &lyon_path,
149                            g,
150                            fill_opacity,
151                            &node_id,
152                            params,
153                            fill_rule,
154                        );
155                    }
156                    usvg::Paint::Pattern(_) => {
157                        log::warn!(
158                            "SVG path '{}' uses pattern fill which is not supported, using white fallback",
159                            node_id
160                        );
161                        let color = [1.0, 1.0, 1.0, fill_opacity];
162                        Self::tessellate_fill_solid(&lyon_path, color, &node_id, params, fill_rule);
163                    }
164                }
165            }
166
167            // Tessellate stroke if present
168            if has_stroke && let Some(stroke) = path.stroke() {
169                let base_vertex_idx = params.vertices.len() as u32;
170                let stroke_width = stroke.width().get(); // Direct float value
171                let color = match stroke.paint() {
172                    usvg::Paint::Color(c) => [
173                        c.red as f32 / 255.0,
174                        c.green as f32 / 255.0,
175                        c.blue as f32 / 255.0,
176                        stroke.opacity().get(),
177                    ],
178                    usvg::Paint::LinearGradient(_)
179                    | usvg::Paint::RadialGradient(_)
180                    | usvg::Paint::Pattern(_) => {
181                        log::warn!(
182                            "SVG path '{}' uses gradient/pattern stroke which is not supported, using white fallback",
183                            node_id
184                        );
185                        [1.0, 1.0, 1.0, 1.0]
186                    }
187                };
188
189                // Build stroke options from SVG stroke properties
190                let mut stroke_opts = StrokeOptions::default().with_line_width(stroke_width);
191
192                // Line cap
193                stroke_opts = match stroke.linecap() {
194                    usvg::LineCap::Butt => {
195                        stroke_opts.with_line_cap(lyon::tessellation::LineCap::Butt)
196                    }
197                    usvg::LineCap::Round => {
198                        stroke_opts.with_line_cap(lyon::tessellation::LineCap::Round)
199                    }
200                    usvg::LineCap::Square => {
201                        stroke_opts.with_line_cap(lyon::tessellation::LineCap::Square)
202                    }
203                };
204
205                // Line join
206                stroke_opts = match stroke.linejoin() {
207                    usvg::LineJoin::Miter => {
208                        stroke_opts.with_line_join(lyon::tessellation::LineJoin::Miter)
209                    }
210                    usvg::LineJoin::Round => {
211                        stroke_opts.with_line_join(lyon::tessellation::LineJoin::Round)
212                    }
213                    usvg::LineJoin::Bevel => {
214                        stroke_opts.with_line_join(lyon::tessellation::LineJoin::Bevel)
215                    }
216                    _ => stroke_opts,
217                };
218
219                // Miter limit
220                stroke_opts = stroke_opts.with_miter_limit(stroke.miterlimit().get());
221
222                // Dash array: Lyon's StrokeOptions does not support dash patterns
223                // natively. To render dashed strokes, the path would need to be
224                // split into dash/gap segments and tessellated per-segment, then
225                // the results merged. This is tracked as future work.
226                // Current behavior: strokes with dasharray are rendered as solid.
227                if let Some(dasharray) = stroke.dasharray() {
228                    let _ = dasharray; // Available for future dash tessellation.
229                }
230
231                let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
232                let path_length = lyon::algorithms::length::approximate_length(&lyon_path, 0.1);
233
234                if let Err(e) = params.stroke_tessellator.tessellate_path(
235                    &lyon_path,
236                    &stroke_opts,
237                    &mut BuffersBuilder::new(
238                        &mut buffers,
239                        CustomStrokeVertexConstructor {
240                            color,
241                            clip,
242                            path_length,
243                        },
244                    ),
245                ) {
246                    log::warn!(
247                        "SVG stroke tessellation failed for path '{}': {:?}, skipping",
248                        node_id,
249                        e
250                    );
251                    return;
252                }
253
254                params.vertices.extend(buffers.vertices);
255                for idx in buffers.indices {
256                    params.indices.push(base_vertex_idx + idx);
257                }
258            }
259        }
260
261        let end_idx = params.vertices.len();
262        let end_idx_indices = params.indices.len();
263        if !node_id.is_empty() && start_idx < end_idx {
264            for anim in params.parsed_animations {
265                if anim.target_id == node_id {
266                    let mut final_anim = anim.clone();
267                    final_anim.vertex_range = start_idx..end_idx;
268                    params.finalized_animations.push(final_anim);
269                }
270            }
271            // Record this path's range for per-path transforms.
272            params.paths.push(crate::types::SvgPath {
273                id: node_id,
274                vertex_range: start_idx..end_idx,
275                index_range: end_idx_indices..params.indices.len(),
276                local_transform: Default::default(),
277            });
278        }
279    }
280
281    /// Tessellate a solid-color fill.
282    fn tessellate_fill_solid(
283        lyon_path: &lyon::path::Path,
284        color: [f32; 4],
285        node_id: &String,
286        params: &mut TessellateParams<'_>,
287        fill_rule: lyon::tessellation::FillRule,
288    ) {
289        let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
290        let base_vertex_idx = params.vertices.len() as u32;
291        if let Err(e) = params.fill_tessellator.tessellate_path(
292            lyon_path,
293            &FillOptions::default().with_fill_rule(fill_rule),
294            &mut BuffersBuilder::new(&mut buffers, SceneVertexConstructor { color }),
295        ) {
296            log::warn!(
297                "SVG fill tessellation failed for path '{}': {:?}, skipping",
298                node_id,
299                e
300            );
301            return;
302        }
303        params.vertices.extend(buffers.vertices);
304        for idx in buffers.indices {
305            params.indices.push(base_vertex_idx + idx);
306        }
307    }
308
309    /// Compute gradient color for a position in SVG space.
310    fn gradient_color_at(stops: &[usvg::Stop], pos: f32, fill_opacity: f32) -> [f32; 4] {
311        if stops.is_empty() {
312            return [1.0, 1.0, 1.0, fill_opacity];
313        }
314        let pos = pos.clamp(0.0, 1.0);
315        let mut start = &stops[0];
316        let mut end = &stops[stops.len() - 1];
317        for w in stops.windows(2) {
318            if pos >= w[0].offset().get() && pos <= w[1].offset().get() {
319                start = &w[0];
320                end = &w[1];
321                break;
322            }
323        }
324        let so = start.offset().get();
325        let eo = end.offset().get();
326        if pos <= so {
327            let c = start.color();
328            return [
329                c.red as f32 / 255.0,
330                c.green as f32 / 255.0,
331                c.blue as f32 / 255.0,
332                start.opacity().get() * fill_opacity,
333            ];
334        }
335        if pos >= eo {
336            let c = end.color();
337            return [
338                c.red as f32 / 255.0,
339                c.green as f32 / 255.0,
340                c.blue as f32 / 255.0,
341                end.opacity().get() * fill_opacity,
342            ];
343        }
344        let range = eo - so;
345        if range < 0.0001 {
346            let c = start.color();
347            return [
348                c.red as f32 / 255.0,
349                c.green as f32 / 255.0,
350                c.blue as f32 / 255.0,
351                start.opacity().get() * fill_opacity,
352            ];
353        }
354        let t = (pos - so) / range;
355        let sc = start.color();
356        let ec = end.color();
357        [
358            (sc.red as f32 + (ec.red as f32 - sc.red as f32) * t) / 255.0,
359            (sc.green as f32 + (ec.green as f32 - sc.green as f32) * t) / 255.0,
360            (sc.blue as f32 + (ec.blue as f32 - sc.blue as f32) * t) / 255.0,
361            (start.opacity().get() + (end.opacity().get() - start.opacity().get()) * t)
362                * fill_opacity,
363        ]
364    }
365
366    /// Tessellate a linear gradient fill with per-vertex colors.
367    fn tessellate_fill_gradient(
368        lyon_path: &lyon::path::Path,
369        gradient: &usvg::LinearGradient,
370        fill_opacity: f32,
371        node_id: &String,
372        params: &mut TessellateParams<'_>,
373        fill_rule: lyon::tessellation::FillRule,
374    ) {
375        let x1 = gradient.x1();
376        let y1 = gradient.y1();
377        let x2 = gradient.x2();
378        let y2 = gradient.y2();
379        let dx = x2 - x1;
380        let dy = y2 - y1;
381        let grad_len_sq = dx * dx + dy * dy;
382
383        let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
384        let base_vertex_idx = params.vertices.len() as u32;
385        if let Err(e) = params.fill_tessellator.tessellate_path(
386            lyon_path,
387            &FillOptions::default(),
388            &mut BuffersBuilder::new(
389                &mut buffers,
390                SceneVertexConstructor {
391                    color: [1.0, 1.0, 1.0, 1.0],
392                },
393            ),
394        ) {
395            log::warn!(
396                "SVG gradient fill tessellation failed for path '{}': {:?}, skipping",
397                node_id,
398                e
399            );
400            return;
401        }
402
403        let stops = gradient.stops();
404        for mut vertex in buffers.vertices {
405            let px = vertex.position[0];
406            let py = vertex.position[1];
407            let t = if grad_len_sq < 0.0001 {
408                0.5
409            } else {
410                ((px - x1) * dx + (py - y1) * dy) / grad_len_sq
411            };
412            vertex.color = Self::gradient_color_at(stops, t, fill_opacity);
413            params.vertices.push(vertex);
414        }
415        for idx in buffers.indices {
416            params.indices.push(base_vertex_idx + idx);
417        }
418    }
419
420    /// Tessellate a radial gradient fill with per-vertex colors.
421    fn tessellate_fill_radial_gradient(
422        lyon_path: &lyon::path::Path,
423        gradient: &usvg::RadialGradient,
424        fill_opacity: f32,
425        node_id: &String,
426        params: &mut TessellateParams<'_>,
427        fill_rule: lyon::tessellation::FillRule,
428    ) {
429        let cx = gradient.cx();
430        let cy = gradient.cy();
431        let r = gradient.r();
432        let stops = gradient.stops();
433
434        let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
435        let base_vertex_idx = params.vertices.len() as u32;
436        if let Err(e) = params.fill_tessellator.tessellate_path(
437            lyon_path,
438            &FillOptions::default(),
439            &mut BuffersBuilder::new(
440                &mut buffers,
441                SceneVertexConstructor {
442                    color: [1.0, 1.0, 1.0, 1.0],
443                },
444            ),
445        ) {
446            log::warn!(
447                "SVG radial gradient fill tessellation failed for path '{}': {:?}, skipping",
448                node_id,
449                e
450            );
451            return;
452        }
453
454        for mut vertex in buffers.vertices {
455            let px = vertex.position[0];
456            let py = vertex.position[1];
457            let dist = ((px - cx) * (px - cx) + (py - cy) * (py - cy)).sqrt();
458            let r_val = r.get();
459            let t = if r_val < 0.001 {
460                0.5
461            } else {
462                (dist / r_val).clamp(0.0, 1.0)
463            };
464            vertex.color = Self::gradient_color_at(stops, t, fill_opacity);
465            params.vertices.push(vertex);
466        }
467        for idx in buffers.indices {
468            params.indices.push(base_vertex_idx + idx);
469        }
470    }
471
472    /// draw_svg -- Renders a pre-loaded SVG icon at the specified logical rect.
473    /// animation_time_offset shifts the animation phase for this instance,
474    /// allowing multiple draws of the same SVG to animate independently.
475    pub fn draw_svg(&mut self, name: &str, rect: Rect, color: Option<[f32; 4]>, material_id: u32) {
476        self.draw_svg_with_offset(name, rect, color, material_id, 0.0);
477    }
478
479    pub fn draw_svg_with_offset(
480        &mut self,
481        name: &str,
482        rect: Rect,
483        color: Option<[f32; 4]>,
484        material_id: u32,
485        animation_time_offset: f32,
486    ) {
487        self.draw_svg_with_order(name, rect, color, material_id, animation_time_offset, 0);
488    }
489
490    pub fn draw_svg_with_order(
491        &mut self,
492        name: &str,
493        rect: Rect,
494        color: Option<[f32; 4]>,
495        material_id: u32,
496        animation_time_offset: f32,
497        draw_order: i32,
498    ) {
499        let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
500            x: -10000.0,
501            y: -10000.0,
502            width: 20000.0,
503            height: 20000.0,
504        });
505        let scale = self.current_scale_factor();
506        let screen_w = self.current_width() as f32 / scale;
507        let screen_h = self.current_height() as f32 / scale;
508
509        if rect.x > clip_rect.x + clip_rect.width
510            || rect.x + rect.width < clip_rect.x
511            || rect.y > clip_rect.y + clip_rect.height
512            || rect.y + rect.height < clip_rect.y
513        {
514            return;
515        }
516
517        log::info!(
518            "DRAW_SVG '{}' called with rect: {:?}, model_view_box: {:?}",
519            name,
520            rect,
521            self.svg.model_cache.get(name).map(|m| m.view_box)
522        );
523
524        if rect.x > screen_w
525            || rect.x + rect.width < 0.0
526            || rect.y > screen_h
527            || rect.y + rect.height < 0.0
528        {
529            return;
530        }
531
532        let model = if let Some(m) = self.svg.model_cache.get(name) {
533            m.clone()
534        } else {
535            return;
536        };
537
538        let base_idx = self.vertices.len() as u32;
539        let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
540            x: -10000.0,
541            y: -10000.0,
542            width: 20000.0,
543            height: 20000.0,
544        });
545        let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
546        let scale = self.current_scale_factor();
547        let snap = |v: f32| (v * scale).round() / scale;
548
549        if model.paths.is_empty() {
550            // Fallback: no path data, treat all vertices as one blob.
551            let mut local_vertices = model.vertices.clone();
552            Self::position_vertices(
553                &mut local_vertices,
554                model.view_box,
555                rect,
556                material_id,
557                clip,
558                snap,
559            );
560            let base_vertex = self.vertices.len() as u32;
561            self.vertices.extend(local_vertices);
562            let index_count = model.indices.len();
563            for idx in &model.indices {
564                self.indices.push(base_vertex + *idx);
565            }
566            let material = Self::resolve_material(material_id);
567            let tid = self.get_texture_id("__mega_heim");
568            Self::emit_draw_call(
569                self,
570                material,
571                tid,
572                clip_rect,
573                index_count as u32,
574                base_vertex,
575            );
576        } else {
577            // Per-path rendering: each path gets its own transform and draw call.
578            for path in &model.paths {
579                let mut path_verts: Vec<Vertex> =
580                    model.vertices[path.vertex_range.clone()].to_vec();
581                // Apply local transform (translate, rotate, scale) in SVG space.
582                if path.local_transform.scale != 1.0
583                    || path.local_transform.rotation != 0.0
584                    || path.local_transform.translate != [0.0, 0.0]
585                {
586                    let s = path.local_transform.scale;
587                    let rad = path.local_transform.rotation.to_radians();
588                    let c = rad.cos();
589                    let sn = rad.sin();
590                    let tx = path.local_transform.translate[0];
591                    let ty = path.local_transform.translate[1];
592                    for v in &mut path_verts {
593                        let px = v.position[0] * s;
594                        let py = v.position[1] * s;
595                        v.position[0] = px * c - py * sn + tx;
596                        v.position[1] = px * sn + py * c + ty;
597                    }
598                }
599                // Apply animations targeting this path.
600                for anim in &model.animations {
601                    if anim.target_id == path.id {
602                        let effective_time = self.current_scene.time + animation_time_offset;
603                        let t = (effective_time % anim.duration) / anim.duration;
604                        let val = anim.evaluate(t);
605                        if anim.attribute_name == "transform" {
606                            let mut min_x = f32::MAX;
607                            let mut min_y = f32::MAX;
608                            let mut max_x = f32::MIN;
609                            let mut max_y = f32::MIN;
610                            for v in &path_verts {
611                                min_x = min_x.min(v.position[0]);
612                                min_y = min_y.min(v.position[1]);
613                                max_x = max_x.max(v.position[0]);
614                                max_y = max_y.max(v.position[1]);
615                            }
616                            let cx = (min_x + max_x) * 0.5;
617                            let cy = (min_y + max_y) * 0.5;
618                            let c = val.to_radians().cos();
619                            let s = val.to_radians().sin();
620                            for v in &mut path_verts {
621                                let dx = v.position[0] - cx;
622                                let dy = v.position[1] - cy;
623                                v.position[0] = cx + dx * c - dy * s;
624                                v.position[1] = cy + dx * s + dy * c;
625                            }
626                        } else if anim.attribute_name == "opacity" {
627                            for v in &mut path_verts {
628                                v.color[3] = val;
629                            }
630                        } else if anim.attribute_name == "stroke-dashoffset" {
631                            for v in &mut path_verts {
632                                v.slice[3] = 1.0 - val;
633                            }
634                        }
635                    }
636                }
637                // Position into output rect.
638                Self::position_vertices(
639                    &mut path_verts,
640                    model.view_box,
641                    rect,
642                    material_id,
643                    clip,
644                    snap,
645                );
646                let base_vertex = self.vertices.len() as u32;
647                let index_start = self.indices.len();
648                self.vertices.extend(path_verts);
649                // Remap indices for this path's vertex offset.
650                let path_index_start = path.index_range.start;
651                for idx in &model.indices[path.index_range.clone()] {
652                    self.indices
653                        .push(base_vertex + *idx - path_index_start as u32);
654                }
655                let index_count = path.index_range.len() as u32;
656                let material = Self::resolve_material(material_id);
657                let tid = self.get_texture_id("__mega_heim");
658                Self::emit_draw_call(self, material, tid, clip_rect, index_count, base_vertex);
659            }
660        }
661    }
662
663    /// Find a filter by ID in the SVG tree's filter list.
664    pub(crate) fn find_filter<'a>(
665        tree: &'a usvg::Tree,
666        filter_id: &str,
667    ) -> Option<&'a usvg::filter::Filter> {
668        tree.filters()
669            .iter()
670            .find(|f| f.id() == filter_id)
671            .map(|arc| arc.as_ref())
672    }
673}