1use crate::components::{
8 Decal, DirectionalLight, GlassPanel, GlassPanelGeometry, InstancedProp, Material,
9 ParticleEmitter, PhysicsJoint, PhysicsJointKind, PointLight, Prop, RectAreaLight,
10 ReflectionProbe, RigidBody, SPOT_MAX_ANGLE_DEG, SdfVolume, SpotLight, SpotLightGeometry,
11 VolumetricFog, VoxelChunk, WaterSurface, WaterWave,
12};
13
14use crate::components::MAX_WATER_WAVES;
17
18fn sdf_current_platform_source(v: &SdfVolume) -> Option<String> {
22 let platform = crate::platform::Platform::current();
23 if let Some(map) = &v.fragment_shaders
24 && let Some(src) = map.get(platform.key()).filter(|s| !s.is_empty())
25 {
26 return Some(src.clone());
27 }
28 if v.fragment_shader.is_empty() {
29 return None;
30 }
31 let ext = std::path::Path::new(&v.fragment_shader)
32 .extension()
33 .and_then(|e| e.to_str())
34 .unwrap_or("");
35 if platform.accepts_ext(ext) {
36 Some(v.fragment_shader.clone())
37 } else {
38 None
39 }
40}
41
42pub fn sdf_volume(mut v: SdfVolume) -> SdfVolume {
49 use crate::components::sdf_volume::{SDF_MAX_STEPS_CEILING, SDF_MAX_STEPS_FLOOR};
50 for axis in v.extent.iter_mut() {
53 if !axis.is_finite() || *axis <= 0.0 {
54 *axis = 1.0;
55 }
56 }
57 if !v.max_gradient.is_finite() || v.max_gradient <= 0.0 {
58 v.max_gradient = 1.0;
59 }
60 v.max_steps = v
61 .max_steps
62 .clamp(SDF_MAX_STEPS_FLOOR, SDF_MAX_STEPS_CEILING);
63 if !v.max_distance.is_finite() || v.max_distance < 0.1 {
64 v.max_distance = 0.1;
65 }
66 if v.volumetric {
67 v.cast_shadows = false;
68 }
69 if let Some(src) = sdf_current_platform_source(&v) {
70 v.fragment_shader = src;
71 }
72 v
73}
74
75pub fn point_light(mut args: PointLight) -> PointLight {
77 args.intensity = args.intensity.max(0.0);
78 args.range = args.range.max(0.0);
79 args
80}
81
82pub(crate) fn spot_light(mut args: SpotLight) -> SpotLight {
83 args.intensity = args.intensity.max(0.0);
84 args.range = args.range.max(0.0);
85 args.direction = args.unit_direction();
86 args.outer_angle = args.outer_angle.clamp(0.0, SPOT_MAX_ANGLE_DEG);
87 args.inner_angle = args.inner_angle.clamp(0.0, args.outer_angle);
88 args
89}
90
91pub(crate) fn rect_area_light(mut args: RectAreaLight) -> RectAreaLight {
92 args.intensity = args.intensity.max(0.0);
93 args.range = args.range.max(0.0);
94 let n = args.normal;
97 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
98 args.normal = if len < 1e-6 {
99 [0.0, 0.0, 1.0]
100 } else {
101 [n[0] / len, n[1] / len, n[2] / len]
102 };
103 args.half_size[0] = args.half_size[0].max(1e-3);
104 args.half_size[1] = args.half_size[1].max(1e-3);
105 args
106}
107
108pub(crate) fn directional_light(mut args: DirectionalLight) -> DirectionalLight {
109 args.intensity = args.intensity.max(0.0);
110 args
111}
112
113pub fn material(mut args: Material) -> Material {
118 args.roughness = args.roughness.clamp(0.0, 1.0);
119 args.metallic = args.metallic.clamp(0.0, 1.0);
120 args.macro_variation = args.macro_variation.clamp(0.0, 1.0);
121 args.terrain_blend = args.terrain_blend.clamp(0.0, 1.0);
122 args.secondary_blend_sharpness = args.secondary_blend_sharpness.clamp(0.0, 1.0);
123 args.alpha_cutoff = args.alpha_cutoff.clamp(0.0, 1.0);
124 args.opacity = args.opacity.clamp(0.0, 1.0);
125 if args.see_through {
128 args.transparent = true;
129 }
130 args
131}
132
133pub(crate) fn glass_panel(mut args: GlassPanel) -> GlassPanel {
134 args.normal = args.unit_normal();
135 args.half_size[0] = args.half_size[0].max(1e-3);
136 args.half_size[1] = args.half_size[1].max(1e-3);
137 args.opacity = args.opacity.clamp(0.0, 1.0);
138 args.refraction_strength = args.refraction_strength.max(0.0);
139 args.fresnel_power = args.fresnel_power.max(0.0);
140 args
141}
142
143pub(crate) fn water_surface(mut args: WaterSurface) -> WaterSurface {
144 args.subdivisions = args.subdivisions.clamp(8, 255);
145 if args.waves.len() > MAX_WATER_WAVES {
146 args.waves.truncate(MAX_WATER_WAVES);
147 }
148 if args.waves.is_empty() {
149 args.waves.push(WaterWave::default());
150 }
151 args
152}
153
154pub fn joint(mut args: PhysicsJoint) -> PhysicsJoint {
156 if let Some(k) = PhysicsJointKind::from_str_norm(&args.kind) {
158 args.kind = k.as_str().to_string();
159 }
160 args
161}
162
163pub fn decal(mut args: Decal) -> Decal {
165 args.tint[3] = args.tint[3].clamp(0.0, 1.0);
169 args
170}
171
172pub(crate) fn reflection_probe(mut args: ReflectionProbe) -> ReflectionProbe {
173 for e in &mut args.half_extents {
176 *e = e.max(0.0);
177 }
178 args
179}
180
181pub(crate) fn rigid_body(mut args: RigidBody) -> RigidBody {
182 args.is_grounded = true;
184 args
185}
186
187pub fn prop(mut args: Prop) -> Prop {
189 args.cull_distance = args.cull_distance.max(0.0);
190 args.is_held = false;
191 args
192}
193
194pub(crate) fn particle_emitter(mut args: ParticleEmitter) -> ParticleEmitter {
195 args.spread_deg = args.spread_deg.clamp(0.0, 180.0);
199 args.speed_min = args.speed_min.max(0.0);
200 if !args.speed_max.is_finite() || args.speed_max < args.speed_min {
201 args.speed_max = args.speed_min;
202 }
203 if !args.lifetime_min.is_finite() || args.lifetime_min <= 0.0 {
204 args.lifetime_min = 0.001;
205 }
206 if !args.lifetime_max.is_finite() || args.lifetime_max < args.lifetime_min {
207 args.lifetime_max = args.lifetime_min;
208 }
209 args.spawn_rate = args.spawn_rate.max(0.0);
210 args.max_particles = args.max_particles.clamp(1, 65_536);
211 args.size_start = args.size_start.max(0.0);
212 args.size_end = args.size_end.max(0.0);
213 for c in args.color_start.iter_mut().chain(args.color_end.iter_mut()) {
214 if !c.is_finite() {
215 *c = 0.0;
216 }
217 }
218 args
219}
220
221pub fn volumetric_fog(mut args: VolumetricFog) -> VolumetricFog {
223 args.density = args.density.max(0.0);
227 args.height_falloff = args.height_falloff.max(0.0);
228 args.ambient = args.ambient.max(0.0);
229 if args.max_distance <= 0.0 || !args.max_distance.is_finite() {
230 args.max_distance = 1.0;
231 }
232 args.phase_g = args.phase_g.clamp(-0.95, 0.95);
235 args
236}
237
238pub(crate) fn instanced_prop(mut args: InstancedProp) -> InstancedProp {
239 args.cull_distance = args.cull_distance.max(0.0);
240 args
241}
242
243pub fn voxel_chunk(mut args: VoxelChunk) -> VoxelChunk {
245 args.block_size = args.block_size.max(0.0);
246 if args.lod_levels == 0 {
247 args.lod_levels = 1;
248 }
249 args.lod_levels = args.lod_levels.min(8);
250 args
251}
252
253#[cfg(test)]
254mod tests {
255 use crate::components::*;
256
257 mod material {
258 use super::*;
259
260 #[test]
261 fn default_is_opaque_and_not_see_through() {
262 let m = Material::default();
263 assert!(!m.transparent);
264 assert!(!m.see_through);
265 assert_eq!(m.opacity, 1.0);
266 }
267
268 #[test]
269 fn see_through_implies_transparent() {
270 let m = super::super::material(Material {
273 see_through: true,
274 ..Material::default()
275 });
276 assert!(m.see_through);
277 assert!(m.transparent);
278 }
279
280 #[test]
281 fn transparent_without_see_through_stays_opaque_layer() {
282 let m = super::super::material(Material {
285 transparent: true,
286 ..Material::default()
287 });
288 assert!(m.transparent);
289 assert!(!m.see_through);
290 }
291 }
292
293 mod decal {
294 use super::*;
295
296 #[test]
297 fn deserialises_with_defaults() {
298 let d: Decal = serde_json::from_str("{}").unwrap();
299 assert_eq!(d.position, [0.0, 0.0, 0.0]);
300 assert_eq!(d.size, [1.0, 1.0, 1.0]);
301 assert_eq!(d.tint, [1.0, 1.0, 1.0, 1.0]);
302 assert!(d.visible);
303 assert!(d.texture.is_none());
304 }
305
306 #[test]
307 fn deserialises_with_all_fields() {
308 crate::ecs::asset_id::reset_interner();
309 let json = r#"{
310 "texture":"tex_bullet",
311 "position":[1.0,2.0,3.0],
312 "rotation_deg":[0,90,0],
313 "size":[0.4,0.2,0.4],
314 "tint":[0.9,0.2,0.1,0.8],
315 "visible":false
316 }"#;
317 let d: Decal = serde_json::from_str(json).unwrap();
318 assert_eq!(d.position, [1.0, 2.0, 3.0]);
319 assert_eq!(d.rotation_deg, [0.0, 90.0, 0.0]);
320 assert_eq!(d.size, [0.4, 0.2, 0.4]);
321 assert_eq!(d.tint, [0.9, 0.2, 0.1, 0.8]);
322 assert!(!d.visible);
323 assert!(d.texture.is_some());
324 }
325
326 #[test]
327 fn clamps_alpha_through_from_args() {
328 let json = r#"{"tint":[1,1,1,5.0]}"#;
329 let parsed: Decal = serde_json::from_str(json).unwrap();
330 let normalised = super::super::decal(parsed);
331 assert_eq!(normalised.tint[3], 1.0);
332
333 let json = r#"{"tint":[1,1,1,-0.5]}"#;
334 let parsed: Decal = serde_json::from_str(json).unwrap();
335 let normalised = super::super::decal(parsed);
336 assert_eq!(normalised.tint[3], 0.0);
337 }
338 }
339
340 mod glass_panel {
341 use super::*;
342
343 #[test]
344 fn from_args_normalizes_normal() {
345 let g = super::super::glass_panel(GlassPanel {
346 normal: [0.0, 0.0, 4.0],
347 ..Default::default()
348 });
349 let len = (g.normal[0].powi(2) + g.normal[1].powi(2) + g.normal[2].powi(2)).sqrt();
350 assert!((len - 1.0).abs() < 1e-5);
351 assert!((g.normal[2] - 1.0).abs() < 1e-5);
352 }
353
354 #[test]
355 fn from_args_falls_back_on_degenerate_normal() {
356 let g = super::super::glass_panel(GlassPanel {
357 normal: [0.0, 0.0, 0.0],
358 ..Default::default()
359 });
360 assert_eq!(g.normal, [0.0, 0.0, 1.0]);
361 }
362
363 #[test]
364 fn from_args_clamps_ranges() {
365 let g = super::super::glass_panel(GlassPanel {
366 half_size: [-2.0, 0.0],
367 opacity: 1.5,
368 refraction_strength: -0.1,
369 fresnel_power: -3.0,
370 ..Default::default()
371 });
372 assert!(g.half_size[0] > 0.0 && g.half_size[1] > 0.0);
373 assert_eq!(g.opacity, 1.0);
374 assert_eq!(g.refraction_strength, 0.0);
375 assert_eq!(g.fresnel_power, 0.0);
376 }
377 }
378
379 mod joint {
380 use super::*;
381
382 #[test]
383 fn deserialises_with_defaults() {
384 let j: PhysicsJoint = serde_json::from_str("{}").unwrap();
385 assert_eq!(j.kind, "fixed");
386 assert_eq!(j.anchor_a, [0.0, 0.0, 0.0]);
387 assert_eq!(j.axis, [0.0, 1.0, 0.0]);
388 assert!(!j.limits_enabled);
389 assert_eq!(j.motor_max_force, 0.0);
390 }
391
392 #[test]
393 fn deserialises_all_fields() {
394 crate::ecs::asset_id::reset_interner();
395 let json = r#"{
396 "kind":"revolute",
397 "body_a":"door",
398 "body_b":"wall",
399 "anchor_a":[0.5,1.0,0.0],
400 "anchor_b":[1.0,1.0,0.0],
401 "axis":[0,1,0],
402 "limits_enabled":true,
403 "limits":[-90,90],
404 "motor_target_velocity":30.0,
405 "motor_max_force":50.0
406 }"#;
407 let j: PhysicsJoint = serde_json::from_str(json).unwrap();
408 assert_eq!(j.parsed_kind(), PhysicsJointKind::Revolute);
409 assert!(j.body_a.is_some());
410 assert!(j.body_b.is_some());
411 assert!(j.limits_enabled);
412 }
413
414 #[test]
415 fn aliases_resolve_to_canonical_kind() {
416 assert_eq!(
417 PhysicsJointKind::from_str_norm("hinge"),
418 Some(PhysicsJointKind::Revolute)
419 );
420 assert_eq!(
421 PhysicsJointKind::from_str_norm("WELD"),
422 Some(PhysicsJointKind::Fixed)
423 );
424 assert_eq!(
425 PhysicsJointKind::from_str_norm("ball"),
426 Some(PhysicsJointKind::Spherical)
427 );
428 assert_eq!(
429 PhysicsJointKind::from_str_norm("slider"),
430 Some(PhysicsJointKind::Prismatic)
431 );
432 }
433
434 #[test]
435 fn from_args_normalises_kind_string() {
436 let json = r#"{"kind":"HINGE"}"#;
437 let parsed: PhysicsJoint = serde_json::from_str(json).unwrap();
438 let normalised = super::super::joint(parsed);
439 assert_eq!(normalised.kind, "revolute");
440 }
441
442 #[test]
443 fn unknown_kind_falls_back_to_fixed() {
444 let j = PhysicsJoint {
445 kind: "frumpus".to_string(),
446 ..Default::default()
447 };
448 assert_eq!(j.parsed_kind(), PhysicsJointKind::Fixed);
449 }
450 }
451
452 mod particle_emitter {
453 use super::*;
454
455 #[test]
456 fn deserialises_with_defaults() {
457 let p: ParticleEmitter = serde_json::from_str("{}").unwrap();
458 assert_eq!(p.position, [0.0, 0.0, 0.0]);
459 assert_eq!(p.direction, [0.0, 1.0, 0.0]);
460 assert_eq!(p.max_particles, 256);
461 assert!(p.visible);
462 assert!(p.texture.is_none());
463 }
464
465 #[test]
466 fn deserialises_with_all_fields() {
467 crate::ecs::asset_id::reset_interner();
468 let json = r#"{
469 "texture":"tex_spark","position":[1,2,3],"direction":[0,1,0],
470 "spread_deg":30,"speed_min":1.5,"speed_max":4.0,
471 "lifetime_min":0.5,"lifetime_max":1.0,"gravity":[0,-1,0],
472 "spawn_rate":60,"max_particles":128,"size_start":0.1,"size_end":0.02,
473 "color_start":[1,0.5,0,1],"color_end":[1,0,0,0],"visible":false
474 }"#;
475 let p: ParticleEmitter = serde_json::from_str(json).unwrap();
476 assert_eq!(p.position, [1.0, 2.0, 3.0]);
477 assert_eq!(p.max_particles, 128);
478 assert_eq!(p.color_start, [1.0, 0.5, 0.0, 1.0]);
479 assert!(!p.visible);
480 assert!(p.texture.is_some());
481 }
482
483 #[test]
484 fn from_args_clamps_invalid_inputs() {
485 let a = ParticleEmitter {
486 spread_deg: 300.0,
487 speed_min: -1.0,
488 speed_max: -5.0,
489 lifetime_min: -0.4,
490 lifetime_max: -2.0,
491 spawn_rate: -10.0,
492 max_particles: 0,
493 size_start: -0.5,
494 size_end: -0.1,
495 ..Default::default()
496 };
497 let n = super::super::particle_emitter(a);
498 assert_eq!(n.spread_deg, 180.0);
499 assert_eq!(n.speed_min, 0.0);
500 assert_eq!(n.speed_max, 0.0);
501 assert!(n.lifetime_min > 0.0);
502 assert!(n.lifetime_max >= n.lifetime_min);
503 assert_eq!(n.spawn_rate, 0.0);
504 assert_eq!(n.max_particles, 1);
505 assert_eq!(n.size_start, 0.0);
506 assert_eq!(n.size_end, 0.0);
507 }
508
509 #[test]
510 fn from_args_lifts_speed_max_to_speed_min() {
511 let a = ParticleEmitter {
512 speed_min: 5.0,
513 speed_max: 2.0,
514 ..Default::default()
515 };
516 let n = super::super::particle_emitter(a);
517 assert!((n.speed_max - n.speed_min).abs() < 1e-6);
518 }
519
520 #[test]
521 fn from_args_clamps_max_particles_upper() {
522 let a = ParticleEmitter {
523 max_particles: 200_000,
524 ..Default::default()
525 };
526 let n = super::super::particle_emitter(a);
527 assert_eq!(n.max_particles, 65_536);
528 }
529 }
530
531 mod volumetric_fog {
532 use super::*;
533
534 #[test]
535 fn deserialises_with_defaults() {
536 let f: VolumetricFog = serde_json::from_str("{}").unwrap();
537 assert!(f.enabled);
538 assert_eq!(f.color, [0.7, 0.78, 0.85]);
539 assert!((f.density - 0.05).abs() < 1e-6);
540 assert!((f.max_distance - 200.0).abs() < 1e-6);
541 }
542
543 #[test]
544 fn deserialises_with_explicit_fields() {
545 let json = r#"{
546 "enabled":false,"density":0.12,"color":[0.5,0.6,0.7],
547 "height_falloff":0.3,"height_reference":1.5,
548 "max_distance":80.0,"phase_g":0.7,"ambient":0.25
549 }"#;
550 let f: VolumetricFog = serde_json::from_str(json).unwrap();
551 assert!(!f.enabled);
552 assert_eq!(f.color, [0.5, 0.6, 0.7]);
553 assert!((f.phase_g - 0.7).abs() < 1e-6);
554 }
555
556 #[test]
557 fn from_args_clamps_invalid_inputs() {
558 let a = VolumetricFog {
559 density: -1.0,
560 height_falloff: -0.4,
561 ambient: -2.0,
562 max_distance: -1.0,
563 phase_g: 1.4,
564 ..Default::default()
565 };
566 let n = super::super::volumetric_fog(a);
567 assert_eq!(n.density, 0.0);
568 assert_eq!(n.height_falloff, 0.0);
569 assert_eq!(n.ambient, 0.0);
570 assert!(n.max_distance > 0.0);
571 assert!(n.phase_g <= 0.95 && n.phase_g > 0.0);
572 }
573
574 #[test]
575 fn from_args_passes_through_valid_inputs() {
576 let a = VolumetricFog {
577 density: 0.08,
578 phase_g: -0.3,
579 ..Default::default()
580 };
581 let n = super::super::volumetric_fog(a);
582 assert!((n.density - 0.08).abs() < 1e-6);
583 assert!((n.phase_g - (-0.3)).abs() < 1e-6);
584 }
585 }
586
587 mod instanced_prop {
588 use super::*;
589 use crate::components::{InstanceTransform, InstancedPropGeometry};
590 use crate::ecs::asset_id::AssetId;
591
592 fn empty() -> InstancedProp {
593 InstancedProp {
594 asset_id: AssetId::default(),
595 mesh: None,
596 material: None,
597 texture: None,
598 instances: Vec::new(),
599 cull_distance: 0.0,
600 }
601 }
602
603 #[test]
604 fn instance_model_matrix_default_is_identity() {
605 let mut p = empty();
606 p.instances.push(InstanceTransform::default());
607 let m = p.instance_model_matrix(0).unwrap();
608 assert_eq!(m[3], [0.0, 0.0, 0.0, 1.0]);
609 assert!((m[0][0] - 1.0).abs() < 1e-5);
610 assert!((m[1][1] - 1.0).abs() < 1e-5);
611 assert!((m[2][2] - 1.0).abs() < 1e-5);
612 }
613
614 #[test]
615 fn instance_model_matrix_translates() {
616 let mut p = empty();
617 p.instances.push(InstanceTransform {
618 position: [5.0, -2.0, 3.0],
619 ..InstanceTransform::default()
620 });
621 let m = p.instance_model_matrix(0).unwrap();
622 assert_eq!(m[3], [5.0, -2.0, 3.0, 1.0]);
623 }
624
625 #[test]
626 fn instance_model_matrix_scales() {
627 let mut p = empty();
628 p.instances.push(InstanceTransform {
629 scale: [2.0, 3.0, 4.0],
630 ..InstanceTransform::default()
631 });
632 let m = p.instance_model_matrix(0).unwrap();
633 assert!((m[0][0] - 2.0).abs() < 1e-5);
635 assert!((m[1][1] - 3.0).abs() < 1e-5);
636 assert!((m[2][2] - 4.0).abs() < 1e-5);
637 }
638
639 #[test]
640 fn instance_model_matrix_out_of_range_returns_none() {
641 let p = empty();
642 assert!(p.instance_model_matrix(0).is_none());
643 }
644
645 #[test]
646 fn from_args_clamps_negative_cull_distance() {
647 let args = InstancedProp {
648 cull_distance: -5.0,
649 ..InstancedProp::default()
650 };
651 let p = super::super::instanced_prop(args);
652 assert_eq!(p.cull_distance, 0.0);
653 }
654 }
655
656 mod sdf_volume {
657 use super::*;
658 use crate::components::sdf_volume::{SDF_MAX_STEPS_CEILING, SDF_MAX_STEPS_FLOOR};
659
660 fn platform_ext() -> &'static str {
664 crate::platform::Platform::current().key()
665 }
666
667 #[test]
668 fn clamps_steps() {
669 let mut a = SdfVolume {
670 max_steps: 1,
671 ..Default::default()
672 };
673 let fixed = super::super::sdf_volume(a.clone());
674 assert_eq!(fixed.max_steps, SDF_MAX_STEPS_FLOOR);
675
676 a.max_steps = 9999;
677 let fixed = super::super::sdf_volume(a);
678 assert_eq!(fixed.max_steps, SDF_MAX_STEPS_CEILING);
679 }
680
681 #[test]
682 fn repairs_bad_extent() {
683 let a = SdfVolume {
684 extent: [0.0, -1.0, f32::NAN],
685 ..Default::default()
686 };
687 let fixed = super::super::sdf_volume(a);
688 assert_eq!(fixed.extent, [1.0, 1.0, 1.0]);
689 }
690
691 #[test]
692 fn repairs_bad_gradient_and_distance() {
693 let a = SdfVolume {
694 max_gradient: -0.5,
695 max_distance: f32::NAN,
696 ..Default::default()
697 };
698 let fixed = super::super::sdf_volume(a);
699 assert_eq!(fixed.max_gradient, 1.0);
700 assert_eq!(fixed.max_distance, 0.1);
701 }
702
703 #[test]
704 fn collapses_map_to_current_backend() {
705 let mut map = std::collections::BTreeMap::new();
711 map.insert("metal".to_string(), "shaders/blob.metal".to_string());
712 map.insert("hlsl".to_string(), "shaders/blob.hlsl".to_string());
713 map.insert("glsl".to_string(), "shaders/blob.glsl".to_string());
714 let a = SdfVolume {
715 fragment_shaders: Some(map),
716 ..Default::default()
717 };
718 let resolved = super::super::sdf_volume(a);
719 assert_eq!(
720 resolved.fragment_shader,
721 format!("shaders/blob.{}", platform_ext())
722 );
723 }
724
725 #[test]
726 fn volumetric_forces_cast_shadows_off() {
727 let a = SdfVolume {
728 volumetric: true,
729 cast_shadows: true,
730 ..Default::default()
731 };
732 let fixed = super::super::sdf_volume(a);
733 assert!(fixed.volumetric);
734 assert!(
735 !fixed.cast_shadows,
736 "volumetric SDFs are translucent and must not cast hard shadows"
737 );
738 }
739
740 #[test]
741 fn roundtrip_through_args() {
742 let mut v = SdfVolume {
743 centre: [1.0, 2.0, 3.0],
744 extent: [4.0, 5.0, 6.0],
745 fragment_shader: "shaders/foo.metal".to_string(),
746 ..Default::default()
747 };
748 v.params[7] = 0.42;
749 let json = serde_json::to_value(v.clone()).expect("serialises");
750 let back: SdfVolume = serde_json::from_value(json).expect("deserialises");
751 let back = super::super::sdf_volume(back);
752 assert_eq!(back.centre, [1.0, 2.0, 3.0]);
753 assert_eq!(back.extent, [4.0, 5.0, 6.0]);
754 assert_eq!(back.fragment_shader, "shaders/foo.metal");
755 assert_eq!(back.params[7], 0.42);
756 }
757 }
758}