1use crate::math::{Fix128, Vec3Fix};
29
30use std::fmt::Write as FmtWrite;
31use std::io::{Read, Write};
32
33#[derive(Clone, Debug, PartialEq, Eq)]
43#[non_exhaustive]
44pub struct PhysicsScene {
45 pub bodies: Vec<SerializedBody>,
47 pub joints: Vec<SerializedJoint>,
49 pub config: PhysicsConfig,
51 pub version: u32,
53}
54
55impl PhysicsScene {
56 #[must_use]
71 pub fn new(
72 bodies: Vec<SerializedBody>,
73 joints: Vec<SerializedJoint>,
74 config: PhysicsConfig,
75 version: u32,
76 ) -> Self {
77 Self {
78 bodies,
79 joints,
80 config,
81 version,
82 }
83 }
84}
85
86#[derive(Clone, Debug, PartialEq, Eq)]
91pub struct SerializedBody {
92 pub position: [i64; 6],
94 pub velocity: [i64; 6],
96 pub rotation: [i64; 8],
98 pub mass: [i64; 2],
100 pub body_type: u8,
102}
103
104#[derive(Clone, Debug, PartialEq, Eq)]
106pub struct SerializedJoint {
107 pub body_a: u32,
109 pub body_b: u32,
111 pub joint_type: u8,
114 pub anchor_a: [i64; 6],
116 pub anchor_b: [i64; 6],
118}
119
120#[derive(Clone, Debug, PartialEq, Eq)]
125#[non_exhaustive]
126pub struct PhysicsConfig {
127 pub substeps: u32,
129 pub iterations: u32,
131 pub gravity: [i64; 6],
133 pub damping: [i64; 2],
135}
136
137impl PhysicsConfig {
138 #[must_use]
145 pub const fn new(substeps: u32, iterations: u32, gravity: [i64; 6], damping: [i64; 2]) -> Self {
146 Self {
147 substeps,
148 iterations,
149 gravity,
150 damping,
151 }
152 }
153}
154
155impl Default for PhysicsConfig {
156 fn default() -> Self {
157 let grav = vec3fix_to_raw(Vec3Fix::new(
158 Fix128::ZERO,
159 Fix128::from_int(-10),
160 Fix128::ZERO,
161 ));
162 let damp = fix128_to_raw(Fix128::from_ratio(99, 100));
163 Self {
164 substeps: 8,
165 iterations: 4,
166 gravity: grav,
167 damping: damp,
168 }
169 }
170}
171
172const MAGIC: &[u8; 6] = b"APHYS\0";
174
175const CURRENT_VERSION: u32 = 1;
177
178const MAX_PREALLOC: usize = 4096;
180
181pub const CURRENT_SCENE_VERSION: u32 = CURRENT_VERSION;
183
184const fn fix128_to_raw(v: Fix128) -> [i64; 2] {
189 [v.hi, v.lo as i64]
190}
191
192#[cfg(test)]
193const fn raw_to_fix128(r: &[i64; 2]) -> Fix128 {
194 Fix128 {
195 hi: r[0],
196 lo: r[1] as u64,
197 }
198}
199
200const fn vec3fix_to_raw(v: Vec3Fix) -> [i64; 6] {
201 [
202 v.x.hi,
203 v.x.lo as i64,
204 v.y.hi,
205 v.y.lo as i64,
206 v.z.hi,
207 v.z.lo as i64,
208 ]
209}
210
211#[cfg(test)]
212const fn raw_to_vec3fix(r: &[i64; 6]) -> Vec3Fix {
213 Vec3Fix::new(
214 Fix128 {
215 hi: r[0],
216 lo: r[1] as u64,
217 },
218 Fix128 {
219 hi: r[2],
220 lo: r[3] as u64,
221 },
222 Fix128 {
223 hi: r[4],
224 lo: r[5] as u64,
225 },
226 )
227}
228
229fn write_u32(w: &mut dyn Write, v: u32) -> std::io::Result<()> {
234 w.write_all(&v.to_le_bytes())
235}
236
237fn write_u8(w: &mut dyn Write, v: u8) -> std::io::Result<()> {
238 w.write_all(&[v])
239}
240
241fn write_i64(w: &mut dyn Write, v: i64) -> std::io::Result<()> {
242 w.write_all(&v.to_le_bytes())
243}
244
245fn read_u32(r: &mut dyn Read) -> std::io::Result<u32> {
246 let mut buf = [0u8; 4];
247 r.read_exact(&mut buf)?;
248 Ok(u32::from_le_bytes(buf))
249}
250
251fn read_u8(r: &mut dyn Read) -> std::io::Result<u8> {
252 let mut buf = [0u8; 1];
253 r.read_exact(&mut buf)?;
254 Ok(buf[0])
255}
256
257fn read_i64(r: &mut dyn Read) -> std::io::Result<i64> {
258 let mut buf = [0u8; 8];
259 r.read_exact(&mut buf)?;
260 Ok(i64::from_le_bytes(buf))
261}
262
263fn write_i64_array(w: &mut dyn Write, arr: &[i64]) -> std::io::Result<()> {
264 for &v in arr {
265 write_i64(w, v)?;
266 }
267 Ok(())
268}
269
270fn read_i64_array<const N: usize>(r: &mut dyn Read) -> std::io::Result<[i64; N]> {
271 let mut arr = [0i64; N];
272 for item in &mut arr {
273 *item = read_i64(r)?;
274 }
275 Ok(arr)
276}
277
278pub fn save_scene(scene: &PhysicsScene, path: &std::path::Path) -> std::io::Result<()> {
290 let mut file = std::fs::File::create(path)?;
291 write_scene_binary(&mut file, scene)
292}
293
294pub fn load_scene(path: &std::path::Path) -> std::io::Result<PhysicsScene> {
300 let mut file = std::fs::File::open(path)?;
301 read_scene_binary(&mut file)
302}
303
304fn write_scene_binary(w: &mut dyn Write, scene: &PhysicsScene) -> std::io::Result<()> {
305 w.write_all(MAGIC)?;
307
308 write_u32(w, scene.version)?;
310
311 write_u32(w, scene.bodies.len() as u32)?;
313 write_u32(w, scene.joints.len() as u32)?;
314
315 write_u32(w, scene.config.substeps)?;
317 write_u32(w, scene.config.iterations)?;
318 write_i64_array(w, &scene.config.gravity)?;
319 write_i64_array(w, &scene.config.damping)?;
320
321 for body in &scene.bodies {
323 write_i64_array(w, &body.position)?;
324 write_i64_array(w, &body.velocity)?;
325 write_i64_array(w, &body.rotation)?;
326 write_i64_array(w, &body.mass)?;
327 write_u8(w, body.body_type)?;
328 }
329
330 for joint in &scene.joints {
332 write_u32(w, joint.body_a)?;
333 write_u32(w, joint.body_b)?;
334 write_u8(w, joint.joint_type)?;
335 write_i64_array(w, &joint.anchor_a)?;
336 write_i64_array(w, &joint.anchor_b)?;
337 }
338
339 Ok(())
340}
341
342fn read_scene_binary(r: &mut dyn Read) -> std::io::Result<PhysicsScene> {
343 let mut magic = [0u8; 6];
345 r.read_exact(&mut magic)?;
346 if &magic != MAGIC {
347 return Err(std::io::Error::new(
348 std::io::ErrorKind::InvalidData,
349 "Invalid magic bytes: expected APHYS\\0",
350 ));
351 }
352
353 let version = read_u32(r)?;
355
356 let body_count = read_u32(r)? as usize;
358 let joint_count = read_u32(r)? as usize;
359
360 let substeps = read_u32(r)?;
362 let iterations = read_u32(r)?;
363 let gravity = read_i64_array::<6>(r)?;
364 let damping = read_i64_array::<2>(r)?;
365
366 let config = PhysicsConfig {
367 substeps,
368 iterations,
369 gravity,
370 damping,
371 };
372
373 let mut bodies = Vec::with_capacity(body_count.min(MAX_PREALLOC));
377 for _ in 0..body_count {
378 let position = read_i64_array::<6>(r)?;
379 let velocity = read_i64_array::<6>(r)?;
380 let rotation = read_i64_array::<8>(r)?;
381 let mass = read_i64_array::<2>(r)?;
382 let body_type = read_u8(r)?;
383 bodies.push(SerializedBody {
384 position,
385 velocity,
386 rotation,
387 mass,
388 body_type,
389 });
390 }
391
392 let mut joints = Vec::with_capacity(joint_count.min(MAX_PREALLOC));
394 for _ in 0..joint_count {
395 let body_a = read_u32(r)?;
396 let body_b = read_u32(r)?;
397 let joint_type = read_u8(r)?;
398 let anchor_a = read_i64_array::<6>(r)?;
399 let anchor_b = read_i64_array::<6>(r)?;
400 joints.push(SerializedJoint {
401 body_a,
402 body_b,
403 joint_type,
404 anchor_a,
405 anchor_b,
406 });
407 }
408
409 Ok(PhysicsScene {
410 bodies,
411 joints,
412 config,
413 version,
414 })
415}
416
417pub fn save_scene_json(scene: &PhysicsScene, path: &std::path::Path) -> std::io::Result<()> {
427 let json = scene_to_json(scene);
428 std::fs::write(path, json)
429}
430
431pub fn load_scene_json(path: &std::path::Path) -> std::io::Result<PhysicsScene> {
437 let json = std::fs::read_to_string(path)?;
438 parse_scene_json(&json).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
439}
440
441fn i64_array_to_json(arr: &[i64]) -> String {
442 let items: Vec<String> = arr.iter().map(|v| format!("{v}")).collect();
443 format!("[{}]", items.join(", "))
444}
445
446fn scene_to_json(scene: &PhysicsScene) -> String {
447 fn write_json(s: &mut String, scene: &PhysicsScene) -> core::fmt::Result {
448 s.push_str("{\n");
449 writeln!(s, " \"version\": {},", scene.version)?;
450
451 s.push_str(" \"config\": {\n");
453 writeln!(s, " \"substeps\": {},", scene.config.substeps)?;
454 writeln!(s, " \"iterations\": {},", scene.config.iterations)?;
455 writeln!(
456 s,
457 " \"gravity\": {},",
458 i64_array_to_json(&scene.config.gravity)
459 )?;
460 writeln!(
461 s,
462 " \"damping\": {}",
463 i64_array_to_json(&scene.config.damping)
464 )?;
465 s.push_str(" },\n");
466
467 s.push_str(" \"bodies\": [\n");
469 for (i, body) in scene.bodies.iter().enumerate() {
470 s.push_str(" {\n");
471 writeln!(
472 s,
473 " \"position\": {},",
474 i64_array_to_json(&body.position)
475 )?;
476 writeln!(
477 s,
478 " \"velocity\": {},",
479 i64_array_to_json(&body.velocity)
480 )?;
481 writeln!(
482 s,
483 " \"rotation\": {},",
484 i64_array_to_json(&body.rotation)
485 )?;
486 writeln!(s, " \"mass\": {},", i64_array_to_json(&body.mass))?;
487 writeln!(s, " \"body_type\": {}", body.body_type)?;
488 if i < scene.bodies.len() - 1 {
489 s.push_str(" },\n");
490 } else {
491 s.push_str(" }\n");
492 }
493 }
494 s.push_str(" ],\n");
495
496 s.push_str(" \"joints\": [\n");
498 for (i, joint) in scene.joints.iter().enumerate() {
499 s.push_str(" {\n");
500 writeln!(s, " \"body_a\": {},", joint.body_a)?;
501 writeln!(s, " \"body_b\": {},", joint.body_b)?;
502 writeln!(s, " \"joint_type\": {},", joint.joint_type)?;
503 writeln!(
504 s,
505 " \"anchor_a\": {},",
506 i64_array_to_json(&joint.anchor_a)
507 )?;
508 writeln!(
509 s,
510 " \"anchor_b\": {}",
511 i64_array_to_json(&joint.anchor_b)
512 )?;
513 if i < scene.joints.len() - 1 {
514 s.push_str(" },\n");
515 } else {
516 s.push_str(" }\n");
517 }
518 }
519 s.push_str(" ]\n");
520
521 s.push_str("}\n");
522 Ok(())
523 }
524
525 let mut s = String::new();
526 let _ = write_json(&mut s, scene);
528 s
529}
530
531fn parse_scene_json(json: &str) -> Result<PhysicsScene, String> {
536 let json = json.trim();
537 if !json.starts_with('{') || !json.ends_with('}') {
538 return Err("Expected JSON object".into());
539 }
540
541 let version = extract_u32(json, "version")?.unwrap_or(CURRENT_VERSION);
542
543 let config_str = extract_object(json, "config").unwrap_or_default();
545 let substeps = extract_u32(&config_str, "substeps")?.unwrap_or(8);
546 let iterations = extract_u32(&config_str, "iterations")?.unwrap_or(4);
547 let gravity = extract_i64_array(&config_str, "gravity", 6)?;
548 let damping = extract_i64_array(&config_str, "damping", 2)?;
549
550 let config = PhysicsConfig {
551 substeps,
552 iterations,
553 gravity: [
554 gravity[0], gravity[1], gravity[2], gravity[3], gravity[4], gravity[5],
555 ],
556 damping: [damping[0], damping[1]],
557 };
558
559 let bodies_str = extract_array(json, "bodies").unwrap_or_default();
561 let body_objects = split_array_objects(&bodies_str);
562 let mut bodies = Vec::new();
563 for obj in &body_objects {
564 let position_v = extract_i64_array(obj, "position", 6)?;
565 let velocity_v = extract_i64_array(obj, "velocity", 6)?;
566 let rotation_v = extract_i64_array(obj, "rotation", 8)?;
567 let mass_v = extract_i64_array(obj, "mass", 2)?;
568 let body_type = extract_u8(obj, "body_type")?.unwrap_or(0);
569
570 let mut position = [0i64; 6];
571 let mut velocity = [0i64; 6];
572 let mut rotation = [0i64; 8];
573 let mut mass = [0i64; 2];
574 position.copy_from_slice(&position_v);
575 velocity.copy_from_slice(&velocity_v);
576 rotation.copy_from_slice(&rotation_v);
577 mass.copy_from_slice(&mass_v);
578
579 bodies.push(SerializedBody {
580 position,
581 velocity,
582 rotation,
583 mass,
584 body_type,
585 });
586 }
587
588 let joints_str = extract_array(json, "joints").unwrap_or_default();
590 let joint_objects = split_array_objects(&joints_str);
591 let mut joints = Vec::new();
592 for obj in &joint_objects {
593 let body_a = extract_u32(obj, "body_a")?.unwrap_or(0);
594 let body_b = extract_u32(obj, "body_b")?.unwrap_or(0);
595 let joint_type = extract_u8(obj, "joint_type")?.unwrap_or(0);
596 let anchor_a_v = extract_i64_array(obj, "anchor_a", 6)?;
597 let anchor_b_v = extract_i64_array(obj, "anchor_b", 6)?;
598
599 let mut anchor_a = [0i64; 6];
600 let mut anchor_b = [0i64; 6];
601 anchor_a.copy_from_slice(&anchor_a_v);
602 anchor_b.copy_from_slice(&anchor_b_v);
603
604 joints.push(SerializedJoint {
605 body_a,
606 body_b,
607 joint_type,
608 anchor_a,
609 anchor_b,
610 });
611 }
612
613 Ok(PhysicsScene {
614 bodies,
615 joints,
616 config,
617 version,
618 })
619}
620
621fn extract_u32(json: &str, key: &str) -> Result<Option<u32>, String> {
632 let pattern = format!("\"{key}\"");
633 let Some(idx) = json.find(&pattern) else {
634 return Ok(None);
635 };
636 let rest = &json[idx + pattern.len()..];
637 let Some(colon) = rest.find(':') else {
638 return Ok(None);
639 };
640 let after_colon = rest[colon + 1..].trim_start();
641 let end = after_colon
643 .find([',', '}', ']', '\n', '\r'])
644 .unwrap_or(after_colon.len());
645 let token = after_colon[..end].trim();
646 token
647 .parse::<u32>()
648 .map(Some)
649 .map_err(|e| format!("Invalid value for {key}: {token:?} ({e})"))
650}
651
652fn extract_u8(json: &str, key: &str) -> Result<Option<u8>, String> {
654 match extract_u32(json, key)? {
655 None => Ok(None),
656 Some(v) => u8::try_from(v)
657 .map(Some)
658 .map_err(|_| format!("Value for {key} does not fit in u8: {v}")),
659 }
660}
661
662fn extract_object(json: &str, key: &str) -> Option<String> {
663 let pattern = format!("\"{key}\"");
664 let idx = json.find(&pattern)?;
665 let rest = &json[idx + pattern.len()..];
666 let brace = rest.find('{')?;
667 let start = brace;
668 let mut depth = 0i32;
669 let bytes = rest.as_bytes();
670 for (i, &b) in bytes[start..].iter().enumerate() {
671 if b == b'{' {
672 depth += 1;
673 }
674 if b == b'}' {
675 depth -= 1;
676 }
677 if depth == 0 {
678 return Some(rest[start..=(start + i)].to_string());
679 }
680 }
681 None
682}
683
684fn extract_array(json: &str, key: &str) -> Option<String> {
685 let pattern = format!("\"{key}\"");
686 let idx = json.find(&pattern)?;
687 let rest = &json[idx + pattern.len()..];
688 let colon = rest.find(':')?;
690 let after_colon = &rest[colon + 1..];
691 let bracket = after_colon.find('[')?;
692 let start = bracket;
693 let mut depth = 0i32;
694 let bytes = after_colon.as_bytes();
695 for (i, &b) in bytes[start..].iter().enumerate() {
696 if b == b'[' {
697 depth += 1;
698 }
699 if b == b']' {
700 depth -= 1;
701 }
702 if depth == 0 {
703 return Some(after_colon[start..=(start + i)].to_string());
704 }
705 }
706 None
707}
708
709fn extract_i64_array(json: &str, key: &str, expected_len: usize) -> Result<Vec<i64>, String> {
710 let arr_str = extract_array(json, key).ok_or_else(|| format!("Missing key: {key}"))?;
711 let inner = arr_str.trim_start_matches('[').trim_end_matches(']');
713 let values: Result<Vec<i64>, _> = inner.split(',').map(|s| s.trim().parse::<i64>()).collect();
714 let values = values.map_err(|e| format!("Parse error for {key}: {e}"))?;
715 if values.len() != expected_len {
716 return Err(format!(
717 "Expected {} values for {}, got {}",
718 expected_len,
719 key,
720 values.len()
721 ));
722 }
723 Ok(values)
724}
725
726fn split_array_objects(arr_str: &str) -> Vec<String> {
727 let inner = arr_str.trim_start_matches('[').trim_end_matches(']').trim();
728 if inner.is_empty() {
729 return Vec::new();
730 }
731
732 let mut objects = Vec::new();
733 let mut depth = 0i32;
734 let mut start = 0;
735 let bytes = inner.as_bytes();
736
737 for (i, &b) in bytes.iter().enumerate() {
738 if b == b'{' {
739 if depth == 0 {
740 start = i;
741 }
742 depth += 1;
743 }
744 if b == b'}' {
745 depth -= 1;
746 if depth == 0 {
747 objects.push(inner[start..=i].to_string());
748 }
749 }
750 }
751
752 objects
753}
754
755#[cfg(test)]
760mod tests {
761 use super::*;
762
763 fn make_test_scene() -> PhysicsScene {
764 PhysicsScene {
765 version: CURRENT_VERSION,
766 config: PhysicsConfig::default(),
767 bodies: vec![
768 SerializedBody {
769 position: [0, 0, 10, 0, 0, 0],
770 velocity: [0, 0, 0, 0, 0, 0],
771 rotation: [0, 0, 0, 0, 0, 0, 1, 0],
772 mass: [1, 0],
773 body_type: 0,
774 },
775 SerializedBody {
776 position: [5, 0, 0, 0, -3, 0],
777 velocity: [1, 0, -1, 0, 0, 0],
778 rotation: [0, 0, 0, 0, 0, 0, 1, 0],
779 mass: [2, 0],
780 body_type: 1,
781 },
782 ],
783 joints: vec![SerializedJoint {
784 body_a: 0,
785 body_b: 1,
786 joint_type: 0,
787 anchor_a: [0, 0, 0, 0, 0, 0],
788 anchor_b: [1, 0, 0, 0, 0, 0],
789 }],
790 }
791 }
792
793 #[test]
794 fn test_binary_roundtrip() {
795 let scene = make_test_scene();
796 let dir = tempfile::tempdir().unwrap();
797 let path = dir.path().join("test.aphys");
798
799 save_scene(&scene, &path).unwrap();
800 let loaded = load_scene(&path).unwrap();
801
802 assert_eq!(loaded.version, scene.version);
803 assert_eq!(loaded.bodies.len(), scene.bodies.len());
804 assert_eq!(loaded.joints.len(), scene.joints.len());
805 assert_eq!(loaded.config.substeps, scene.config.substeps);
806 }
807
808 #[test]
809 fn test_binary_body_data_preserved() {
810 let scene = make_test_scene();
811 let dir = tempfile::tempdir().unwrap();
812 let path = dir.path().join("test2.aphys");
813
814 save_scene(&scene, &path).unwrap();
815 let loaded = load_scene(&path).unwrap();
816
817 assert_eq!(loaded.bodies[0].position, scene.bodies[0].position);
818 assert_eq!(loaded.bodies[0].mass, scene.bodies[0].mass);
819 assert_eq!(loaded.bodies[0].body_type, scene.bodies[0].body_type);
820 assert_eq!(loaded.bodies[1].velocity, scene.bodies[1].velocity);
821 }
822
823 #[test]
824 fn test_binary_joint_data_preserved() {
825 let scene = make_test_scene();
826 let dir = tempfile::tempdir().unwrap();
827 let path = dir.path().join("test3.aphys");
828
829 save_scene(&scene, &path).unwrap();
830 let loaded = load_scene(&path).unwrap();
831
832 assert_eq!(loaded.joints[0].body_a, 0);
833 assert_eq!(loaded.joints[0].body_b, 1);
834 assert_eq!(loaded.joints[0].joint_type, 0);
835 assert_eq!(loaded.joints[0].anchor_b, scene.joints[0].anchor_b);
836 }
837
838 #[test]
839 fn test_binary_invalid_magic() {
840 let dir = tempfile::tempdir().unwrap();
841 let path = dir.path().join("bad.aphys");
842 std::fs::write(&path, b"BADMG\0").unwrap();
843
844 let result = load_scene(&path);
845 assert!(result.is_err());
846 }
847
848 #[test]
849 fn test_json_roundtrip() {
850 let scene = make_test_scene();
851 let dir = tempfile::tempdir().unwrap();
852 let path = dir.path().join("test.json");
853
854 save_scene_json(&scene, &path).unwrap();
855 let loaded = load_scene_json(&path).unwrap();
856
857 assert_eq!(loaded.version, scene.version);
858 assert_eq!(loaded.bodies.len(), scene.bodies.len());
859 assert_eq!(loaded.joints.len(), scene.joints.len());
860 }
861
862 #[test]
863 fn test_json_body_data_preserved() {
864 let scene = make_test_scene();
865 let dir = tempfile::tempdir().unwrap();
866 let path = dir.path().join("test2.json");
867
868 save_scene_json(&scene, &path).unwrap();
869 let loaded = load_scene_json(&path).unwrap();
870
871 assert_eq!(loaded.bodies[0].position, scene.bodies[0].position);
872 assert_eq!(loaded.bodies[1].velocity, scene.bodies[1].velocity);
873 assert_eq!(loaded.bodies[0].rotation, scene.bodies[0].rotation);
874 }
875
876 #[test]
877 fn test_json_config_preserved() {
878 let scene = make_test_scene();
879 let dir = tempfile::tempdir().unwrap();
880 let path = dir.path().join("test3.json");
881
882 save_scene_json(&scene, &path).unwrap();
883 let loaded = load_scene_json(&path).unwrap();
884
885 assert_eq!(loaded.config.substeps, scene.config.substeps);
886 assert_eq!(loaded.config.iterations, scene.config.iterations);
887 assert_eq!(loaded.config.gravity, scene.config.gravity);
888 assert_eq!(loaded.config.damping, scene.config.damping);
889 }
890
891 #[test]
892 fn test_json_empty_scene() {
893 let scene = PhysicsScene {
894 version: CURRENT_VERSION,
895 config: PhysicsConfig::default(),
896 bodies: vec![],
897 joints: vec![],
898 };
899 let dir = tempfile::tempdir().unwrap();
900 let path = dir.path().join("empty.json");
901
902 save_scene_json(&scene, &path).unwrap();
903 let loaded = load_scene_json(&path).unwrap();
904
905 assert!(loaded.bodies.is_empty());
906 assert!(loaded.joints.is_empty());
907 }
908
909 #[test]
910 fn test_binary_empty_scene() {
911 let scene = PhysicsScene {
912 version: CURRENT_VERSION,
913 config: PhysicsConfig::default(),
914 bodies: vec![],
915 joints: vec![],
916 };
917 let dir = tempfile::tempdir().unwrap();
918 let path = dir.path().join("empty.aphys");
919
920 save_scene(&scene, &path).unwrap();
921 let loaded = load_scene(&path).unwrap();
922
923 assert!(loaded.bodies.is_empty());
924 assert!(loaded.joints.is_empty());
925 }
926
927 #[test]
928 fn test_fix128_raw_roundtrip() {
929 let val = Fix128::from_ratio(355, 113); let raw = fix128_to_raw(val);
931 let restored = raw_to_fix128(&raw);
932 assert_eq!(val.hi, restored.hi);
933 assert_eq!(val.lo, restored.lo);
934 }
935
936 #[test]
937 fn test_vec3fix_raw_roundtrip() {
938 let val = Vec3Fix::new(Fix128::from_int(42), Fix128::from_ratio(-7, 3), Fix128::PI);
939 let raw = vec3fix_to_raw(val);
940 let restored = raw_to_vec3fix(&raw);
941 assert_eq!(val.x.hi, restored.x.hi);
942 assert_eq!(val.x.lo, restored.x.lo);
943 assert_eq!(val.y.hi, restored.y.hi);
944 assert_eq!(val.z.hi, restored.z.hi);
945 }
946
947 #[test]
948 fn test_binary_negative_values() {
949 let scene = PhysicsScene {
950 version: CURRENT_VERSION,
951 config: PhysicsConfig::default(),
952 bodies: vec![SerializedBody {
953 position: [-100, 0, -200, 0, -300, 0],
954 velocity: [-1, 0, -2, 0, -3, 0],
955 rotation: [0, 0, 0, 0, 0, 0, 1, 0],
956 mass: [5, 0],
957 body_type: 2,
958 }],
959 joints: vec![],
960 };
961 let dir = tempfile::tempdir().unwrap();
962 let path = dir.path().join("neg.aphys");
963
964 save_scene(&scene, &path).unwrap();
965 let loaded = load_scene(&path).unwrap();
966
967 assert_eq!(loaded.bodies[0].position[0], -100);
968 assert_eq!(loaded.bodies[0].position[2], -200);
969 assert_eq!(loaded.bodies[0].body_type, 2);
970 }
971
972 #[test]
973 fn test_json_negative_values() {
974 let scene = PhysicsScene {
975 version: CURRENT_VERSION,
976 config: PhysicsConfig::default(),
977 bodies: vec![SerializedBody {
978 position: [-10, 0, -20, 0, -30, 0],
979 velocity: [0, 0, 0, 0, 0, 0],
980 rotation: [0, 0, 0, 0, 0, 0, 1, 0],
981 mass: [1, 0],
982 body_type: 0,
983 }],
984 joints: vec![],
985 };
986 let dir = tempfile::tempdir().unwrap();
987 let path = dir.path().join("neg.json");
988
989 save_scene_json(&scene, &path).unwrap();
990 let loaded = load_scene_json(&path).unwrap();
991
992 assert_eq!(loaded.bodies[0].position[0], -10);
993 assert_eq!(loaded.bodies[0].position[2], -20);
994 }
995
996 #[test]
997 fn test_multiple_joints_roundtrip() {
998 let scene = PhysicsScene {
999 version: CURRENT_VERSION,
1000 config: PhysicsConfig::default(),
1001 bodies: vec![SerializedBody {
1002 position: [0; 6],
1003 velocity: [0; 6],
1004 rotation: [0, 0, 0, 0, 0, 0, 1, 0],
1005 mass: [1, 0],
1006 body_type: 0,
1007 }],
1008 joints: vec![
1009 SerializedJoint {
1010 body_a: 0,
1011 body_b: 0,
1012 joint_type: 1,
1013 anchor_a: [1, 0, 2, 0, 3, 0],
1014 anchor_b: [4, 0, 5, 0, 6, 0],
1015 },
1016 SerializedJoint {
1017 body_a: 0,
1018 body_b: 0,
1019 joint_type: 4,
1020 anchor_a: [7, 0, 8, 0, 9, 0],
1021 anchor_b: [10, 0, 11, 0, 12, 0],
1022 },
1023 ],
1024 };
1025 let dir = tempfile::tempdir().unwrap();
1026 let path = dir.path().join("multi_joint.aphys");
1027
1028 save_scene(&scene, &path).unwrap();
1029 let loaded = load_scene(&path).unwrap();
1030
1031 assert_eq!(loaded.joints.len(), 2);
1032 assert_eq!(loaded.joints[0].joint_type, 1);
1033 assert_eq!(loaded.joints[1].joint_type, 4);
1034 assert_eq!(loaded.joints[1].anchor_a[0], 7);
1035 }
1036
1037 #[test]
1038 fn test_scene_to_json_format() {
1039 let scene = make_test_scene();
1040 let json = scene_to_json(&scene);
1041 assert!(json.contains("\"version\""));
1042 assert!(json.contains("\"bodies\""));
1043 assert!(json.contains("\"joints\""));
1044 assert!(json.contains("\"config\""));
1045 }
1046}