1mod gpu;
2mod sprite;
3mod texture;
4pub mod camera;
5mod tilemap;
6mod lighting;
7pub mod font;
8pub mod msdf;
9pub mod shader;
10pub mod postprocess;
11pub mod radiance;
12pub mod geometry;
13pub mod rendertarget;
14pub mod sdf;
15pub mod test_harness;
17
18pub use gpu::GpuContext;
19pub use sprite::{SpriteCommand, SpritePipeline};
20pub use texture::{TextureId, TextureStore};
21pub use camera::Camera2D;
22pub use tilemap::{Tilemap, TilemapStore};
23pub use lighting::{LightingState, LightingUniform, PointLight, LightData, MAX_LIGHTS};
24pub use msdf::{MsdfFont, MsdfFontStore, MsdfGlyph};
25pub use shader::ShaderStore;
26pub use postprocess::PostProcessPipeline;
27pub use radiance::{RadiancePipeline, RadianceState, EmissiveSurface, Occluder, DirectionalLight, SpotLight};
28pub use geometry::GeometryBatch;
29pub use rendertarget::RenderTargetStore;
30pub use sdf::{SdfPipelineStore, SdfCommand, SdfFill};
31
32use crate::scripting::geometry_ops::GeoCommand;
33use crate::scripting::sdf_ops::SdfDrawCommand;
34use anyhow::Result;
35
36fn convert_sdf_draw_command(c: SdfDrawCommand) -> SdfCommand {
38 let fill = match c.fill_type {
39 0 => SdfFill::Solid { color: c.color },
40 1 => SdfFill::Outline { color: c.color, thickness: c.fill_param },
41 2 => SdfFill::SolidWithOutline { fill: c.color, outline: c.color2, thickness: c.fill_param },
42 3 => SdfFill::Gradient { from: c.color, to: c.color2, angle: c.fill_param, scale: c.gradient_scale },
43 4 => SdfFill::Glow { color: c.color, intensity: c.fill_param },
44 5 => SdfFill::CosinePalette {
45 a: [c.palette_params[0], c.palette_params[1], c.palette_params[2]],
46 b: [c.palette_params[3], c.palette_params[4], c.palette_params[5]],
47 c: [c.palette_params[6], c.palette_params[7], c.palette_params[8]],
48 d: [c.palette_params[9], c.palette_params[10], c.palette_params[11]],
49 },
50 _ => SdfFill::Solid { color: c.color },
51 };
52 SdfCommand {
53 sdf_expr: c.sdf_expr,
54 fill,
55 x: c.x,
56 y: c.y,
57 bounds: c.bounds,
58 layer: c.layer,
59 rotation: c.rotation,
60 scale: c.scale,
61 opacity: c.opacity,
62 }
63}
64
65#[derive(Debug, PartialEq)]
69enum RenderOp {
70 Sprites { start: usize, end: usize },
72 Geometry { start: usize, end: usize },
74 Sdf { start: usize, end: usize },
76}
77
78fn build_render_schedule(
83 sprites: &[SpriteCommand],
84 geo: &[GeoCommand],
85 sdf: &[SdfCommand],
86) -> Vec<RenderOp> {
87 let mut schedule = Vec::new();
88 let mut si = 0;
89 let mut gi = 0;
90 let mut di = 0;
91
92 while si < sprites.len() || gi < geo.len() || di < sdf.len() {
93 let sprite_layer = if si < sprites.len() { sprites[si].layer } else { i32::MAX };
95 let geo_layer = if gi < geo.len() { geo[gi].layer() } else { i32::MAX };
96 let sdf_layer = if di < sdf.len() { sdf[di].layer } else { i32::MAX };
97
98 let min_layer = sprite_layer.min(geo_layer).min(sdf_layer);
100
101 if sprite_layer == min_layer {
103 let start = si;
104 let bound = geo_layer.min(sdf_layer);
106 while si < sprites.len() && sprites[si].layer <= bound {
107 si += 1;
108 }
109 schedule.push(RenderOp::Sprites { start, end: si });
110 } else if geo_layer == min_layer {
111 let start = gi;
112 let sprite_bound = if si < sprites.len() { sprites[si].layer } else { i32::MAX };
115 let sdf_bound = if di < sdf.len() { sdf[di].layer } else { i32::MAX };
116 while gi < geo.len() && geo[gi].layer() < sprite_bound && geo[gi].layer() <= sdf_bound {
117 gi += 1;
118 }
119 schedule.push(RenderOp::Geometry { start, end: gi });
120 } else {
121 let start = di;
122 let sprite_bound = if si < sprites.len() { sprites[si].layer } else { i32::MAX };
124 let geo_bound = if gi < geo.len() { geo[gi].layer() } else { i32::MAX };
125 while di < sdf.len() && sdf[di].layer < sprite_bound && sdf[di].layer < geo_bound {
126 di += 1;
127 }
128 schedule.push(RenderOp::Sdf { start, end: di });
129 }
130 }
131
132 schedule
133}
134
135pub struct Renderer {
137 pub gpu: GpuContext,
138 pub sprites: SpritePipeline,
139 pub geometry: GeometryBatch,
140 pub shaders: ShaderStore,
141 pub postprocess: PostProcessPipeline,
142 pub textures: TextureStore,
143 pub camera: Camera2D,
144 pub lighting: LightingState,
145 pub radiance: RadiancePipeline,
146 pub radiance_state: RadianceState,
147 pub render_targets: RenderTargetStore,
149 pub frame_commands: Vec<SpriteCommand>,
151 pub geo_commands: Vec<GeoCommand>,
153 pub sdf_commands: Vec<SdfCommand>,
155 pub sdf_pipeline: SdfPipelineStore,
157 pub scale_factor: f32,
159 pub clear_color: [f32; 4],
161 pub elapsed_time: f32,
163 pub delta_time: f32,
165 pub mouse_pos: [f32; 2],
167 pub capture_pending: bool,
169 pub capture_result: Option<Vec<u8>>,
171}
172
173impl Renderer {
174 pub fn new(window: std::sync::Arc<winit::window::Window>) -> Result<Self> {
176 let scale_factor = window.scale_factor() as f32;
177 let gpu = GpuContext::new(window)?;
178 let sprites = SpritePipeline::new(&gpu);
179 let geometry = GeometryBatch::new(&gpu);
180 let shaders = ShaderStore::new(&gpu);
181 let postprocess = PostProcessPipeline::new(&gpu);
182 let sdf_pipeline = SdfPipelineStore::new(&gpu);
183 let radiance_pipeline = RadiancePipeline::new(&gpu);
184 let textures = TextureStore::new();
185 let logical_w = gpu.config.width as f32 / scale_factor;
187 let logical_h = gpu.config.height as f32 / scale_factor;
188 let camera = Camera2D {
189 viewport_size: [logical_w, logical_h],
190 ..Camera2D::default()
191 };
192 Ok(Self {
193 gpu,
194 sprites,
195 geometry,
196 shaders,
197 postprocess,
198 radiance: radiance_pipeline,
199 radiance_state: RadianceState::new(),
200 textures,
201 camera,
202 lighting: LightingState::default(),
203 render_targets: RenderTargetStore::new(),
204 frame_commands: Vec::new(),
205 geo_commands: Vec::new(),
206 sdf_commands: Vec::new(),
207 sdf_pipeline,
208 scale_factor,
209 clear_color: [0.1, 0.1, 0.15, 1.0],
210 elapsed_time: 0.0,
211 delta_time: 0.0,
212 mouse_pos: [0.0, 0.0],
213 capture_pending: false,
214 capture_result: None,
215 })
216 }
217
218 pub fn set_geo_commands(&mut self, cmds: Vec<GeoCommand>) {
220 self.geo_commands = cmds;
221 }
222
223 pub fn set_sdf_commands(&mut self, cmds: Vec<SdfDrawCommand>) {
226 self.sdf_commands = cmds.into_iter().map(convert_sdf_draw_command).collect();
227 }
228
229 pub fn render_frame(&mut self) -> Result<()> {
231 let output = self.gpu.surface.get_current_texture()?;
232 let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
233
234 let mut encoder = self.gpu.device.create_command_encoder(
235 &wgpu::CommandEncoderDescriptor { label: Some("frame_encoder") },
236 );
237
238 self.frame_commands.sort_by(|a, b| {
240 a.layer
241 .cmp(&b.layer)
242 .then(a.shader_id.cmp(&b.shader_id))
243 .then(a.blend_mode.cmp(&b.blend_mode))
244 .then(a.texture_id.cmp(&b.texture_id))
245 });
246
247 self.geo_commands.sort_by_key(|c| c.layer());
249
250 self.sdf_commands.sort_by_key(|c| c.layer);
252
253 let schedule = build_render_schedule(&self.frame_commands, &self.geo_commands, &self.sdf_commands);
255
256 self.shaders.flush(
258 &self.gpu.queue,
259 self.elapsed_time,
260 self.delta_time,
261 self.camera.viewport_size,
262 self.mouse_pos,
263 );
264
265 let lighting_uniform = self.lighting.to_uniform();
266 let clear_color = wgpu::Color {
267 r: self.clear_color[0] as f64,
268 g: self.clear_color[1] as f64,
269 b: self.clear_color[2] as f64,
270 a: self.clear_color[3] as f64,
271 };
272
273 self.sprites.prepare(&self.gpu.device, &self.gpu.queue, &self.camera, &lighting_uniform);
275 self.sdf_pipeline.prepare(&self.gpu.queue, &self.camera, 0.0);
276
277 let gi_active = self.radiance.compute(
279 &self.gpu,
280 &mut encoder,
281 &self.radiance_state,
282 &self.lighting,
283 self.camera.x,
284 self.camera.y,
285 self.camera.viewport_size[0],
286 self.camera.viewport_size[1],
287 );
288
289 if self.postprocess.has_effects() {
290 {
292 let sprite_target = self.postprocess.sprite_target(&self.gpu);
293 let camera_bg = self.sprites.camera_bind_group();
294
295 if schedule.is_empty() {
296 self.sprites.render(
298 &self.gpu.device, &self.gpu.queue, &self.textures, &self.shaders,
299 &[], sprite_target, &mut encoder, Some(clear_color),
300 );
301 } else {
302 let mut first = true;
303 for op in &schedule {
304 let cc = if first { Some(clear_color) } else { None };
305 first = false;
306 match op {
307 RenderOp::Sprites { start, end } => {
308 self.sprites.render(
309 &self.gpu.device, &self.gpu.queue, &self.textures, &self.shaders,
310 &self.frame_commands[*start..*end],
311 sprite_target, &mut encoder, cc,
312 );
313 }
314 RenderOp::Geometry { start, end } => {
315 self.geometry.flush_commands(
316 &self.gpu.device, &mut encoder, sprite_target,
317 camera_bg, &self.geo_commands[*start..*end], cc,
318 );
319 }
320 RenderOp::Sdf { start, end } => {
321 self.sdf_pipeline.render(
322 &self.gpu.device, &mut encoder, sprite_target,
323 &self.sdf_commands[*start..*end], cc,
324 );
325 }
326 }
327 }
328 }
329 }
330 if gi_active {
332 let sprite_target = self.postprocess.sprite_target(&self.gpu);
333 self.radiance.compose(&mut encoder, sprite_target);
334 }
335 self.postprocess.apply(&self.gpu, &mut encoder, &view);
336 } else {
337 let camera_bg = self.sprites.camera_bind_group();
339
340 if schedule.is_empty() {
341 self.sprites.render(
343 &self.gpu.device, &self.gpu.queue, &self.textures, &self.shaders,
344 &[], &view, &mut encoder, Some(clear_color),
345 );
346 } else {
347 let mut first = true;
348 for op in &schedule {
349 let cc = if first { Some(clear_color) } else { None };
350 first = false;
351 match op {
352 RenderOp::Sprites { start, end } => {
353 self.sprites.render(
354 &self.gpu.device, &self.gpu.queue, &self.textures, &self.shaders,
355 &self.frame_commands[*start..*end],
356 &view, &mut encoder, cc,
357 );
358 }
359 RenderOp::Geometry { start, end } => {
360 self.geometry.flush_commands(
361 &self.gpu.device, &mut encoder, &view,
362 camera_bg, &self.geo_commands[*start..*end], cc,
363 );
364 }
365 RenderOp::Sdf { start, end } => {
366 self.sdf_pipeline.render(
367 &self.gpu.device, &mut encoder, &view,
368 &self.sdf_commands[*start..*end], cc,
369 );
370 }
371 }
372 }
373 }
374 if gi_active {
376 self.radiance.compose(&mut encoder, &view);
377 }
378 }
379
380 self.gpu.queue.submit(std::iter::once(encoder.finish()));
381
382 if self.capture_pending {
384 self.capture_pending = false;
385 self.capture_result = self.capture_surface(&output.texture);
386 }
387
388 output.present();
389
390 self.frame_commands.clear();
391 self.geo_commands.clear();
392 self.sdf_commands.clear();
393 Ok(())
394 }
395
396 pub fn resize(&mut self, physical_width: u32, physical_height: u32, scale_factor: f32) {
399 if physical_width > 0 && physical_height > 0 {
400 self.scale_factor = scale_factor;
401 self.gpu.config.width = physical_width;
402 self.gpu.config.height = physical_height;
403 self.gpu.surface.configure(&self.gpu.device, &self.gpu.config);
404 self.camera.viewport_size = [
406 physical_width as f32 / scale_factor,
407 physical_height as f32 / scale_factor,
408 ];
409 }
410 }
411
412 fn capture_surface(&self, texture: &wgpu::Texture) -> Option<Vec<u8>> {
416 let width = self.gpu.config.width;
417 let height = self.gpu.config.height;
418 let bytes_per_pixel: u32 = 4;
419 let unpadded_bytes_per_row = width * bytes_per_pixel;
420 let padded_bytes_per_row = ((unpadded_bytes_per_row + 255) / 256) * 256;
421
422 let buffer = self.gpu.device.create_buffer(&wgpu::BufferDescriptor {
423 label: Some("capture_readback"),
424 size: (padded_bytes_per_row * height) as u64,
425 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
426 mapped_at_creation: false,
427 });
428
429 let mut encoder = self.gpu.device.create_command_encoder(
430 &wgpu::CommandEncoderDescriptor { label: Some("capture_encoder") },
431 );
432
433 encoder.copy_texture_to_buffer(
434 wgpu::TexelCopyTextureInfo {
435 texture,
436 mip_level: 0,
437 origin: wgpu::Origin3d::ZERO,
438 aspect: wgpu::TextureAspect::All,
439 },
440 wgpu::TexelCopyBufferInfo {
441 buffer: &buffer,
442 layout: wgpu::TexelCopyBufferLayout {
443 offset: 0,
444 bytes_per_row: Some(padded_bytes_per_row),
445 rows_per_image: Some(height),
446 },
447 },
448 wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
449 );
450
451 self.gpu.queue.submit(std::iter::once(encoder.finish()));
452
453 let buffer_slice = buffer.slice(..);
455 let (tx, rx) = std::sync::mpsc::channel();
456 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
457 let _ = tx.send(result);
458 });
459 self.gpu.device.poll(wgpu::Maintain::Wait);
460
461 if rx.recv().ok()?.ok().is_none() {
462 return None;
463 }
464
465 let data = buffer_slice.get_mapped_range();
466
467 let is_bgra = format!("{:?}", self.gpu.config.format).contains("Bgra");
469 let mut pixels = Vec::with_capacity((width * height * 4) as usize);
470 for y in 0..height {
471 let start = (y * padded_bytes_per_row) as usize;
472 let end = start + (width * 4) as usize;
473 let row = &data[start..end];
474 if is_bgra {
475 for chunk in row.chunks_exact(4) {
477 pixels.extend_from_slice(&[chunk[2], chunk[1], chunk[0], chunk[3]]);
478 }
479 } else {
480 pixels.extend_from_slice(row);
481 }
482 }
483
484 drop(data);
485 buffer.unmap();
486
487 use image::ImageEncoder;
489 let mut png_bytes = Vec::new();
490 let encoder = image::codecs::png::PngEncoder::new(&mut png_bytes);
491 if encoder.write_image(&pixels, width, height, image::ExtendedColorType::Rgba8).is_err() {
492 return None;
493 }
494
495 Some(png_bytes)
496 }
497
498 pub fn create_render_target(&mut self, id: u32, width: u32, height: u32) {
502 let surface_format = self.gpu.config.format;
503 self.render_targets.create(&self.gpu.device, id, width, height, surface_format);
504 if let Some(view) = self.render_targets.get_view(id) {
505 self.textures.register_render_target(
506 &self.gpu.device,
507 &self.sprites.texture_bind_group_layout,
508 id,
509 view,
510 width,
511 height,
512 );
513 }
514 }
515
516 pub fn destroy_render_target(&mut self, id: u32) {
518 self.render_targets.destroy(id);
519 self.textures.unregister_render_target(id);
520 }
521
522 pub fn render_targets_prepass(
527 &mut self,
528 target_queues: std::collections::HashMap<u32, Vec<SpriteCommand>>,
529 ) {
530 if target_queues.is_empty() {
531 return;
532 }
533
534 let mut encoder = self.gpu.device.create_command_encoder(
535 &wgpu::CommandEncoderDescriptor { label: Some("rt_encoder") },
536 );
537 let lighting_uniform = self.lighting.to_uniform();
538
539 for (target_id, mut cmds) in target_queues {
540 let view = self.render_targets.get_view(target_id);
541 let dims = self.render_targets.get_dims(target_id);
542 if let (Some(view), Some((tw, th))) = (view, dims) {
543 cmds.sort_by(|a, b| {
545 a.layer
546 .cmp(&b.layer)
547 .then(a.shader_id.cmp(&b.shader_id))
548 .then(a.blend_mode.cmp(&b.blend_mode))
549 .then(a.texture_id.cmp(&b.texture_id))
550 });
551 let target_camera = Camera2D {
553 x: tw as f32 / 2.0,
554 y: th as f32 / 2.0,
555 zoom: 1.0,
556 viewport_size: [tw as f32, th as f32],
557 ..Camera2D::default()
558 };
559 self.sprites.prepare(&self.gpu.device, &self.gpu.queue, &target_camera, &lighting_uniform);
560 self.sprites.render(
561 &self.gpu.device,
562 &self.gpu.queue,
563 &self.textures,
564 &self.shaders,
565 &cmds,
566 view,
567 &mut encoder,
568 Some(wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }),
569 );
570 }
571 }
572
573 self.gpu.queue.submit(std::iter::once(encoder.finish()));
574 }
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580
581 fn sprite(layer: i32) -> SpriteCommand {
584 SpriteCommand {
585 texture_id: 1, x: 0.0, y: 0.0, w: 16.0, h: 16.0, layer,
586 uv_x: 0.0, uv_y: 0.0, uv_w: 1.0, uv_h: 1.0,
587 tint_r: 1.0, tint_g: 1.0, tint_b: 1.0, tint_a: 1.0,
588 rotation: 0.0, origin_x: 0.5, origin_y: 0.5,
589 flip_x: false, flip_y: false, opacity: 1.0,
590 blend_mode: 0, shader_id: 0,
591 }
592 }
593
594 fn geo(layer: i32) -> GeoCommand {
595 GeoCommand::Triangle {
596 x1: 0.0, y1: 0.0, x2: 16.0, y2: 0.0, x3: 8.0, y3: 16.0,
597 r: 1.0, g: 1.0, b: 1.0, a: 1.0, layer,
598 }
599 }
600
601 fn sdf(layer: i32) -> SdfCommand {
602 SdfCommand {
603 sdf_expr: "length(p) - 10.0".to_string(),
604 fill: SdfFill::Solid { color: [1.0, 0.0, 0.0, 1.0] },
605 x: 32.0, y: 32.0, bounds: 15.0, layer,
606 rotation: 0.0, scale: 1.0, opacity: 1.0,
607 }
608 }
609
610 fn sdf_draw(fill_type: u32) -> SdfDrawCommand {
611 SdfDrawCommand {
612 sdf_expr: "length(p) - 10.0".to_string(),
613 fill_type,
614 color: [1.0, 0.0, 0.0, 1.0],
615 color2: [0.0, 1.0, 0.0, 1.0],
616 fill_param: 2.0,
617 palette_params: [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 0.0, 0.33, 0.67],
618 gradient_scale: 1.5,
619 x: 10.0, y: 20.0, bounds: 30.0, layer: 5,
620 rotation: 0.1, scale: 2.0, opacity: 0.8,
621 }
622 }
623
624 #[test]
627 fn test_schedule_empty_inputs() {
628 let schedule = build_render_schedule(&[], &[], &[]);
629 assert!(schedule.is_empty());
630 }
631
632 #[test]
633 fn test_schedule_sprites_only() {
634 let sprites = vec![sprite(0), sprite(1)];
635 let schedule = build_render_schedule(&sprites, &[], &[]);
636 assert_eq!(schedule, vec![RenderOp::Sprites { start: 0, end: 2 }]);
637 }
638
639 #[test]
640 fn test_schedule_geo_only() {
641 let geo_cmds = vec![geo(0), geo(1)];
642 let schedule = build_render_schedule(&[], &geo_cmds, &[]);
643 assert_eq!(schedule, vec![RenderOp::Geometry { start: 0, end: 2 }]);
644 }
645
646 #[test]
647 fn test_schedule_sdf_only() {
648 let sdf_cmds = vec![sdf(0), sdf(1)];
649 let schedule = build_render_schedule(&[], &[], &sdf_cmds);
650 assert_eq!(schedule, vec![RenderOp::Sdf { start: 0, end: 2 }]);
651 }
652
653 #[test]
654 fn test_schedule_same_layer_order() {
655 let sprites = vec![sprite(0)];
657 let geo_cmds = vec![geo(0)];
658 let sdf_cmds = vec![sdf(0)];
659 let schedule = build_render_schedule(&sprites, &geo_cmds, &sdf_cmds);
660 assert_eq!(schedule.len(), 3);
661 assert_eq!(schedule[0], RenderOp::Sprites { start: 0, end: 1 });
662 assert_eq!(schedule[1], RenderOp::Geometry { start: 0, end: 1 });
663 assert_eq!(schedule[2], RenderOp::Sdf { start: 0, end: 1 });
664 }
665
666 #[test]
667 fn test_schedule_interleaved_layers() {
668 let sprites = vec![sprite(0)];
670 let geo_cmds = vec![geo(1)];
671 let sdf_cmds = vec![sdf(2)];
672 let schedule = build_render_schedule(&sprites, &geo_cmds, &sdf_cmds);
673 assert_eq!(schedule.len(), 3);
674 assert_eq!(schedule[0], RenderOp::Sprites { start: 0, end: 1 });
675 assert_eq!(schedule[1], RenderOp::Geometry { start: 0, end: 1 });
676 assert_eq!(schedule[2], RenderOp::Sdf { start: 0, end: 1 });
677 }
678
679 #[test]
680 fn test_schedule_mixed_layers() {
681 let sprites = vec![sprite(0), sprite(2)];
683 let geo_cmds = vec![geo(1)];
684 let schedule = build_render_schedule(&sprites, &geo_cmds, &[]);
685 assert!(schedule.len() >= 2);
687 assert!(matches!(schedule[0], RenderOp::Sprites { .. }));
688 }
689
690 #[test]
691 fn test_schedule_all_consumed() {
692 let sprites = vec![sprite(0), sprite(0), sprite(1)];
694 let geo_cmds = vec![geo(0), geo(2)];
695 let sdf_cmds = vec![sdf(1)];
696 let schedule = build_render_schedule(&sprites, &geo_cmds, &sdf_cmds);
697
698 let mut sprite_count = 0;
699 let mut geo_count = 0;
700 let mut sdf_count = 0;
701 for op in &schedule {
702 match op {
703 RenderOp::Sprites { start, end } => sprite_count += end - start,
704 RenderOp::Geometry { start, end } => geo_count += end - start,
705 RenderOp::Sdf { start, end } => sdf_count += end - start,
706 }
707 }
708 assert_eq!(sprite_count, 3, "all sprites consumed");
709 assert_eq!(geo_count, 2, "all geo consumed");
710 assert_eq!(sdf_count, 1, "all sdf consumed");
711 }
712
713 #[test]
716 fn test_convert_sdf_solid() {
717 let cmd = convert_sdf_draw_command(sdf_draw(0));
718 assert!(matches!(cmd.fill, SdfFill::Solid { color } if color == [1.0, 0.0, 0.0, 1.0]));
719 }
720
721 #[test]
722 fn test_convert_sdf_outline() {
723 let cmd = convert_sdf_draw_command(sdf_draw(1));
724 assert!(matches!(cmd.fill, SdfFill::Outline { color, thickness }
725 if color == [1.0, 0.0, 0.0, 1.0] && thickness == 2.0));
726 }
727
728 #[test]
729 fn test_convert_sdf_solid_with_outline() {
730 let cmd = convert_sdf_draw_command(sdf_draw(2));
731 assert!(matches!(cmd.fill, SdfFill::SolidWithOutline { fill, outline, thickness }
732 if fill == [1.0, 0.0, 0.0, 1.0] && outline == [0.0, 1.0, 0.0, 1.0] && thickness == 2.0));
733 }
734
735 #[test]
736 fn test_convert_sdf_gradient() {
737 let cmd = convert_sdf_draw_command(sdf_draw(3));
738 assert!(matches!(cmd.fill, SdfFill::Gradient { from, to, angle, scale }
739 if from == [1.0, 0.0, 0.0, 1.0] && to == [0.0, 1.0, 0.0, 1.0]
740 && angle == 2.0 && scale == 1.5));
741 }
742
743 #[test]
744 fn test_convert_sdf_glow() {
745 let cmd = convert_sdf_draw_command(sdf_draw(4));
746 assert!(matches!(cmd.fill, SdfFill::Glow { color, intensity }
747 if color == [1.0, 0.0, 0.0, 1.0] && intensity == 2.0));
748 }
749
750 #[test]
751 fn test_convert_sdf_cosine_palette() {
752 let cmd = convert_sdf_draw_command(sdf_draw(5));
753 assert!(matches!(cmd.fill, SdfFill::CosinePalette { a, b, c, d }
754 if a == [0.5, 0.5, 0.5] && b == [0.5, 0.5, 0.5]
755 && c == [1.0, 1.0, 1.0] && d == [0.0, 0.33, 0.67]));
756 }
757
758 #[test]
759 fn test_convert_sdf_unknown_fallback() {
760 let cmd = convert_sdf_draw_command(sdf_draw(99));
761 assert!(matches!(cmd.fill, SdfFill::Solid { color } if color == [1.0, 0.0, 0.0, 1.0]));
762 }
763
764 #[test]
765 fn test_convert_sdf_field_passthrough() {
766 let cmd = convert_sdf_draw_command(sdf_draw(0));
767 assert_eq!(cmd.sdf_expr, "length(p) - 10.0");
768 assert_eq!(cmd.x, 10.0);
769 assert_eq!(cmd.y, 20.0);
770 assert_eq!(cmd.bounds, 30.0);
771 assert_eq!(cmd.layer, 5);
772 assert_eq!(cmd.rotation, 0.1);
773 assert_eq!(cmd.scale, 2.0);
774 assert_eq!(cmd.opacity, 0.8);
775 }
776}