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::vertex::{CustomStrokeVertexConstructor, SceneVertexConstructor, Vertex};
4use crate::types::{SvgAnimation, SvgModel, SvgPath};
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(
135                            &lyon_path, color, &node_id, params, fill_rule,
136                        );
137                    }
138                    usvg::Paint::LinearGradient(g) => {
139                        Self::tessellate_fill_gradient(
140                            &lyon_path, g, fill_opacity, &node_id, params, fill_rule,
141                        );
142                    }
143                    usvg::Paint::RadialGradient(g) => {
144                        Self::tessellate_fill_radial_gradient(
145                            &lyon_path, g, fill_opacity, &node_id, params, fill_rule,
146                        );
147                    }
148                    usvg::Paint::Pattern(_) => {
149                        log::warn!(
150                            "SVG path '{}' uses pattern fill which is not supported, using white fallback",
151                            node_id
152                        );
153                        let color = [1.0, 1.0, 1.0, fill_opacity];
154                        Self::tessellate_fill_solid(
155                            &lyon_path, color, &node_id, params, fill_rule,
156                        );
157                    }
158                }
159            }
160
161            // Tessellate stroke if present
162            if has_stroke && let Some(stroke) = path.stroke() {
163                let base_vertex_idx = params.vertices.len() as u32;
164                let stroke_width = stroke.width().get(); // Direct float value
165                let color = match stroke.paint() {
166                    usvg::Paint::Color(c) => [
167                        c.red as f32 / 255.0,
168                        c.green as f32 / 255.0,
169                        c.blue as f32 / 255.0,
170                        stroke.opacity().get(),
171                    ],
172                    usvg::Paint::LinearGradient(_)
173                    | usvg::Paint::RadialGradient(_)
174                    | usvg::Paint::Pattern(_) => {
175                        log::warn!(
176                            "SVG path '{}' uses gradient/pattern stroke which is not supported, using white fallback",
177                            node_id
178                        );
179                        [1.0, 1.0, 1.0, 1.0]
180                    }
181                };
182
183                // Build stroke options from SVG stroke properties
184                let mut stroke_opts = StrokeOptions::default()
185                    .with_line_width(stroke_width);
186
187                // Line cap
188                stroke_opts = match stroke.linecap() {
189                    usvg::LineCap::Butt => stroke_opts.with_line_cap(lyon::tessellation::LineCap::Butt),
190                    usvg::LineCap::Round => stroke_opts.with_line_cap(lyon::tessellation::LineCap::Round),
191                    usvg::LineCap::Square => stroke_opts.with_line_cap(lyon::tessellation::LineCap::Square),
192                };
193
194                // Line join
195                stroke_opts = match stroke.linejoin() {
196                    usvg::LineJoin::Miter => stroke_opts.with_line_join(lyon::tessellation::LineJoin::Miter),
197                    usvg::LineJoin::Round => stroke_opts.with_line_join(lyon::tessellation::LineJoin::Round),
198                    usvg::LineJoin::Bevel => stroke_opts.with_line_join(lyon::tessellation::LineJoin::Bevel),
199                    _ => stroke_opts,
200                };
201
202                // Miter limit
203                stroke_opts = stroke_opts.with_miter_limit(stroke.miterlimit().get());
204
205                // Dash array: Lyon's StrokeOptions does not support dash patterns
206                // natively. To render dashed strokes, the path would need to be
207                // split into dash/gap segments and tessellated per-segment, then
208                // the results merged. This is tracked as future work.
209                // Current behavior: strokes with dasharray are rendered as solid.
210                if let Some(dasharray) = stroke.dasharray() {
211                    let _ = dasharray; // Available for future dash tessellation.
212                }
213
214                let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
215                let path_length = lyon::algorithms::length::approximate_length(&lyon_path, 0.1);
216
217                if let Err(e) = params.stroke_tessellator.tessellate_path(
218                    &lyon_path,
219                    &stroke_opts,
220                    &mut BuffersBuilder::new(
221                        &mut buffers,
222                        CustomStrokeVertexConstructor { color, clip, path_length },
223                    ),
224                ) {
225                    log::warn!(
226                        "SVG stroke tessellation failed for path '{}': {:?}, skipping",
227                        node_id,
228                        e
229                    );
230                    return;
231                }
232
233                params.vertices.extend(buffers.vertices);
234                for idx in buffers.indices {
235                    params.indices.push(base_vertex_idx + idx);
236                }
237            }
238        }
239
240        let end_idx = params.vertices.len();
241        let end_idx_indices = params.indices.len();
242        if !node_id.is_empty() && start_idx < end_idx {
243            for anim in params.parsed_animations {
244                if anim.target_id == node_id {
245                    let mut final_anim = anim.clone();
246                    final_anim.vertex_range = start_idx..end_idx;
247                    params.finalized_animations.push(final_anim);
248                }
249            }
250            // Record this path's range for per-path transforms.
251            params.paths.push(crate::types::SvgPath {
252                id: node_id,
253                vertex_range: start_idx..end_idx,
254                index_range: end_idx_indices..params.indices.len(),
255                local_transform: Default::default(),
256            });
257        }
258    }
259
260    /// Tessellate a solid-color fill.
261    fn tessellate_fill_solid(
262        lyon_path: &lyon::path::Path,
263        color: [f32; 4],
264        node_id: &String,
265        params: &mut TessellateParams<'_>,
266        fill_rule: lyon::tessellation::FillRule,
267    ) {
268        let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
269        let base_vertex_idx = params.vertices.len() as u32;
270        if let Err(e) = params.fill_tessellator.tessellate_path(
271            lyon_path,
272            &FillOptions::default().with_fill_rule(fill_rule),
273            &mut BuffersBuilder::new(&mut buffers, SceneVertexConstructor { color }),
274        ) {
275            log::warn!(
276                "SVG fill tessellation failed for path '{}': {:?}, skipping",
277                node_id,
278                e
279            );
280            return;
281        }
282        params.vertices.extend(buffers.vertices);
283        for idx in buffers.indices {
284            params.indices.push(base_vertex_idx + idx);
285        }
286    }
287
288    /// Compute gradient color for a position in SVG space.
289    fn gradient_color_at(
290        stops: &[usvg::Stop],
291        pos: f32,
292        fill_opacity: f32,
293    ) -> [f32; 4] {
294        if stops.is_empty() {
295            return [1.0, 1.0, 1.0, fill_opacity];
296        }
297        let pos = pos.clamp(0.0, 1.0);
298        let mut start = &stops[0];
299        let mut end = &stops[stops.len() - 1];
300        for w in stops.windows(2) {
301            if pos >= w[0].offset().get() && pos <= w[1].offset().get() {
302                start = &w[0];
303                end = &w[1];
304                break;
305            }
306        }
307        let so = start.offset().get();
308        let eo = end.offset().get();
309        if pos <= so {
310            let c = start.color();
311            return [c.red as f32 / 255.0, c.green as f32 / 255.0, c.blue as f32 / 255.0, start.opacity().get() * fill_opacity];
312        }
313        if pos >= eo {
314            let c = end.color();
315            return [c.red as f32 / 255.0, c.green as f32 / 255.0, c.blue as f32 / 255.0, end.opacity().get() * fill_opacity];
316        }
317        let range = eo - so;
318        if range < 0.0001 {
319            let c = start.color();
320            return [c.red as f32 / 255.0, c.green as f32 / 255.0, c.blue as f32 / 255.0, start.opacity().get() * fill_opacity];
321        }
322        let t = (pos - so) / range;
323        let sc = start.color();
324        let ec = end.color();
325        [
326            (sc.red as f32 + (ec.red as f32 - sc.red as f32) * t) / 255.0,
327            (sc.green as f32 + (ec.green as f32 - sc.green as f32) * t) / 255.0,
328            (sc.blue as f32 + (ec.blue as f32 - sc.blue as f32) * t) / 255.0,
329            (start.opacity().get() + (end.opacity().get() - start.opacity().get()) * t) * fill_opacity,
330        ]
331    }
332
333    /// Tessellate a linear gradient fill with per-vertex colors.
334    fn tessellate_fill_gradient(
335        lyon_path: &lyon::path::Path,
336        gradient: &usvg::LinearGradient,
337        fill_opacity: f32,
338        node_id: &String,
339        params: &mut TessellateParams<'_>,
340        fill_rule: lyon::tessellation::FillRule,
341    ) {
342        let x1 = gradient.x1();
343        let y1 = gradient.y1();
344        let x2 = gradient.x2();
345        let y2 = gradient.y2();
346        let dx = x2 - x1;
347        let dy = y2 - y1;
348        let grad_len_sq = dx * dx + dy * dy;
349
350        let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
351        let base_vertex_idx = params.vertices.len() as u32;
352        if let Err(e) = params.fill_tessellator.tessellate_path(
353            lyon_path,
354            &FillOptions::default(),
355            &mut BuffersBuilder::new(&mut buffers, SceneVertexConstructor { color: [1.0, 1.0, 1.0, 1.0] }),
356        ) {
357            log::warn!("SVG gradient fill tessellation failed for path '{}': {:?}, skipping", node_id, e);
358            return;
359        }
360
361        let stops = gradient.stops();
362        for mut vertex in buffers.vertices {
363            let px = vertex.position[0];
364            let py = vertex.position[1];
365            let t = if grad_len_sq < 0.0001 { 0.5 } else { ((px - x1) * dx + (py - y1) * dy) / grad_len_sq };
366            vertex.color = Self::gradient_color_at(stops, t as f32, fill_opacity);
367            params.vertices.push(vertex);
368        }
369        for idx in buffers.indices {
370            params.indices.push(base_vertex_idx + idx);
371        }
372    }
373
374    /// Tessellate a radial gradient fill with per-vertex colors.
375    fn tessellate_fill_radial_gradient(
376        lyon_path: &lyon::path::Path,
377        gradient: &usvg::RadialGradient,
378        fill_opacity: f32,
379        node_id: &String,
380        params: &mut TessellateParams<'_>,
381        fill_rule: lyon::tessellation::FillRule,
382    ) {
383        let cx = gradient.cx();
384        let cy = gradient.cy();
385        let r = gradient.r();
386        let stops = gradient.stops();
387
388        let mut buffers: VertexBuffers<Vertex, u32> = VertexBuffers::new();
389        let base_vertex_idx = params.vertices.len() as u32;
390        if let Err(e) = params.fill_tessellator.tessellate_path(
391            lyon_path,
392            &FillOptions::default(),
393            &mut BuffersBuilder::new(&mut buffers, SceneVertexConstructor { color: [1.0, 1.0, 1.0, 1.0] }),
394        ) {
395            log::warn!("SVG radial gradient fill tessellation failed for path '{}': {:?}, skipping", node_id, e);
396            return;
397        }
398
399        for mut vertex in buffers.vertices {
400            let px = vertex.position[0];
401            let py = vertex.position[1];
402            let dist = ((px - cx) * (px - cx) + (py - cy) * (py - cy)).sqrt();
403            let r_val = r.get();
404            let t = if r_val < 0.001 { 0.5 } else { (dist / r_val).clamp(0.0, 1.0) };
405            vertex.color = Self::gradient_color_at(stops, t, fill_opacity);
406            params.vertices.push(vertex);
407        }
408        for idx in buffers.indices {
409            params.indices.push(base_vertex_idx + idx);
410        }
411    }
412
413    /// draw_svg -- Renders a pre-loaded SVG icon at the specified logical rect.
414    /// animation_time_offset shifts the animation phase for this instance,
415    /// allowing multiple draws of the same SVG to animate independently.
416    pub fn draw_svg(&mut self, name: &str, rect: Rect, color: Option<[f32; 4]>, material_id: u32) {
417        self.draw_svg_with_offset(name, rect, color, material_id, 0.0);
418    }
419
420    pub fn draw_svg_with_offset(&mut self, name: &str, rect: Rect, color: Option<[f32; 4]>, material_id: u32, animation_time_offset: f32) {
421        self.draw_svg_with_order(name, rect, color, material_id, animation_time_offset, 0);
422    }
423
424    pub fn draw_svg_with_order(&mut self, name: &str, rect: Rect, color: Option<[f32; 4]>, material_id: u32, animation_time_offset: f32, draw_order: i32) {
425        let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
426            x: -10000.0,
427            y: -10000.0,
428            width: 20000.0,
429            height: 20000.0,
430        });
431        let scale = self.current_scale_factor();
432        let screen_w = self.current_width() as f32 / scale;
433        let screen_h = self.current_height() as f32 / scale;
434
435        if rect.x > clip_rect.x + clip_rect.width
436            || rect.x + rect.width < clip_rect.x
437            || rect.y > clip_rect.y + clip_rect.height
438            || rect.y + rect.height < clip_rect.y
439        {
440            return;
441        }
442
443        log::info!("DRAW_SVG '{}' called with rect: {:?}, model_view_box: {:?}", name, rect, self.svg.model_cache.get(name).map(|m| m.view_box));
444        
445        if rect.x > screen_w
446            || rect.x + rect.width < 0.0
447            || rect.y > screen_h
448            || rect.y + rect.height < 0.0
449        {
450            return;
451        }
452
453        let model = if let Some(m) = self.svg.model_cache.get(name) {
454            m.clone()
455        } else {
456            return;
457        };
458
459        let base_idx = self.vertices.len() as u32;
460        let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
461            x: -10000.0,
462            y: -10000.0,
463            width: 20000.0,
464            height: 20000.0,
465        });
466        let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
467        let scale = self.current_scale_factor();
468        let snap = |v: f32| (v * scale).round() / scale;
469
470        if model.paths.is_empty() {
471            // Fallback: no path data, treat all vertices as one blob.
472            let mut local_vertices = model.vertices.clone();
473            Self::position_vertices(&mut local_vertices, model.view_box, rect, material_id, clip, snap);
474            let base_vertex = self.vertices.len() as u32;
475            self.vertices.extend(local_vertices);
476            let index_count = model.indices.len();
477            for idx in &model.indices {
478                self.indices.push(base_vertex + *idx);
479            }
480            let material = Self::resolve_material(material_id);
481            let tid = self.get_texture_id("__mega_heim");
482            Self::emit_draw_call(self, material, tid, clip_rect, index_count as u32, base_vertex);
483        } else {
484            // Per-path rendering: each path gets its own transform and draw call.
485            for path in &model.paths {
486                let mut path_verts: Vec<Vertex> = model.vertices[path.vertex_range.clone()].to_vec();
487                // Apply local transform (translate, rotate, scale) in SVG space.
488                if path.local_transform.scale != 1.0 || path.local_transform.rotation != 0.0 || path.local_transform.translate != [0.0, 0.0] {
489                    let s = path.local_transform.scale;
490                    let rad = path.local_transform.rotation.to_radians();
491                    let c = rad.cos();
492                    let sn = rad.sin();
493                    let tx = path.local_transform.translate[0];
494                    let ty = path.local_transform.translate[1];
495                    for v in &mut path_verts {
496                        let px = v.position[0] * s;
497                        let py = v.position[1] * s;
498                        v.position[0] = px * c - py * sn + tx;
499                        v.position[1] = px * sn + py * c + ty;
500                    }
501                }
502                // Apply animations targeting this path.
503                for anim in &model.animations {
504                    if anim.target_id == path.id {
505                        let effective_time = self.current_scene.time + animation_time_offset;
506                        let t = (effective_time % anim.duration) / anim.duration;
507                        let val = anim.evaluate(t);
508                        if anim.attribute_name == "transform" {
509                            let mut min_x = f32::MAX; let mut min_y = f32::MAX;
510                            let mut max_x = f32::MIN; let mut max_y = f32::MIN;
511                            for v in &path_verts {
512                                min_x = min_x.min(v.position[0]);
513                                min_y = min_y.min(v.position[1]);
514                                max_x = max_x.max(v.position[0]);
515                                max_y = max_y.max(v.position[1]);
516                            }
517                            let cx = (min_x + max_x) * 0.5;
518                            let cy = (min_y + max_y) * 0.5;
519                            let c = val.to_radians().cos();
520                            let s = val.to_radians().sin();
521                            for v in &mut path_verts {
522                                let dx = v.position[0] - cx;
523                                let dy = v.position[1] - cy;
524                                v.position[0] = cx + dx * c - dy * s;
525                                v.position[1] = cy + dx * s + dy * c;
526                            }
527                        } else if anim.attribute_name == "opacity" {
528                            for v in &mut path_verts { v.color[3] = val; }
529                        } else if anim.attribute_name == "stroke-dashoffset" {
530                            for v in &mut path_verts { v.slice[3] = 1.0 - val; }
531                        }
532                    }
533                }
534                // Position into output rect.
535                Self::position_vertices(&mut path_verts, model.view_box, rect, material_id, clip, snap);
536                let base_vertex = self.vertices.len() as u32;
537                let index_start = self.indices.len();
538                self.vertices.extend(path_verts);
539                // Remap indices for this path's vertex offset.
540                let path_index_start = path.index_range.start;
541                for idx in &model.indices[path.index_range.clone()] {
542                    self.indices.push(base_vertex + *idx - path_index_start as u32);
543                }
544                let index_count = path.index_range.len() as u32;
545                let material = Self::resolve_material(material_id);
546                let tid = self.get_texture_id("__mega_heim");
547                Self::emit_draw_call(self, material, tid, clip_rect, index_count, base_vertex);
548            }
549        }
550    }
551
552    /// Find a filter by ID in the SVG tree's filter list.
553    pub(crate) fn find_filter<'a>(
554        tree: &'a usvg::Tree,
555        filter_id: &str,
556    ) -> Option<&'a usvg::filter::Filter> {
557        tree.filters()
558            .iter()
559            .find(|f| f.id() == filter_id)
560            .map(|arc| arc.as_ref())
561    }
562}