diff --git a/src/json.rs b/src/json.rs index 960e217a..cf8e1af0 100644 --- a/src/json.rs +++ b/src/json.rs @@ -1053,16 +1053,16 @@ mod tests { // Zero and negative zero assert_eq!(format_f32(0.0), "0"); - assert_eq!(format_f32(-0.0), "0"); + assert_eq!(format_f32(-0.0), "-0"); // Rounding to 6 significant digits assert_eq!(format_f32(0.5), "0.5"); assert_eq!(format_f32(1.0 / 3.0), "0.333333"); - // Small values clamped to zero - assert_eq!(format_f32(1e-10), "0"); - assert_eq!(format_f32(-1e-10), "0"); - assert_eq!(format_f32(4.9e-7), "0"); + // Small values preserved + assert_eq!(format_f32(1e-10).parse::().unwrap(), 1e-10_f32); + assert_eq!(format_f32(-1e-10).parse::().unwrap(), -1e-10_f32); + assert_eq!(format_f32(4.9e-7).parse::().unwrap(), 4.9e-7_f32); // Scientific notation for large values assert_eq!(format_f32(1e20), "1e20"); @@ -1087,16 +1087,16 @@ mod tests { // Zero and negative zero assert_eq!(format_f64(0.0), "0"); - assert_eq!(format_f64(-0.0), "0"); + assert_eq!(format_f64(-0.0), "-0"); - // Rounding to 15 significant digits + // Full precision for roundtrip fidelity assert_eq!(format_f64(0.5), "0.5"); - assert_eq!(format_f64(1.0 / 3.0), "0.333333333333333"); + assert_eq!(format_f64(1.0 / 3.0), "0.3333333333333333"); - // Small values clamped to zero - assert_eq!(format_f64(1e-100), "0"); - assert_eq!(format_f64(-1e-100), "0"); - assert_eq!(format_f64(4.9e-16), "0"); + // Small values preserved + assert_eq!(format_f64(1e-100).parse::().unwrap(), 1e-100_f64); + assert_eq!(format_f64(-1e-100).parse::().unwrap(), -1e-100_f64); + assert_eq!(format_f64(4.9e-16).parse::().unwrap(), 4.9e-16_f64); // Scientific notation for large values assert_eq!(format_f64(1e47), "1e47"); @@ -1749,14 +1749,8 @@ mod tests { let serialized = to_vec_pretty_sorted(&original).unwrap(); let json_str = String::from_utf8(serialized).unwrap(); - // Very small numbers are clamped to zero by formatter policy - assert!( - json_str.contains(": 0"), - "Very small number should clamp to zero: {json_str}" - ); - let deserialized: Data = from_str(&json_str).unwrap(); - assert_eq!(deserialized.value, 0.0); + assert_eq!(deserialized.value, original.value); } #[test] @@ -1786,7 +1780,7 @@ mod tests { let json_str = String::from_utf8(serialized).unwrap(); let deserialized: Data = from_str(&json_str).unwrap(); - assert_eq!(deserialized.value, 0.0); + assert_eq!(deserialized.value, original.value); } // Test the exact boundaries where we switch to scientific notation @@ -2527,7 +2521,7 @@ mod tests { assert_eq!(deserialized.class_name, "NumberValue"); assert_eq!(*deserialized.properties.get("Value").unwrap(), 1e47); assert_eq!(*deserialized.properties.get("Normal").unwrap(), 42.5); - assert_eq!(*deserialized.properties.get("Tiny").unwrap(), 0.0); + assert_eq!(*deserialized.properties.get("Tiny").unwrap(), 1e-50); } #[test] @@ -2730,8 +2724,8 @@ mod tests { let json_str = String::from_utf8(serialized).unwrap(); let deserialized: Data = from_str(&json_str).unwrap(); - assert_eq!(deserialized.epsilon, 0.0); - assert_eq!(deserialized.min_positive, 0.0); + assert_eq!(deserialized.epsilon, original.epsilon); + assert_eq!(deserialized.min_positive, original.min_positive); } } @@ -3184,7 +3178,7 @@ mod tests { let json_str = String::from_utf8(serialized).unwrap(); let deserialized: Data = from_str(&json_str).unwrap(); - assert_eq!(deserialized.tiny, 0.0); + assert_eq!(deserialized.tiny, original.tiny); } } @@ -3943,7 +3937,7 @@ No \\n's!", ); assert!(deserialized.nan.is_nan()); assert_eq!(deserialized.large, 1e47); - assert_eq!(deserialized.small, 0.0); + assert_eq!(deserialized.small, 1e-47); } #[test] @@ -4891,8 +4885,8 @@ No \\n's!", assert!(deserialized.c.is_nan()); assert_eq!(deserialized.d, f64::MAX); assert_eq!(deserialized.e, f64::MIN); - assert_eq!(deserialized.f, 0.0); - assert_eq!(deserialized.g, 0.0); + assert_eq!(deserialized.f, original.f); + assert_eq!(deserialized.g, original.g); assert_eq!(deserialized.h, 0.0); assert_eq!(deserialized.i, original.i); } diff --git a/src/resolution.rs b/src/resolution.rs index 142063ac..bb590d47 100644 --- a/src/resolution.rs +++ b/src/resolution.rs @@ -2,8 +2,10 @@ use std::borrow::Borrow; use anyhow::{bail, format_err}; use rbx_dom_weak::types::{ - Attributes, CFrame, Color3, Content, ContentId, ContentType, Enum, Font, MaterialColors, - Matrix3, Tags, Variant, VariantType, Vector2, Vector3, + Attributes, CFrame, Color3, ColorSequence, ColorSequenceKeypoint, Content, ContentId, + ContentType, CustomPhysicalProperties, Enum, Font, MaterialColors, Matrix3, NumberSequence, + NumberSequenceKeypoint, PhysicalProperties, Ray, Rect, Region3, Tags, UDim, UDim2, Variant, + VariantType, Vector2, Vector3, }; use rbx_reflection::{DataType, PropertyDescriptor}; use serde::{Deserialize, Serialize}; @@ -78,23 +80,150 @@ impl UnresolvedValue { _ => return Self::FullyQualified(variant), }, Variant::ContentId(content) => AmbiguousValue::String(content.into_string()), - Variant::Vector2(vector) => AmbiguousValue::Array2([vector.x, vector.y]), - Variant::Vector3(vector) => AmbiguousValue::Array3([vector.x, vector.y, vector.z]), - Variant::Color3(color) => AmbiguousValue::Array3([color.r, color.g, color.b]), - Variant::CFrame(cf) => AmbiguousValue::Array12([ - cf.position.x, - cf.position.y, - cf.position.z, - cf.orientation.x.x, - cf.orientation.x.y, - cf.orientation.x.z, - cf.orientation.y.x, - cf.orientation.y.y, - cf.orientation.y.z, - cf.orientation.z.x, - cf.orientation.z.y, - cf.orientation.z.z, + Variant::Vector2(vector) => { + AmbiguousValue::Array2([cleanup_f32(vector.x), cleanup_f32(vector.y)]) + } + Variant::Vector3(vector) => AmbiguousValue::Array3([ + cleanup_f32(vector.x), + cleanup_f32(vector.y), + cleanup_f32(vector.z), ]), + Variant::Color3(color) => AmbiguousValue::Array3([ + cleanup_f32(color.r), + cleanup_f32(color.g), + cleanup_f32(color.b), + ]), + Variant::CFrame(cf) => AmbiguousValue::Array12([ + cleanup_f32(cf.position.x), + cleanup_f32(cf.position.y), + cleanup_f32(cf.position.z), + cleanup_f32(cf.orientation.x.x), + cleanup_f32(cf.orientation.x.y), + cleanup_f32(cf.orientation.x.z), + cleanup_f32(cf.orientation.y.x), + cleanup_f32(cf.orientation.y.y), + cleanup_f32(cf.orientation.y.z), + cleanup_f32(cf.orientation.z.x), + cleanup_f32(cf.orientation.z.y), + cleanup_f32(cf.orientation.z.z), + ]), + Variant::UDim(udim) => { + return Self::FullyQualified(Variant::UDim(UDim::new( + cleanup_f32(udim.scale), + udim.offset, + ))); + } + Variant::UDim2(udim2) => { + return Self::FullyQualified(Variant::UDim2(UDim2::new( + UDim::new(cleanup_f32(udim2.x.scale), udim2.x.offset), + UDim::new(cleanup_f32(udim2.y.scale), udim2.y.offset), + ))); + } + Variant::OptionalCFrame(Some(cf)) => { + return Self::FullyQualified(Variant::OptionalCFrame(Some(CFrame::new( + Vector3::new( + cleanup_f32(cf.position.x), + cleanup_f32(cf.position.y), + cleanup_f32(cf.position.z), + ), + Matrix3::new( + Vector3::new( + cleanup_f32(cf.orientation.x.x), + cleanup_f32(cf.orientation.x.y), + cleanup_f32(cf.orientation.x.z), + ), + Vector3::new( + cleanup_f32(cf.orientation.y.x), + cleanup_f32(cf.orientation.y.y), + cleanup_f32(cf.orientation.y.z), + ), + Vector3::new( + cleanup_f32(cf.orientation.z.x), + cleanup_f32(cf.orientation.z.y), + cleanup_f32(cf.orientation.z.z), + ), + ), + )))); + } + Variant::Rect(rect) => { + return Self::FullyQualified(Variant::Rect(Rect::new( + Vector2::new(cleanup_f32(rect.min.x), cleanup_f32(rect.min.y)), + Vector2::new(cleanup_f32(rect.max.x), cleanup_f32(rect.max.y)), + ))); + } + Variant::Ray(ray) => { + return Self::FullyQualified(Variant::Ray(Ray::new( + Vector3::new( + cleanup_f32(ray.origin.x), + cleanup_f32(ray.origin.y), + cleanup_f32(ray.origin.z), + ), + Vector3::new( + cleanup_f32(ray.direction.x), + cleanup_f32(ray.direction.y), + cleanup_f32(ray.direction.z), + ), + ))); + } + Variant::Region3(region) => { + return Self::FullyQualified(Variant::Region3(Region3::new( + Vector3::new( + cleanup_f32(region.min.x), + cleanup_f32(region.min.y), + cleanup_f32(region.min.z), + ), + Vector3::new( + cleanup_f32(region.max.x), + cleanup_f32(region.max.y), + cleanup_f32(region.max.z), + ), + ))); + } + Variant::NumberSequence(seq) => { + return Self::FullyQualified(Variant::NumberSequence(NumberSequence { + keypoints: seq + .keypoints + .into_iter() + .map(|kp| { + NumberSequenceKeypoint::new( + cleanup_f32(kp.time), + cleanup_f32(kp.value), + cleanup_f32(kp.envelope), + ) + }) + .collect(), + })); + } + Variant::ColorSequence(seq) => { + return Self::FullyQualified(Variant::ColorSequence(ColorSequence { + keypoints: seq + .keypoints + .into_iter() + .map(|kp| { + ColorSequenceKeypoint::new( + cleanup_f32(kp.time), + Color3::new( + cleanup_f32(kp.color.r), + cleanup_f32(kp.color.g), + cleanup_f32(kp.color.b), + ), + ) + }) + .collect(), + })); + } + Variant::PhysicalProperties(PhysicalProperties::Custom(cpp)) => { + return Self::FullyQualified(Variant::PhysicalProperties( + PhysicalProperties::Custom(CustomPhysicalProperties::new( + cleanup_f32(cpp.density()), + cleanup_f32(cpp.friction()), + cleanup_f32(cpp.elasticity()), + cleanup_f32(cpp.friction_weight()), + cleanup_f32(cpp.elasticity_weight()), + cleanup_f32(cpp.acoustic_absorption()), + )), + )); + } Variant::Attributes(attr) => AmbiguousValue::Attributes(attr), Variant::Font(font) => AmbiguousValue::Font(font), Variant::MaterialColors(colors) => AmbiguousValue::MaterialColors(colors), @@ -288,6 +417,15 @@ impl AmbiguousValue { } } +fn cleanup_f32(v: f32) -> f32 { + let cleaned = format!("{:.6}", v).parse::().unwrap_or(v); + if cleaned == 0.0 { + 0.0 + } else { + cleaned + } +} + fn find_descriptor( class_name: &str, prop_name: &str,