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
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 log::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 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]; 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(
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 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(); 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 let mut stroke_opts = StrokeOptions::default()
185 .with_line_width(stroke_width);
186
187 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 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 stroke_opts = stroke_opts.with_miter_limit(stroke.miterlimit().get());
204
205 if let Some(dasharray) = stroke.dasharray() {
211 let _ = dasharray; }
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 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 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 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 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 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 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 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 for path in &model.paths {
486 let mut path_verts: Vec<Vertex> = model.vertices[path.vertex_range.clone()].to_vec();
487 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 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 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 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 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}