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
10pub(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 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 tracing::error!("Failed to parse SVG '{}': {:?}, skipping load", name, e);
34 return;
35 }
36 };
37
38 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 !has_fill && !has_stroke {
109 tracing::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]; if has_fill && let Some(fill) = path.fill() {
118 let paint = fill.paint();
119 let fill_opacity = fill.opacity().get();
120 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 tracing::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 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(); 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 tracing::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 let mut stroke_opts = StrokeOptions::default().with_line_width(stroke_width);
191
192 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 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 stroke_opts = stroke_opts.with_miter_limit(stroke.miterlimit().get());
221
222 if let Some(dasharray) = stroke.dasharray() {
228 let _ = dasharray; }
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 tracing::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 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 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 tracing::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 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 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 tracing::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 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 tracing::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 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 tracing::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 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 for path in &model.paths {
579 let mut path_verts: Vec<Vertex> =
580 model.vertices[path.vertex_range.clone()].to_vec();
581 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 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 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 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 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}