From bb8b91345c46ab8400c6928272c6f43d0d2d6014 Mon Sep 17 00:00:00 2001 From: Connor Date: Tue, 30 Dec 2025 23:13:12 +0900 Subject: [PATCH] Phase 2 --- examples/basic_parsing.rs | 6 +- src/error.rs | 8 + src/lib.rs | 7 + src/model/mod.rs | 71 ++++- src/parser/mod.rs | 11 +- src/property/mod.rs | 531 +++++++++++++++++++++++++++++++++++++ src/types/component.rs | 163 ++++++++++++ src/types/game_object.rs | 180 +++++++++++++ src/types/ids.rs | 131 +++++++++ src/types/mod.rs | 17 ++ src/types/transform.rs | 398 +++++++++++++++++++++++++++ src/types/values.rs | 303 +++++++++++++++++++++ tests/integration_tests.rs | 4 +- 13 files changed, 1816 insertions(+), 14 deletions(-) create mode 100644 src/property/mod.rs create mode 100644 src/types/component.rs create mode 100644 src/types/game_object.rs create mode 100644 src/types/ids.rs create mode 100644 src/types/mod.rs create mode 100644 src/types/transform.rs create mode 100644 src/types/values.rs diff --git a/examples/basic_parsing.rs b/examples/basic_parsing.rs index 95337e3..32fb574 100644 --- a/examples/basic_parsing.rs +++ b/examples/basic_parsing.rs @@ -35,9 +35,9 @@ fn main() { println!("Found {} GameObjects:", game_objects.len()); for go in game_objects { if let Some(go_props) = go.get("GameObject") { - if let Some(props) = go_props.as_mapping() { - if let Some(name) = props.get(&serde_yaml::Value::String("m_Name".to_string())) { - println!(" - {}", name.as_str().unwrap_or("Unknown")); + if let Some(props) = go_props.as_object() { + if let Some(name) = props.get("m_Name").and_then(|v| v.as_str()) { + println!(" - {}", name); } } } diff --git a/src/error.rs b/src/error.rs index 74a5418..0099a8a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -46,6 +46,14 @@ pub enum Error { /// Type conversion error #[error("Type conversion error: expected {expected}, found {found}")] TypeMismatch { expected: String, found: String }, + + /// Property value conversion error + #[error("Failed to convert property value from {from} to {to}")] + PropertyConversion { from: String, to: String }, + + /// Invalid property path + #[error("Invalid property path: {0}")] + InvalidPropertyPath(String), } impl Error { diff --git a/src/lib.rs b/src/lib.rs index f154a9e..47e17bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,8 +19,15 @@ pub mod error; pub mod model; pub mod parser; +pub mod property; +pub mod types; // Re-exports pub use error::{Error, Result}; pub use model::{UnityDocument, UnityFile}; pub use parser::parse_unity_file; +pub use property::PropertyValue; +pub use types::{ + Color, Component, ExternalRef, FileID, FileRef, GameObject, GenericComponent, LocalID, + Quaternion, RectTransform, Transform, Vector2, Vector3, +}; diff --git a/src/model/mod.rs b/src/model/mod.rs index dc17f57..368bb77 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,3 +1,5 @@ +use crate::property::PropertyValue; +use crate::types::{Color, FileID, Quaternion, Vector2, Vector3}; use indexmap::IndexMap; use std::path::PathBuf; @@ -27,7 +29,7 @@ impl UnityFile { } /// Get a document by its file ID - pub fn get_document(&self, file_id: i64) -> Option<&UnityDocument> { + pub fn get_document(&self, file_id: FileID) -> Option<&UnityDocument> { self.documents.iter().find(|doc| doc.file_id == file_id) } @@ -55,7 +57,7 @@ pub struct UnityDocument { pub type_id: u32, /// File ID (from &ID anchor) - pub file_id: i64, + pub file_id: FileID, /// Class name (e.g., "GameObject", "Transform", "RectTransform") pub class_name: String, @@ -66,7 +68,7 @@ pub struct UnityDocument { impl UnityDocument { /// Create a new UnityDocument - pub fn new(type_id: u32, file_id: i64, class_name: String) -> Self { + pub fn new(type_id: u32, file_id: FileID, class_name: String) -> Self { Self { type_id, file_id, @@ -76,10 +78,67 @@ impl UnityDocument { } /// Get a property value by key - pub fn get(&self, key: &str) -> Option<&serde_yaml::Value> { + pub fn get(&self, key: &str) -> Option<&PropertyValue> { self.properties.get(key) } + /// Get a property value as a string + pub fn get_string(&self, key: &str) -> Option<&str> { + self.get(key).and_then(|v| v.as_str()) + } + + /// Get a property value as an i64 + pub fn get_i64(&self, key: &str) -> Option { + self.get(key).and_then(|v| v.as_i64()) + } + + /// Get a property value as an f64 + pub fn get_f64(&self, key: &str) -> Option { + self.get(key).and_then(|v| v.as_f64()) + } + + /// Get a property value as a bool + pub fn get_bool(&self, key: &str) -> Option { + self.get(key).and_then(|v| v.as_bool()) + } + + /// Get a property value as a Vector2 + pub fn get_vector2(&self, key: &str) -> Option<&Vector2> { + self.get(key).and_then(|v| v.as_vector2()) + } + + /// Get a property value as a Vector3 + pub fn get_vector3(&self, key: &str) -> Option<&Vector3> { + self.get(key).and_then(|v| v.as_vector3()) + } + + /// Get a property value as a Color + pub fn get_color(&self, key: &str) -> Option<&Color> { + self.get(key).and_then(|v| v.as_color()) + } + + /// Get a property value as a Quaternion + pub fn get_quaternion(&self, key: &str) -> Option<&Quaternion> { + self.get(key).and_then(|v| v.as_quaternion()) + } + + /// Get a property value as a FileID + pub fn get_file_ref(&self, key: &str) -> Option { + self.get(key) + .and_then(|v| v.as_file_ref()) + .map(|r| r.file_id) + } + + /// Get a property value as an array + pub fn get_array(&self, key: &str) -> Option<&Vec> { + self.get(key).and_then(|v| v.as_array()) + } + + /// Get a property value as an object + pub fn get_object(&self, key: &str) -> Option<&IndexMap> { + self.get(key).and_then(|v| v.as_object()) + } + /// Check if this is a GameObject pub fn is_game_object(&self) -> bool { self.class_name == "GameObject" || self.type_id == 1 @@ -91,5 +150,5 @@ impl UnityDocument { } } -/// Property map type (ordered map of string keys to YAML values) -pub type PropertyMap = IndexMap; +/// Property map type (ordered map of string keys to typed property values) +pub type PropertyMap = IndexMap; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a959bfa..8e4bb85 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6,6 +6,7 @@ mod yaml; pub use unity_tag::{UnityTag, parse_unity_tag}; pub use yaml::split_yaml_documents; +use crate::property::convert_yaml_value; use crate::{Error, Result, UnityDocument, UnityFile}; use std::path::Path; @@ -74,10 +75,14 @@ fn parse_document(raw_doc: &str) -> Result> { } else { match serde_yaml::from_str::(yaml_content) { Ok(serde_yaml::Value::Mapping(map)) => { - // Convert to IndexMap + // Convert to IndexMap with PropertyValue map.into_iter() .filter_map(|(k, v)| { - k.as_str().map(|s| (s.to_string(), v)) + k.as_str().and_then(|s| { + convert_yaml_value(&v) + .ok() + .map(|pv| (s.to_string(), pv)) + }) }) .collect() } @@ -95,7 +100,7 @@ fn parse_document(raw_doc: &str) -> Result> { Ok(Some(UnityDocument { type_id: tag.type_id, - file_id: tag.file_id, + file_id: crate::types::FileID::from_i64(tag.file_id), class_name, properties, })) diff --git a/src/property/mod.rs b/src/property/mod.rs new file mode 100644 index 0000000..0cf6e72 --- /dev/null +++ b/src/property/mod.rs @@ -0,0 +1,531 @@ +//! Property value types and conversion +//! +//! This module provides the `PropertyValue` enum which represents +//! typed Unity property values, and conversion logic from YAML values. + +use crate::types::{Color, ExternalRef, FileID, FileRef, Quaternion, Vector2, Vector3}; +use crate::Error; +use indexmap::IndexMap; +use std::fmt; + +/// A typed property value in a Unity object +/// +/// This enum represents all possible value types that can appear +/// in Unity YAML files, including Unity-specific types like Vector3 and Color. +#[derive(Debug, Clone, PartialEq)] +pub enum PropertyValue { + /// Integer value + Integer(i64), + /// Floating-point value + Float(f64), + /// String value + String(String), + /// Boolean value + Boolean(bool), + /// Null value + Null, + + // Unity-specific types + /// 2D vector (x, y) + Vector2(Vector2), + /// 3D vector (x, y, z) + Vector3(Vector3), + /// Color (r, g, b, a) + Color(Color), + /// Quaternion rotation (x, y, z, w) + Quaternion(Quaternion), + /// Reference to another object by file ID + FileRef(FileRef), + /// Reference to an external asset by GUID + ExternalRef(ExternalRef), + + // Collections + /// Array of values + Array(Vec), + /// Nested object with properties + Object(IndexMap), +} + +impl PropertyValue { + /// Try to get this value as an integer + pub fn as_i64(&self) -> Option { + match self { + PropertyValue::Integer(v) => Some(*v), + _ => None, + } + } + + /// Try to get this value as a float + pub fn as_f64(&self) -> Option { + match self { + PropertyValue::Float(v) => Some(*v), + PropertyValue::Integer(v) => Some(*v as f64), + _ => None, + } + } + + /// Try to get this value as a string reference + pub fn as_str(&self) -> Option<&str> { + match self { + PropertyValue::String(s) => Some(s.as_str()), + _ => None, + } + } + + /// Try to get this value as a boolean + pub fn as_bool(&self) -> Option { + match self { + PropertyValue::Boolean(b) => Some(*b), + _ => None, + } + } + + /// Try to get this value as a Vector2 + pub fn as_vector2(&self) -> Option<&Vector2> { + match self { + PropertyValue::Vector2(v) => Some(v), + _ => None, + } + } + + /// Try to get this value as a Vector3 + pub fn as_vector3(&self) -> Option<&Vector3> { + match self { + PropertyValue::Vector3(v) => Some(v), + _ => None, + } + } + + /// Try to get this value as a Color + pub fn as_color(&self) -> Option<&Color> { + match self { + PropertyValue::Color(c) => Some(c), + _ => None, + } + } + + /// Try to get this value as a Quaternion + pub fn as_quaternion(&self) -> Option<&Quaternion> { + match self { + PropertyValue::Quaternion(q) => Some(q), + _ => None, + } + } + + /// Try to get this value as a FileRef + pub fn as_file_ref(&self) -> Option<&FileRef> { + match self { + PropertyValue::FileRef(r) => Some(r), + _ => None, + } + } + + /// Try to get this value as an ExternalRef + pub fn as_external_ref(&self) -> Option<&ExternalRef> { + match self { + PropertyValue::ExternalRef(r) => Some(r), + _ => None, + } + } + + /// Try to get this value as an array + pub fn as_array(&self) -> Option<&Vec> { + match self { + PropertyValue::Array(arr) => Some(arr), + _ => None, + } + } + + /// Try to get this value as an object + pub fn as_object(&self) -> Option<&IndexMap> { + match self { + PropertyValue::Object(obj) => Some(obj), + _ => None, + } + } + + /// Check if this value is null + pub fn is_null(&self) -> bool { + matches!(self, PropertyValue::Null) + } + + /// Check if this value is an array + pub fn is_array(&self) -> bool { + matches!(self, PropertyValue::Array(_)) + } + + /// Check if this value is an object + pub fn is_object(&self) -> bool { + matches!(self, PropertyValue::Object(_)) + } + + /// Check if this value is a Vector3 + pub fn is_vector3(&self) -> bool { + matches!(self, PropertyValue::Vector3(_)) + } + + /// Check if this value is a Color + pub fn is_color(&self) -> bool { + matches!(self, PropertyValue::Color(_)) + } + + /// Check if this value is a FileRef + pub fn is_file_ref(&self) -> bool { + matches!(self, PropertyValue::FileRef(_)) + } +} + +impl fmt::Display for PropertyValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PropertyValue::Integer(v) => write!(f, "{}", v), + PropertyValue::Float(v) => write!(f, "{}", v), + PropertyValue::String(s) => write!(f, "\"{}\"", s), + PropertyValue::Boolean(b) => write!(f, "{}", b), + PropertyValue::Null => write!(f, "null"), + PropertyValue::Vector2(v) => write!(f, "({}, {})", v.x, v.y), + PropertyValue::Vector3(v) => write!(f, "({}, {}, {})", v.x, v.y, v.z), + PropertyValue::Color(c) => write!(f, "rgba({}, {}, {}, {})", c.r, c.g, c.b, c.a), + PropertyValue::Quaternion(q) => write!(f, "({}, {}, {}, {})", q.x, q.y, q.z, q.w), + PropertyValue::FileRef(r) => write!(f, "{{fileID: {}}}", r.file_id), + PropertyValue::ExternalRef(r) => write!(f, "{{guid: {}, type: {}}}", r.guid, r.type_id), + PropertyValue::Array(arr) => { + write!(f, "[")?; + for (i, item) in arr.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", item)?; + } + write!(f, "]") + } + PropertyValue::Object(obj) => { + write!(f, "{{")?; + for (i, (k, v)) in obj.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}: {}", k, v)?; + } + write!(f, "}}") + } + } + } +} + +/// Convert a serde_yaml::Value to a PropertyValue +/// +/// This function recognizes Unity-specific patterns in YAML mappings: +/// - `{fileID: N}` → `PropertyValue::FileRef` +/// - `{x, y, z}` → `PropertyValue::Vector3` +/// - `{x, y}` → `PropertyValue::Vector2` +/// - `{r, g, b, a}` → `PropertyValue::Color` +/// - `{x, y, z, w}` → `PropertyValue::Quaternion` +/// - `{guid, type}` → `PropertyValue::ExternalRef` +pub fn convert_yaml_value(value: &serde_yaml::Value) -> crate::Result { + match value { + serde_yaml::Value::Null => Ok(PropertyValue::Null), + serde_yaml::Value::Bool(b) => Ok(PropertyValue::Boolean(*b)), + serde_yaml::Value::Number(n) => { + if let Some(i) = n.as_i64() { + Ok(PropertyValue::Integer(i)) + } else if let Some(f) = n.as_f64() { + Ok(PropertyValue::Float(f)) + } else { + Err(Error::invalid_format(format!( + "Unsupported number format: {}", + n + ))) + } + } + serde_yaml::Value::String(s) => Ok(PropertyValue::String(s.clone())), + serde_yaml::Value::Sequence(seq) => { + let mut array = Vec::with_capacity(seq.len()); + for item in seq { + array.push(convert_yaml_value(item)?); + } + Ok(PropertyValue::Array(array)) + } + serde_yaml::Value::Mapping(map) => { + // Check for Unity-specific patterns + if let Some(unity_type) = try_convert_unity_type(map)? { + Ok(unity_type) + } else { + // Convert to generic object + let mut object = IndexMap::new(); + for (k, v) in map { + if let Some(key) = k.as_str() { + object.insert(key.to_string(), convert_yaml_value(v)?); + } + } + Ok(PropertyValue::Object(object)) + } + } + _ => Err(Error::invalid_format(format!( + "Unsupported YAML value type: {:?}", + value + ))), + } +} + +/// Try to convert a YAML mapping to a Unity-specific type +fn try_convert_unity_type( + map: &serde_yaml::Mapping, +) -> crate::Result> { + // Helper to get a float from a mapping + let get_f32 = |key: &str| -> Option { + map.get(&serde_yaml::Value::String(key.to_string())) + .and_then(|v| v.as_f64()) + .map(|f| f as f32) + }; + + // Helper to get an i64 from a mapping + let get_i64 = |key: &str| -> Option { + map.get(&serde_yaml::Value::String(key.to_string())) + .and_then(|v| v.as_i64()) + }; + + // Helper to get a string from a mapping + let get_string = |key: &str| -> Option { + map.get(&serde_yaml::Value::String(key.to_string())) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + }; + + // Check for {fileID: N} pattern + if map.len() == 1 && map.contains_key(&serde_yaml::Value::String("fileID".to_string())) { + if let Some(file_id) = get_i64("fileID") { + return Ok(Some(PropertyValue::FileRef(FileRef::new( + FileID::from_i64(file_id), + )))); + } + } + + // Check for {guid: ..., type: N} pattern + if map.len() == 2 + && map.contains_key(&serde_yaml::Value::String("guid".to_string())) + && map.contains_key(&serde_yaml::Value::String("type".to_string())) + { + if let (Some(guid), Some(type_id)) = (get_string("guid"), get_i64("type")) { + return Ok(Some(PropertyValue::ExternalRef(ExternalRef::new( + guid, + type_id as i32, + )))); + } + } + + // Check for {r, g, b, a} pattern (Color) + if map.len() == 4 + && map.contains_key(&serde_yaml::Value::String("r".to_string())) + && map.contains_key(&serde_yaml::Value::String("g".to_string())) + && map.contains_key(&serde_yaml::Value::String("b".to_string())) + && map.contains_key(&serde_yaml::Value::String("a".to_string())) + { + if let (Some(r), Some(g), Some(b), Some(a)) = + (get_f32("r"), get_f32("g"), get_f32("b"), get_f32("a")) + { + return Ok(Some(PropertyValue::Color(Color::new(r, g, b, a)))); + } + } + + // Check for {x, y, z, w} pattern (Quaternion) + if map.len() == 4 + && map.contains_key(&serde_yaml::Value::String("x".to_string())) + && map.contains_key(&serde_yaml::Value::String("y".to_string())) + && map.contains_key(&serde_yaml::Value::String("z".to_string())) + && map.contains_key(&serde_yaml::Value::String("w".to_string())) + { + if let (Some(x), Some(y), Some(z), Some(w)) = + (get_f32("x"), get_f32("y"), get_f32("z"), get_f32("w")) + { + return Ok(Some(PropertyValue::Quaternion(Quaternion::new( + x, y, z, w, + )))); + } + } + + // Check for {x, y, z} pattern (Vector3) + if map.len() == 3 + && map.contains_key(&serde_yaml::Value::String("x".to_string())) + && map.contains_key(&serde_yaml::Value::String("y".to_string())) + && map.contains_key(&serde_yaml::Value::String("z".to_string())) + { + if let (Some(x), Some(y), Some(z)) = (get_f32("x"), get_f32("y"), get_f32("z")) { + return Ok(Some(PropertyValue::Vector3(Vector3::new(x, y, z)))); + } + } + + // Check for {x, y} pattern (Vector2) + if map.len() == 2 + && map.contains_key(&serde_yaml::Value::String("x".to_string())) + && map.contains_key(&serde_yaml::Value::String("y".to_string())) + { + if let (Some(x), Some(y)) = (get_f32("x"), get_f32("y")) { + return Ok(Some(PropertyValue::Vector2(Vector2::new(x, y)))); + } + } + + // Not a Unity-specific type + Ok(None) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_yaml::Value; + + #[test] + fn test_convert_primitives() { + assert_eq!( + convert_yaml_value(&Value::Null).unwrap(), + PropertyValue::Null + ); + assert_eq!( + convert_yaml_value(&Value::Bool(true)).unwrap(), + PropertyValue::Boolean(true) + ); + assert_eq!( + convert_yaml_value(&Value::Number(42.into())).unwrap(), + PropertyValue::Integer(42) + ); + assert_eq!( + convert_yaml_value(&Value::String("test".to_string())).unwrap(), + PropertyValue::String("test".to_string()) + ); + } + + #[test] + fn test_convert_vector3() { + let yaml = serde_yaml::from_str("{ x: 1.0, y: 2.0, z: 3.0 }").unwrap(); + let result = convert_yaml_value(&yaml).unwrap(); + + if let PropertyValue::Vector3(v) = result { + assert_eq!(v.x, 1.0); + assert_eq!(v.y, 2.0); + assert_eq!(v.z, 3.0); + } else { + panic!("Expected Vector3, got {:?}", result); + } + } + + #[test] + fn test_convert_vector2() { + let yaml = serde_yaml::from_str("{ x: 1.0, y: 2.0 }").unwrap(); + let result = convert_yaml_value(&yaml).unwrap(); + + if let PropertyValue::Vector2(v) = result { + assert_eq!(v.x, 1.0); + assert_eq!(v.y, 2.0); + } else { + panic!("Expected Vector2, got {:?}", result); + } + } + + #[test] + fn test_convert_color() { + let yaml = serde_yaml::from_str("{ r: 1.0, g: 0.5, b: 0.0, a: 1.0 }").unwrap(); + let result = convert_yaml_value(&yaml).unwrap(); + + if let PropertyValue::Color(c) = result { + assert_eq!(c.r, 1.0); + assert_eq!(c.g, 0.5); + assert_eq!(c.b, 0.0); + assert_eq!(c.a, 1.0); + } else { + panic!("Expected Color, got {:?}", result); + } + } + + #[test] + fn test_convert_quaternion() { + let yaml = serde_yaml::from_str("{ x: 0.0, y: 0.0, z: 0.0, w: 1.0 }").unwrap(); + let result = convert_yaml_value(&yaml).unwrap(); + + if let PropertyValue::Quaternion(q) = result { + assert_eq!(q.x, 0.0); + assert_eq!(q.w, 1.0); + } else { + panic!("Expected Quaternion, got {:?}", result); + } + } + + #[test] + fn test_convert_file_ref() { + let yaml = serde_yaml::from_str("{ fileID: 12345 }").unwrap(); + let result = convert_yaml_value(&yaml).unwrap(); + + if let PropertyValue::FileRef(r) = result { + assert_eq!(r.file_id.as_i64(), 12345); + } else { + panic!("Expected FileRef, got {:?}", result); + } + } + + #[test] + fn test_convert_external_ref() { + let yaml = serde_yaml::from_str("{ guid: abc123, type: 2 }").unwrap(); + let result = convert_yaml_value(&yaml).unwrap(); + + if let PropertyValue::ExternalRef(r) = result { + assert_eq!(r.guid, "abc123"); + assert_eq!(r.type_id, 2); + } else { + panic!("Expected ExternalRef, got {:?}", result); + } + } + + #[test] + fn test_convert_array() { + let yaml = serde_yaml::from_str("[1, 2, 3]").unwrap(); + let result = convert_yaml_value(&yaml).unwrap(); + + if let PropertyValue::Array(arr) = result { + assert_eq!(arr.len(), 3); + assert_eq!(arr[0].as_i64(), Some(1)); + assert_eq!(arr[1].as_i64(), Some(2)); + assert_eq!(arr[2].as_i64(), Some(3)); + } else { + panic!("Expected Array, got {:?}", result); + } + } + + #[test] + fn test_convert_object() { + let yaml = serde_yaml::from_str("{ name: Test, value: 42 }").unwrap(); + let result = convert_yaml_value(&yaml).unwrap(); + + if let PropertyValue::Object(obj) = result { + assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("Test")); + assert_eq!(obj.get("value").and_then(|v| v.as_i64()), Some(42)); + } else { + panic!("Expected Object, got {:?}", result); + } + } + + #[test] + fn test_accessors() { + let val = PropertyValue::Integer(42); + assert_eq!(val.as_i64(), Some(42)); + assert_eq!(val.as_f64(), Some(42.0)); + assert_eq!(val.as_str(), None); + + let val = PropertyValue::String("test".to_string()); + assert_eq!(val.as_str(), Some("test")); + assert_eq!(val.as_i64(), None); + } + + #[test] + fn test_type_checks() { + assert!(PropertyValue::Null.is_null()); + assert!(PropertyValue::Array(vec![]).is_array()); + assert!(PropertyValue::Vector3(Vector3::zero()).is_vector3()); + assert!(PropertyValue::Color(Color::white()).is_color()); + } + + #[test] + fn test_display() { + assert_eq!(format!("{}", PropertyValue::Integer(42)), "42"); + assert_eq!(format!("{}", PropertyValue::String("test".to_string())), "\"test\""); + assert_eq!(format!("{}", PropertyValue::Vector3(Vector3::new(1.0, 2.0, 3.0))), "(1, 2, 3)"); + } +} diff --git a/src/types/component.rs b/src/types/component.rs new file mode 100644 index 0000000..839b14e --- /dev/null +++ b/src/types/component.rs @@ -0,0 +1,163 @@ +//! Component trait and generic component wrapper + +use crate::model::UnityDocument; +use crate::types::FileRef; + +/// A trait for Unity components +/// +/// Components are attached to GameObjects and provide functionality. +pub trait Component { + /// Get the GameObject this component is attached to + fn game_object(&self) -> Option; + + /// Check if this component is enabled + fn is_enabled(&self) -> bool { + true // Default implementation + } + + /// Get the underlying UnityDocument + fn document(&self) -> &UnityDocument; +} + +/// A generic component wrapper that works with any component type +/// +/// # Examples +/// +/// ```no_run +/// use cursebreaker_parser::{UnityFile, types::{Component, GenericComponent}}; +/// +/// let file = UnityFile::from_path("Scene.unity")?; +/// for doc in &file.documents { +/// if !doc.is_game_object() { +/// if let Some(comp) = GenericComponent::new(doc) { +/// println!("Component: {}", doc.class_name); +/// if let Some(go_ref) = comp.game_object() { +/// println!(" Attached to: {}", go_ref.file_id); +/// } +/// } +/// } +/// } +/// # Ok::<(), cursebreaker_parser::Error>(()) +/// ``` +#[derive(Debug)] +pub struct GenericComponent<'a> { + document: &'a UnityDocument, +} + +impl<'a> GenericComponent<'a> { + /// Create a GenericComponent wrapper from a UnityDocument + /// + /// Returns None if the document is a GameObject (GameObjects are not components). + pub fn new(document: &'a UnityDocument) -> Option { + if !document.is_game_object() { + Some(Self { document }) + } else { + None + } + } + + /// Get the class name of this component + pub fn class_name(&self) -> &str { + &self.document.class_name + } +} + +impl<'a> Component for GenericComponent<'a> { + fn game_object(&self) -> Option { + // Look for m_GameObject property which is common to all components + self.document + .get(&self.document.class_name) + .and_then(|obj| obj.as_object()) + .and_then(|props| props.get("m_GameObject")) + .and_then(|v| v.as_file_ref()) + .copied() + } + + fn is_enabled(&self) -> bool { + // Look for m_Enabled property + self.document + .get(&self.document.class_name) + .and_then(|obj| obj.as_object()) + .and_then(|props| props.get("m_Enabled")) + .and_then(|v| v.as_bool()) + .unwrap_or(true) // Default to enabled + } + + fn document(&self) -> &UnityDocument { + self.document + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::property::PropertyValue; + use crate::types::FileID; + use indexmap::IndexMap; + + fn create_test_component() -> UnityDocument { + let mut properties = IndexMap::new(); + + let mut comp_props = IndexMap::new(); + comp_props.insert( + "m_GameObject".to_string(), + PropertyValue::FileRef(crate::types::FileRef::new(FileID::from_i64(67890))), + ); + comp_props.insert("m_Enabled".to_string(), PropertyValue::Boolean(true)); + + properties.insert("Transform".to_string(), PropertyValue::Object(comp_props)); + + UnityDocument { + type_id: 4, + file_id: FileID::from_i64(12345), + class_name: "Transform".to_string(), + properties, + } + } + + #[test] + fn test_component_creation() { + let doc = create_test_component(); + let comp = GenericComponent::new(&doc); + assert!(comp.is_some()); + } + + #[test] + fn test_component_game_object_ref() { + let doc = create_test_component(); + let comp = GenericComponent::new(&doc).unwrap(); + let go_ref = comp.game_object(); + assert!(go_ref.is_some()); + assert_eq!(go_ref.unwrap().file_id.as_i64(), 67890); + } + + #[test] + fn test_component_is_enabled() { + let doc = create_test_component(); + let comp = GenericComponent::new(&doc).unwrap(); + assert!(comp.is_enabled()); + } + + #[test] + fn test_component_class_name() { + let doc = create_test_component(); + let comp = GenericComponent::new(&doc).unwrap(); + assert_eq!(comp.class_name(), "Transform"); + } + + #[test] + fn test_game_object_is_not_component() { + let mut properties = IndexMap::new(); + properties.insert("GameObject".to_string(), PropertyValue::Object(IndexMap::new())); + + let doc = UnityDocument { + type_id: 1, + file_id: FileID::from_i64(12345), + class_name: "GameObject".to_string(), + properties, + }; + + let comp = GenericComponent::new(&doc); + assert!(comp.is_none()); + } +} diff --git a/src/types/game_object.rs b/src/types/game_object.rs new file mode 100644 index 0000000..8559df0 --- /dev/null +++ b/src/types/game_object.rs @@ -0,0 +1,180 @@ +//! GameObject wrapper for ergonomic access to GameObject properties + +use crate::model::UnityDocument; +use crate::types::{FileID, FileRef}; + +/// A wrapper around a UnityDocument that represents a GameObject +/// +/// Provides convenient access to common GameObject properties. +/// +/// # Examples +/// +/// ```no_run +/// use cursebreaker_parser::{UnityFile, types::GameObject}; +/// +/// let file = UnityFile::from_path("Scene.unity")?; +/// for doc in &file.documents { +/// if let Some(go) = GameObject::new(doc) { +/// println!("GameObject: {}", go.name().unwrap_or("Unnamed")); +/// println!(" Active: {}", go.is_active()); +/// println!(" Components: {}", go.components().len()); +/// } +/// } +/// # Ok::<(), cursebreaker_parser::Error>(()) +/// ``` +#[derive(Debug)] +pub struct GameObject<'a> { + document: &'a UnityDocument, +} + +impl<'a> GameObject<'a> { + /// Create a GameObject wrapper from a UnityDocument + /// + /// Returns None if the document is not a GameObject. + pub fn new(document: &'a UnityDocument) -> Option { + if document.is_game_object() { + Some(Self { document }) + } else { + None + } + } + + /// Get the GameObject's name + pub fn name(&self) -> Option<&str> { + self.document + .get("GameObject") + .and_then(|obj| obj.as_object()) + .and_then(|props| props.get("m_Name")) + .and_then(|v| v.as_str()) + } + + /// Check if the GameObject is active + pub fn is_active(&self) -> bool { + self.document + .get("GameObject") + .and_then(|obj| obj.as_object()) + .and_then(|props| props.get("m_IsActive")) + .and_then(|v| v.as_bool()) + .unwrap_or(true) // Default to true if not specified + } + + /// Get the GameObject's layer + pub fn layer(&self) -> Option { + self.document + .get("GameObject") + .and_then(|obj| obj.as_object()) + .and_then(|props| props.get("m_Layer")) + .and_then(|v| v.as_i64()) + } + + /// Get the GameObject's tag as a tag ID + pub fn tag(&self) -> Option { + self.document + .get("GameObject") + .and_then(|obj| obj.as_object()) + .and_then(|props| props.get("m_TagString")) + .and_then(|v| v.as_i64()) + } + + /// Get the list of component references attached to this GameObject + pub fn components(&self) -> Vec { + self.document + .get("GameObject") + .and_then(|obj| obj.as_object()) + .and_then(|props| props.get("m_Component")) + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|item| { + item.as_object().and_then(|obj| { + obj.get("component") + .and_then(|v| v.as_file_ref()) + .copied() + }) + }) + .collect() + }) + .unwrap_or_default() + } + + /// Get the file ID of this GameObject + pub fn file_id(&self) -> FileID { + self.document.file_id + } + + /// Get the underlying UnityDocument + pub fn document(&self) -> &'a UnityDocument { + self.document + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::property::PropertyValue; + use indexmap::IndexMap; + + fn create_test_game_object() -> UnityDocument { + let mut properties = IndexMap::new(); + + let mut go_props = IndexMap::new(); + go_props.insert("m_Name".to_string(), PropertyValue::String("TestObject".to_string())); + go_props.insert("m_IsActive".to_string(), PropertyValue::Boolean(true)); + go_props.insert("m_Layer".to_string(), PropertyValue::Integer(0)); + go_props.insert("m_Component".to_string(), PropertyValue::Array(vec![])); + + properties.insert("GameObject".to_string(), PropertyValue::Object(go_props)); + + UnityDocument { + type_id: 1, + file_id: FileID::from_i64(12345), + class_name: "GameObject".to_string(), + properties, + } + } + + #[test] + fn test_game_object_creation() { + let doc = create_test_game_object(); + let go = GameObject::new(&doc); + assert!(go.is_some()); + } + + #[test] + fn test_game_object_name() { + let doc = create_test_game_object(); + let go = GameObject::new(&doc).unwrap(); + assert_eq!(go.name(), Some("TestObject")); + } + + #[test] + fn test_game_object_is_active() { + let doc = create_test_game_object(); + let go = GameObject::new(&doc).unwrap(); + assert!(go.is_active()); + } + + #[test] + fn test_game_object_layer() { + let doc = create_test_game_object(); + let go = GameObject::new(&doc).unwrap(); + assert_eq!(go.layer(), Some(0)); + } + + #[test] + fn test_game_object_file_id() { + let doc = create_test_game_object(); + let go = GameObject::new(&doc).unwrap(); + assert_eq!(go.file_id().as_i64(), 12345); + } + + #[test] + fn test_non_game_object() { + let mut doc = create_test_game_object(); + doc.type_id = 4; // Transform type ID + doc.class_name = "Transform".to_string(); + + let go = GameObject::new(&doc); + assert!(go.is_none()); + } +} diff --git a/src/types/ids.rs b/src/types/ids.rs new file mode 100644 index 0000000..c180009 --- /dev/null +++ b/src/types/ids.rs @@ -0,0 +1,131 @@ +//! Unity ID types + +use std::fmt; + +/// A Unity file ID, used to reference Unity objects within a file +/// +/// # Examples +/// +/// ``` +/// use cursebreaker_parser::FileID; +/// +/// let file_id = FileID::from_i64(1866116814460599870); +/// assert_eq!(file_id.as_i64(), 1866116814460599870); +/// ``` +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct FileID(i64); + +impl FileID { + /// Create a FileID from an i64 + pub fn from_i64(id: i64) -> Self { + Self(id) + } + + /// Get the underlying i64 value + pub fn as_i64(&self) -> i64 { + self.0 + } +} + +impl fmt::Display for FileID { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for FileID { + fn from(id: i64) -> Self { + Self::from_i64(id) + } +} + +impl From for i64 { + fn from(file_id: FileID) -> Self { + file_id.as_i64() + } +} + +/// A local ID for objects within a Unity file +/// +/// This is currently an alias for FileID but may have different +/// semantics in future versions. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct LocalID(i64); + +impl LocalID { + /// Create a LocalID from an i64 + pub fn from_i64(id: i64) -> Self { + Self(id) + } + + /// Get the underlying i64 value + pub fn as_i64(&self) -> i64 { + self.0 + } +} + +impl fmt::Display for LocalID { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for LocalID { + fn from(id: i64) -> Self { + Self::from_i64(id) + } +} + +impl From for i64 { + fn from(local_id: LocalID) -> Self { + local_id.as_i64() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_file_id_creation() { + let file_id = FileID::from_i64(12345); + assert_eq!(file_id.as_i64(), 12345); + } + + #[test] + fn test_file_id_display() { + let file_id = FileID::from_i64(1866116814460599870); + assert_eq!(format!("{}", file_id), "1866116814460599870"); + } + + #[test] + fn test_file_id_equality() { + let id1 = FileID::from_i64(12345); + let id2 = FileID::from_i64(12345); + let id3 = FileID::from_i64(67890); + + assert_eq!(id1, id2); + assert_ne!(id1, id3); + } + + #[test] + fn test_file_id_conversion() { + let id: FileID = 12345.into(); + assert_eq!(id.as_i64(), 12345); + + let value: i64 = id.into(); + assert_eq!(value, 12345); + } + + #[test] + fn test_local_id_creation() { + let local_id = LocalID::from_i64(12345); + assert_eq!(local_id.as_i64(), 12345); + } + + #[test] + fn test_local_id_display() { + let local_id = LocalID::from_i64(67890); + assert_eq!(format!("{}", local_id), "67890"); + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..5522582 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,17 @@ +//! Unity-specific types and wrappers +//! +//! This module provides type-safe representations of Unity types, +//! including IDs, value types (Vector3, Color, etc.), and high-level +//! wrappers for GameObjects and Components. + +mod component; +mod game_object; +mod ids; +mod transform; +mod values; + +pub use component::{Component, GenericComponent}; +pub use game_object::GameObject; +pub use ids::{FileID, LocalID}; +pub use transform::{RectTransform, Transform}; +pub use values::{Color, ExternalRef, FileRef, Quaternion, Vector2, Vector3}; diff --git a/src/types/transform.rs b/src/types/transform.rs new file mode 100644 index 0000000..30f2064 --- /dev/null +++ b/src/types/transform.rs @@ -0,0 +1,398 @@ +//! Transform and RectTransform component wrappers + +use crate::model::UnityDocument; +use crate::types::{Component, FileRef, Quaternion, Vector2, Vector3}; + +/// A wrapper around a UnityDocument that represents a Transform component +/// +/// Provides convenient access to Transform properties like position, rotation, and scale. +/// +/// # Examples +/// +/// ```no_run +/// use cursebreaker_parser::{UnityFile, types::Transform}; +/// +/// let file = UnityFile::from_path("Scene.unity")?; +/// for doc in &file.documents { +/// if let Some(transform) = Transform::new(doc) { +/// if let Some(pos) = transform.local_position() { +/// println!("Position: ({}, {}, {})", pos.x, pos.y, pos.z); +/// } +/// } +/// } +/// # Ok::<(), cursebreaker_parser::Error>(()) +/// ``` +#[derive(Debug)] +pub struct Transform<'a> { + document: &'a UnityDocument, +} + +impl<'a> Transform<'a> { + /// Create a Transform wrapper from a UnityDocument + /// + /// Returns None if the document is not a Transform or RectTransform. + pub fn new(document: &'a UnityDocument) -> Option { + if document.class_name == "Transform" || document.class_name == "RectTransform" { + Some(Self { document }) + } else { + None + } + } + + /// Get the local position of this transform + pub fn local_position(&self) -> Option<&Vector3> { + self.get_transform_props() + .and_then(|props| props.get("m_LocalPosition")) + .and_then(|v| v.as_vector3()) + } + + /// Get the local rotation of this transform + pub fn local_rotation(&self) -> Option<&Quaternion> { + self.get_transform_props() + .and_then(|props| props.get("m_LocalRotation")) + .and_then(|v| v.as_quaternion()) + } + + /// Get the local scale of this transform + pub fn local_scale(&self) -> Option<&Vector3> { + self.get_transform_props() + .and_then(|props| props.get("m_LocalScale")) + .and_then(|v| v.as_vector3()) + } + + /// Get the parent transform reference + pub fn parent(&self) -> Option { + self.get_transform_props() + .and_then(|props| props.get("m_Father")) + .and_then(|v| v.as_file_ref()) + .copied() + } + + /// Get the list of child transform references + pub fn children(&self) -> Vec { + self.get_transform_props() + .and_then(|props| props.get("m_Children")) + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|item| item.as_file_ref()) + .copied() + .collect() + }) + .unwrap_or_default() + } + + /// Helper to get the transform properties object + fn get_transform_props(&self) -> Option<&indexmap::IndexMap> { + self.document + .get(&self.document.class_name) + .and_then(|v| v.as_object()) + } +} + +impl<'a> Component for Transform<'a> { + fn game_object(&self) -> Option { + self.get_transform_props() + .and_then(|props| props.get("m_GameObject")) + .and_then(|v| v.as_file_ref()) + .copied() + } + + fn is_enabled(&self) -> bool { + true // Transforms are always enabled + } + + fn document(&self) -> &UnityDocument { + self.document + } +} + +/// A wrapper around a UnityDocument that represents a RectTransform component +/// +/// RectTransform is used for UI elements and extends Transform with additional properties. +/// +/// # Examples +/// +/// ```no_run +/// use cursebreaker_parser::{UnityFile, types::RectTransform}; +/// +/// let file = UnityFile::from_path("Canvas.prefab")?; +/// for doc in &file.documents { +/// if let Some(rect_transform) = RectTransform::new(doc) { +/// if let Some(anchor_min) = rect_transform.anchor_min() { +/// println!("Anchor Min: ({}, {})", anchor_min.x, anchor_min.y); +/// } +/// } +/// } +/// # Ok::<(), cursebreaker_parser::Error>(()) +/// ``` +#[derive(Debug)] +pub struct RectTransform<'a> { + transform: Transform<'a>, +} + +impl<'a> RectTransform<'a> { + /// Create a RectTransform wrapper from a UnityDocument + /// + /// Returns None if the document is not a RectTransform. + pub fn new(document: &'a UnityDocument) -> Option { + if document.class_name == "RectTransform" { + Some(Self { + transform: Transform { document }, + }) + } else { + None + } + } + + /// Get the anchor min (bottom-left anchor) + pub fn anchor_min(&self) -> Option<&Vector2> { + self.get_rect_props() + .and_then(|props| props.get("m_AnchorMin")) + .and_then(|v| v.as_vector2()) + } + + /// Get the anchor max (top-right anchor) + pub fn anchor_max(&self) -> Option<&Vector2> { + self.get_rect_props() + .and_then(|props| props.get("m_AnchorMax")) + .and_then(|v| v.as_vector2()) + } + + /// Get the anchored position + pub fn anchored_position(&self) -> Option<&Vector2> { + self.get_rect_props() + .and_then(|props| props.get("m_AnchoredPosition")) + .and_then(|v| v.as_vector2()) + } + + /// Get the size delta + pub fn size_delta(&self) -> Option<&Vector2> { + self.get_rect_props() + .and_then(|props| props.get("m_SizeDelta")) + .and_then(|v| v.as_vector2()) + } + + /// Get the pivot point + pub fn pivot(&self) -> Option<&Vector2> { + self.get_rect_props() + .and_then(|props| props.get("m_Pivot")) + .and_then(|v| v.as_vector2()) + } + + /// Get the local position (from Transform) + pub fn local_position(&self) -> Option<&Vector3> { + self.transform.local_position() + } + + /// Get the local rotation (from Transform) + pub fn local_rotation(&self) -> Option<&Quaternion> { + self.transform.local_rotation() + } + + /// Get the local scale (from Transform) + pub fn local_scale(&self) -> Option<&Vector3> { + self.transform.local_scale() + } + + /// Get the parent transform reference (from Transform) + pub fn parent(&self) -> Option { + self.transform.parent() + } + + /// Get the list of child transform references (from Transform) + pub fn children(&self) -> Vec { + self.transform.children() + } + + /// Helper to get the RectTransform properties object + fn get_rect_props(&self) -> Option<&indexmap::IndexMap> { + self.transform.get_transform_props() + } +} + +impl<'a> Component for RectTransform<'a> { + fn game_object(&self) -> Option { + self.transform.game_object() + } + + fn is_enabled(&self) -> bool { + self.transform.is_enabled() + } + + fn document(&self) -> &UnityDocument { + self.transform.document() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::property::PropertyValue; + use crate::types::FileID; + use indexmap::IndexMap; + + fn create_test_transform() -> UnityDocument { + let mut properties = IndexMap::new(); + + let mut transform_props = IndexMap::new(); + transform_props.insert( + "m_LocalPosition".to_string(), + PropertyValue::Vector3(Vector3::new(1.0, 2.0, 3.0)), + ); + transform_props.insert( + "m_LocalRotation".to_string(), + PropertyValue::Quaternion(Quaternion::identity()), + ); + transform_props.insert( + "m_LocalScale".to_string(), + PropertyValue::Vector3(Vector3::one()), + ); + transform_props.insert( + "m_GameObject".to_string(), + PropertyValue::FileRef(crate::types::FileRef::new(FileID::from_i64(67890))), + ); + transform_props.insert("m_Children".to_string(), PropertyValue::Array(vec![])); + + properties.insert("Transform".to_string(), PropertyValue::Object(transform_props)); + + UnityDocument { + type_id: 4, + file_id: FileID::from_i64(12345), + class_name: "Transform".to_string(), + properties, + } + } + + fn create_test_rect_transform() -> UnityDocument { + let mut properties = IndexMap::new(); + + let mut rect_props = IndexMap::new(); + rect_props.insert( + "m_LocalPosition".to_string(), + PropertyValue::Vector3(Vector3::zero()), + ); + rect_props.insert( + "m_LocalRotation".to_string(), + PropertyValue::Quaternion(Quaternion::identity()), + ); + rect_props.insert( + "m_LocalScale".to_string(), + PropertyValue::Vector3(Vector3::one()), + ); + rect_props.insert( + "m_AnchorMin".to_string(), + PropertyValue::Vector2(Vector2::zero()), + ); + rect_props.insert( + "m_AnchorMax".to_string(), + PropertyValue::Vector2(Vector2::one()), + ); + rect_props.insert( + "m_AnchoredPosition".to_string(), + PropertyValue::Vector2(Vector2::zero()), + ); + rect_props.insert( + "m_SizeDelta".to_string(), + PropertyValue::Vector2(Vector2::new(100.0, 50.0)), + ); + rect_props.insert( + "m_Pivot".to_string(), + PropertyValue::Vector2(Vector2::new(0.5, 0.5)), + ); + + properties.insert("RectTransform".to_string(), PropertyValue::Object(rect_props)); + + UnityDocument { + type_id: 224, + file_id: FileID::from_i64(12345), + class_name: "RectTransform".to_string(), + properties, + } + } + + #[test] + fn test_transform_creation() { + let doc = create_test_transform(); + let transform = Transform::new(&doc); + assert!(transform.is_some()); + } + + #[test] + fn test_transform_local_position() { + let doc = create_test_transform(); + let transform = Transform::new(&doc).unwrap(); + let pos = transform.local_position().unwrap(); + assert_eq!(pos.x, 1.0); + assert_eq!(pos.y, 2.0); + assert_eq!(pos.z, 3.0); + } + + #[test] + fn test_transform_local_rotation() { + let doc = create_test_transform(); + let transform = Transform::new(&doc).unwrap(); + let rot = transform.local_rotation().unwrap(); + assert_eq!(rot.w, 1.0); + } + + #[test] + fn test_transform_local_scale() { + let doc = create_test_transform(); + let transform = Transform::new(&doc).unwrap(); + let scale = transform.local_scale().unwrap(); + assert_eq!(scale, &Vector3::one()); + } + + #[test] + fn test_rect_transform_creation() { + let doc = create_test_rect_transform(); + let rect_transform = RectTransform::new(&doc); + assert!(rect_transform.is_some()); + } + + #[test] + fn test_rect_transform_anchor_min() { + let doc = create_test_rect_transform(); + let rect_transform = RectTransform::new(&doc).unwrap(); + let anchor_min = rect_transform.anchor_min().unwrap(); + assert_eq!(anchor_min, &Vector2::zero()); + } + + #[test] + fn test_rect_transform_anchor_max() { + let doc = create_test_rect_transform(); + let rect_transform = RectTransform::new(&doc).unwrap(); + let anchor_max = rect_transform.anchor_max().unwrap(); + assert_eq!(anchor_max, &Vector2::one()); + } + + #[test] + fn test_rect_transform_size_delta() { + let doc = create_test_rect_transform(); + let rect_transform = RectTransform::new(&doc).unwrap(); + let size_delta = rect_transform.size_delta().unwrap(); + assert_eq!(size_delta.x, 100.0); + assert_eq!(size_delta.y, 50.0); + } + + #[test] + fn test_rect_transform_pivot() { + let doc = create_test_rect_transform(); + let rect_transform = RectTransform::new(&doc).unwrap(); + let pivot = rect_transform.pivot().unwrap(); + assert_eq!(pivot.x, 0.5); + assert_eq!(pivot.y, 0.5); + } + + #[test] + fn test_rect_transform_inherits_from_transform() { + let doc = create_test_rect_transform(); + let rect_transform = RectTransform::new(&doc).unwrap(); + + // Test inherited methods + assert!(rect_transform.local_position().is_some()); + assert!(rect_transform.local_rotation().is_some()); + assert!(rect_transform.local_scale().is_some()); + } +} diff --git a/src/types/values.rs b/src/types/values.rs new file mode 100644 index 0000000..59d10b8 --- /dev/null +++ b/src/types/values.rs @@ -0,0 +1,303 @@ +//! Unity-specific value types + +use super::FileID; + +/// A 2D vector used in Unity +/// +/// # Examples +/// +/// ``` +/// use cursebreaker_parser::Vector2; +/// +/// let v = Vector2::new(1.0, 2.0); +/// assert_eq!(v.x, 1.0); +/// assert_eq!(v.y, 2.0); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Vector2 { + pub x: f32, + pub y: f32, +} + +impl Vector2 { + /// Create a new Vector2 + pub fn new(x: f32, y: f32) -> Self { + Self { x, y } + } + + /// Create a zero vector (0, 0) + pub fn zero() -> Self { + Self::new(0.0, 0.0) + } + + /// Create a one vector (1, 1) + pub fn one() -> Self { + Self::new(1.0, 1.0) + } +} + +/// A 3D vector used in Unity for positions, scales, etc. +/// +/// # Examples +/// +/// ``` +/// use cursebreaker_parser::Vector3; +/// +/// let v = Vector3::new(1.0, 2.0, 3.0); +/// assert_eq!(v.x, 1.0); +/// assert_eq!(v.y, 2.0); +/// assert_eq!(v.z, 3.0); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Vector3 { + pub x: f32, + pub y: f32, + pub z: f32, +} + +impl Vector3 { + /// Create a new Vector3 + pub fn new(x: f32, y: f32, z: f32) -> Self { + Self { x, y, z } + } + + /// Create a zero vector (0, 0, 0) + pub fn zero() -> Self { + Self::new(0.0, 0.0, 0.0) + } + + /// Create a one vector (1, 1, 1) + pub fn one() -> Self { + Self::new(1.0, 1.0, 1.0) + } + + /// Create an up vector (0, 1, 0) + pub fn up() -> Self { + Self::new(0.0, 1.0, 0.0) + } + + /// Create a forward vector (0, 0, 1) + pub fn forward() -> Self { + Self::new(0.0, 0.0, 1.0) + } + + /// Create a right vector (1, 0, 0) + pub fn right() -> Self { + Self::new(1.0, 0.0, 0.0) + } +} + +/// A color with RGBA components +/// +/// # Examples +/// +/// ``` +/// use cursebreaker_parser::Color; +/// +/// let c = Color::new(1.0, 0.5, 0.0, 1.0); +/// assert_eq!(c.r, 1.0); +/// assert_eq!(c.a, 1.0); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Color { + pub r: f32, + pub g: f32, + pub b: f32, + pub a: f32, +} + +impl Color { + /// Create a new Color + pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self { + Self { r, g, b, a } + } + + /// Create a white color (1, 1, 1, 1) + pub fn white() -> Self { + Self::new(1.0, 1.0, 1.0, 1.0) + } + + /// Create a black color (0, 0, 0, 1) + pub fn black() -> Self { + Self::new(0.0, 0.0, 0.0, 1.0) + } + + /// Create a transparent color (0, 0, 0, 0) + pub fn clear() -> Self { + Self::new(0.0, 0.0, 0.0, 0.0) + } + + /// Create a red color (1, 0, 0, 1) + pub fn red() -> Self { + Self::new(1.0, 0.0, 0.0, 1.0) + } + + /// Create a green color (0, 1, 0, 1) + pub fn green() -> Self { + Self::new(0.0, 1.0, 0.0, 1.0) + } + + /// Create a blue color (0, 0, 1, 1) + pub fn blue() -> Self { + Self::new(0.0, 0.0, 1.0, 1.0) + } +} + +/// A quaternion used for rotations in Unity +/// +/// # Examples +/// +/// ``` +/// use cursebreaker_parser::Quaternion; +/// +/// let q = Quaternion::identity(); +/// assert_eq!(q.w, 1.0); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Quaternion { + pub x: f32, + pub y: f32, + pub z: f32, + pub w: f32, +} + +impl Quaternion { + /// Create a new Quaternion + pub fn new(x: f32, y: f32, z: f32, w: f32) -> Self { + Self { x, y, z, w } + } + + /// Create an identity quaternion (0, 0, 0, 1) + pub fn identity() -> Self { + Self::new(0.0, 0.0, 0.0, 1.0) + } +} + +/// A reference to another Unity object by file ID +/// +/// # Examples +/// +/// ``` +/// use cursebreaker_parser::{FileRef, FileID}; +/// +/// let file_ref = FileRef::new(FileID::from_i64(12345)); +/// assert_eq!(file_ref.file_id.as_i64(), 12345); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct FileRef { + pub file_id: FileID, +} + +impl FileRef { + /// Create a new FileRef + pub fn new(file_id: FileID) -> Self { + Self { file_id } + } +} + +/// A reference to an external Unity asset by GUID +/// +/// # Examples +/// +/// ``` +/// use cursebreaker_parser::ExternalRef; +/// +/// let ext_ref = ExternalRef::new("abc123".to_string(), 2); +/// assert_eq!(ext_ref.guid, "abc123"); +/// assert_eq!(ext_ref.type_id, 2); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ExternalRef { + pub guid: String, + pub type_id: i32, +} + +impl ExternalRef { + /// Create a new ExternalRef + pub fn new(guid: String, type_id: i32) -> Self { + Self { guid, type_id } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_vector2_creation() { + let v = Vector2::new(1.0, 2.0); + assert_eq!(v.x, 1.0); + assert_eq!(v.y, 2.0); + } + + #[test] + fn test_vector2_zero() { + let v = Vector2::zero(); + assert_eq!(v, Vector2::new(0.0, 0.0)); + } + + #[test] + fn test_vector3_creation() { + let v = Vector3::new(1.0, 2.0, 3.0); + assert_eq!(v.x, 1.0); + assert_eq!(v.y, 2.0); + assert_eq!(v.z, 3.0); + } + + #[test] + fn test_vector3_zero() { + let v = Vector3::zero(); + assert_eq!(v, Vector3::new(0.0, 0.0, 0.0)); + } + + #[test] + fn test_vector3_directions() { + assert_eq!(Vector3::up(), Vector3::new(0.0, 1.0, 0.0)); + assert_eq!(Vector3::forward(), Vector3::new(0.0, 0.0, 1.0)); + assert_eq!(Vector3::right(), Vector3::new(1.0, 0.0, 0.0)); + } + + #[test] + fn test_color_creation() { + let c = Color::new(1.0, 0.5, 0.0, 1.0); + assert_eq!(c.r, 1.0); + assert_eq!(c.g, 0.5); + assert_eq!(c.b, 0.0); + assert_eq!(c.a, 1.0); + } + + #[test] + fn test_color_presets() { + assert_eq!(Color::white(), Color::new(1.0, 1.0, 1.0, 1.0)); + assert_eq!(Color::black(), Color::new(0.0, 0.0, 0.0, 1.0)); + assert_eq!(Color::red(), Color::new(1.0, 0.0, 0.0, 1.0)); + assert_eq!(Color::green(), Color::new(0.0, 1.0, 0.0, 1.0)); + assert_eq!(Color::blue(), Color::new(0.0, 0.0, 1.0, 1.0)); + } + + #[test] + fn test_quaternion_creation() { + let q = Quaternion::new(0.0, 0.0, 0.0, 1.0); + assert_eq!(q.x, 0.0); + assert_eq!(q.w, 1.0); + } + + #[test] + fn test_quaternion_identity() { + let q = Quaternion::identity(); + assert_eq!(q, Quaternion::new(0.0, 0.0, 0.0, 1.0)); + } + + #[test] + fn test_file_ref_creation() { + let file_ref = FileRef::new(FileID::from_i64(12345)); + assert_eq!(file_ref.file_id.as_i64(), 12345); + } + + #[test] + fn test_external_ref_creation() { + let ext_ref = ExternalRef::new("abc123".to_string(), 2); + assert_eq!(ext_ref.guid, "abc123"); + assert_eq!(ext_ref.type_id, 2); + } +} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 3ea385e..072c423 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -25,8 +25,8 @@ fn test_parse_cardgrabber_prefab() { // Verify the name property exists if let Some(go_props) = game_object.get("GameObject") { - if let Some(props) = go_props.as_mapping() { - let has_name = props.keys().any(|k| k.as_str() == Some("m_Name")); + if let Some(props) = go_props.as_object() { + let has_name = props.contains_key("m_Name"); assert!(has_name, "GameObject should have m_Name property"); } }