Skip to main content

openusd/sdf/
value.rs

1use std::collections::HashMap;
2
3use crate::gf::f16;
4use strum::{EnumIs, EnumTryAs, IntoStaticStr};
5
6use crate::gf;
7use crate::tf::Token;
8
9use super::*;
10
11/// A type-erased container for scene description data loaded from a USD file.
12///
13/// This is the Rust equivalent of USD's [`VtValue`](https://openusd.org/dev/api/class_vt_value.html),
14/// representing any value that can appear in a scene description layer. Each variant corresponds
15/// to a USD data type.
16///
17/// Vector and matrix variant suffixes indicate the element type:
18/// - `d` — `f64` (double)
19/// - `f` — `f32` (float)
20/// - `h` — `f16` (half)
21/// - `i` — `i32` (int)
22///
23/// Type-safe extraction is supported via [`TryFrom<Value>`] implementations for common Rust
24/// types (e.g. `f32`, `String`, `gf::Vec3f`).
25#[derive(Debug, Clone, PartialEq, EnumIs, EnumTryAs, IntoStaticStr, derive_more::From)]
26pub enum Value {
27    /// None value, only produced by expressions (not directly assignable).
28    #[from(skip)]
29    None,
30
31    Bool(bool),
32    BoolVec(Vec<bool>),
33
34    Uchar(u8),
35    UcharVec(Vec<u8>),
36
37    Int(i32),
38    IntVec(Vec<i32>),
39
40    Uint(u32),
41    UintVec(Vec<u32>),
42
43    Int64(i64),
44    Int64Vec(Vec<i64>),
45
46    Uint64(u64),
47    Uint64Vec(Vec<u64>),
48
49    Half(f16),
50    HalfVec(Vec<f16>),
51
52    Float(f32),
53    FloatVec(Vec<f32>),
54
55    Double(f64),
56    DoubleVec(Vec<f64>),
57
58    String(String),
59    StringVec(Vec<String>),
60
61    Token(Token),
62    TokenVec(Vec<Token>),
63
64    AssetPath(AssetPath),
65    AssetPathVec(Vec<AssetPath>),
66
67    Quath(gf::Quath),
68    Quatf(gf::Quatf),
69    Quatd(gf::Quatd),
70    QuathVec(Vec<gf::Quath>),
71    QuatfVec(Vec<gf::Quatf>),
72    QuatdVec(Vec<gf::Quatd>),
73
74    Vec2h(gf::Vec2h),
75    Vec2f(gf::Vec2f),
76    Vec2d(gf::Vec2d),
77    Vec2i(gf::Vec2i),
78    Vec2hVec(Vec<gf::Vec2h>),
79    Vec2fVec(Vec<gf::Vec2f>),
80    Vec2dVec(Vec<gf::Vec2d>),
81    Vec2iVec(Vec<gf::Vec2i>),
82
83    Vec3h(gf::Vec3h),
84    Vec3f(gf::Vec3f),
85    Vec3d(gf::Vec3d),
86    Vec3i(gf::Vec3i),
87    Vec3hVec(Vec<gf::Vec3h>),
88    Vec3fVec(Vec<gf::Vec3f>),
89    Vec3dVec(Vec<gf::Vec3d>),
90    Vec3iVec(Vec<gf::Vec3i>),
91
92    Vec4h(gf::Vec4h),
93    Vec4f(gf::Vec4f),
94    Vec4d(gf::Vec4d),
95    Vec4i(gf::Vec4i),
96    Vec4hVec(Vec<gf::Vec4h>),
97    Vec4fVec(Vec<gf::Vec4f>),
98    Vec4dVec(Vec<gf::Vec4d>),
99    Vec4iVec(Vec<gf::Vec4i>),
100
101    Matrix2d(gf::Mat2d),
102    Matrix3d(gf::Mat3d),
103    Matrix4d(gf::Matrix4d),
104    Matrix2dVec(Vec<gf::Mat2d>),
105    Matrix3dVec(Vec<gf::Mat3d>),
106    Matrix4dVec(Vec<gf::Matrix4d>),
107
108    Specifier(Specifier),
109    Permission(Permission),
110    Variability(Variability),
111
112    Dictionary(HashMap<String, Value>),
113
114    TokenListOp(TokenListOp),
115    StringListOp(StringListOp),
116    PathListOp(PathListOp),
117    ReferenceListOp(ReferenceListOp),
118    IntListOp(IntListOp),
119    Int64ListOp(Int64ListOp),
120    UIntListOp(UintListOp),
121    UInt64ListOp(Uint64ListOp),
122    PayloadListOp(PayloadListOp),
123
124    Payload(Payload),
125    PathVec(Vec<Path>),
126    /// Layer-level relocates: `(source, target)` path pairs for namespace remapping.
127    Relocates(RelocateList),
128    VariantSelectionMap(HashMap<String, String>),
129    TimeSamples(TimeSampleMap),
130
131    LayerOffsetVec(Vec<LayerOffset>),
132
133    #[from(skip)]
134    ValueBlock,
135    #[from(skip)]
136    Value,
137
138    /// Heterogeneous array (e.g. spline knots). Each element is a `Value`.
139    ValueVec(Vec<Value>),
140
141    #[from(skip)]
142    UnregisteredValue(String),
143    #[from(skip)]
144    UnregisteredValueListOp(StringListOp),
145
146    TimeCode(TimeCode),
147    TimeCodeVec(Vec<TimeCode>),
148    #[from(skip)]
149    PathExpression(String),
150}
151
152#[cfg(feature = "serde")]
153impl serde::Serialize for Value {
154    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
155        use serde::ser::SerializeMap;
156
157        match self {
158            Value::None | Value::ValueBlock | Value::Value => serializer.serialize_none(),
159
160            Value::Bool(v) => v.serialize(serializer),
161            Value::BoolVec(v) => v.serialize(serializer),
162            Value::Uchar(v) => v.serialize(serializer),
163            Value::UcharVec(v) => v.serialize(serializer),
164            Value::Int(v) => v.serialize(serializer),
165            Value::IntVec(v) => v.serialize(serializer),
166            Value::Uint(v) => v.serialize(serializer),
167            Value::UintVec(v) => v.serialize(serializer),
168            Value::Int64(v) => v.serialize(serializer),
169            Value::Int64Vec(v) => v.serialize(serializer),
170            Value::Uint64(v) => v.serialize(serializer),
171            Value::Uint64Vec(v) => v.serialize(serializer),
172            Value::Half(v) => v.serialize(serializer),
173            Value::HalfVec(v) => v.serialize(serializer),
174            Value::Float(v) => v.serialize(serializer),
175            Value::FloatVec(v) => v.serialize(serializer),
176            Value::Double(v) => v.serialize(serializer),
177            Value::DoubleVec(v) => v.serialize(serializer),
178            Value::TimeCode(v) => v.serialize(serializer),
179            Value::TimeCodeVec(v) => v.serialize(serializer),
180
181            Value::String(v) | Value::PathExpression(v) => v.serialize(serializer),
182            Value::Token(v) => v.serialize(serializer),
183            Value::AssetPath(v) => v.serialize(serializer),
184            Value::StringVec(v) => v.serialize(serializer),
185            Value::TokenVec(v) => v.serialize(serializer),
186            Value::AssetPathVec(v) => v.serialize(serializer),
187
188            Value::Vec2h(v) => v.serialize(serializer),
189            Value::Vec3h(v) => v.serialize(serializer),
190            Value::Vec4h(v) => v.serialize(serializer),
191            Value::Quath(v) => v.serialize(serializer),
192            Value::Vec2f(v) => v.serialize(serializer),
193            Value::Vec3f(v) => v.serialize(serializer),
194            Value::Vec4f(v) => v.serialize(serializer),
195            Value::Quatf(v) => v.serialize(serializer),
196            Value::Vec2d(v) => v.serialize(serializer),
197            Value::Vec3d(v) => v.serialize(serializer),
198            Value::Vec4d(v) => v.serialize(serializer),
199            Value::Quatd(v) => v.serialize(serializer),
200            Value::Vec2i(v) => v.serialize(serializer),
201            Value::Vec3i(v) => v.serialize(serializer),
202            Value::Vec4i(v) => v.serialize(serializer),
203
204            Value::Vec2hVec(v) => v.serialize(serializer),
205            Value::Vec3hVec(v) => v.serialize(serializer),
206            Value::Vec4hVec(v) => v.serialize(serializer),
207            Value::QuathVec(v) => v.serialize(serializer),
208            Value::Vec2fVec(v) => v.serialize(serializer),
209            Value::Vec3fVec(v) => v.serialize(serializer),
210            Value::Vec4fVec(v) => v.serialize(serializer),
211            Value::QuatfVec(v) => v.serialize(serializer),
212            Value::Vec2dVec(v) => v.serialize(serializer),
213            Value::Vec3dVec(v) => v.serialize(serializer),
214            Value::Vec4dVec(v) => v.serialize(serializer),
215            Value::QuatdVec(v) => v.serialize(serializer),
216            Value::Vec2iVec(v) => v.serialize(serializer),
217            Value::Vec3iVec(v) => v.serialize(serializer),
218            Value::Vec4iVec(v) => v.serialize(serializer),
219
220            Value::Matrix2d(m) => m.0.chunks(2).collect::<Vec<_>>().serialize(serializer),
221            Value::Matrix3d(m) => m.0.chunks(3).collect::<Vec<_>>().serialize(serializer),
222            Value::Matrix4d(m) => m.0.chunks(4).collect::<Vec<_>>().serialize(serializer),
223            Value::Matrix2dVec(v) => v
224                .iter()
225                .map(|m| m.0.chunks(2).collect::<Vec<_>>())
226                .collect::<Vec<_>>()
227                .serialize(serializer),
228            Value::Matrix3dVec(v) => v
229                .iter()
230                .map(|m| m.0.chunks(3).collect::<Vec<_>>())
231                .collect::<Vec<_>>()
232                .serialize(serializer),
233            Value::Matrix4dVec(v) => v
234                .iter()
235                .map(|m| m.0.chunks(4).collect::<Vec<_>>())
236                .collect::<Vec<_>>()
237                .serialize(serializer),
238
239            Value::Specifier(v) => v.serialize(serializer),
240            Value::Permission(v) => v.serialize(serializer),
241            Value::Variability(v) => v.serialize(serializer),
242            Value::Dictionary(v) => v.serialize(serializer),
243
244            Value::TokenListOp(v) => v.serialize(serializer),
245            Value::StringListOp(v) => v.serialize(serializer),
246            Value::PathListOp(v) => v.serialize(serializer),
247            Value::ReferenceListOp(v) => v.serialize(serializer),
248            Value::IntListOp(v) => v.serialize(serializer),
249            Value::Int64ListOp(v) => v.serialize(serializer),
250            Value::UIntListOp(v) => v.serialize(serializer),
251            Value::UInt64ListOp(v) => v.serialize(serializer),
252            Value::PayloadListOp(v) => v.serialize(serializer),
253
254            Value::Payload(v) => v.serialize(serializer),
255            Value::PathVec(v) => v.serialize(serializer),
256            Value::Relocates(v) => {
257                let mut map = serializer.serialize_map(Some(v.len()))?;
258                for (src, tgt) in v {
259                    map.serialize_entry(src.as_str(), tgt.as_str())?;
260                }
261                map.end()
262            }
263            Value::VariantSelectionMap(v) => v.serialize(serializer),
264            Value::LayerOffsetVec(v) => v.serialize(serializer),
265
266            Value::ValueVec(v) => v.serialize(serializer),
267
268            Value::UnregisteredValue(v) => v.serialize(serializer),
269            Value::UnregisteredValueListOp(v) => v.serialize(serializer),
270
271            // Time samples serialize as a map with string keys.
272            Value::TimeSamples(v) => {
273                let mut map = serializer.serialize_map(Some(v.len()))?;
274                for (time, value) in v {
275                    let key = if time.fract() == 0.0 && time.is_finite() {
276                        format!("{}", *time as i64)
277                    } else {
278                        format!("{time}")
279                    };
280                    map.serialize_entry(&key, value)?;
281                }
282                map.end()
283            }
284        }
285    }
286}
287
288/// Error returned when a [`Value`] cannot be converted to the requested type.
289#[derive(Debug, Clone)]
290pub struct ValueConversionError {
291    expected: &'static str,
292    actual: &'static str,
293}
294
295impl ValueConversionError {
296    /// Creates a new error from the expected type name and the actual value.
297    pub fn new(expected: &'static str, actual: &Value) -> Self {
298        Self {
299            expected,
300            actual: actual.into(),
301        }
302    }
303
304    /// Returns an `Err` with a new conversion error.
305    pub fn err<T>(expected: &'static str, actual: &Value) -> Result<T, Self> {
306        Err(Self::new(expected, actual))
307    }
308}
309
310impl std::fmt::Display for ValueConversionError {
311    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312        write!(f, "expected {}, got {}", self.expected, self.actual)
313    }
314}
315
316impl std::error::Error for ValueConversionError {}
317
318impl Value {
319    /// Extracts the payload as `T` if this holds the matching variant, else
320    /// `None`. A typed view over the `try_as_*` accessors and the
321    /// [`TryFrom<Value>`] impls — e.g. `value.get::<tf::Token>()`. `T = Value`
322    /// returns the value unchanged.
323    pub fn get<T: TryFrom<Value>>(self) -> Option<T> {
324        T::try_from(self).ok()
325    }
326
327    /// Whether this value embeds namespace paths that [`remap_paths`](Self::remap_paths)
328    /// rewrites — `PathVec`, `PathListOp` (relationship targets, attribute
329    /// connections, `inheritPaths`, `specializes`), `Relocates`, and
330    /// `ReferenceListOp` / `PayloadListOp` (whose internal entries target this
331    /// layer's own namespace). The single source of truth a copy value policy
332    /// checks before remapping, so the set of path-bearing variants lives only
333    /// here.
334    pub fn has_embedded_paths(&self) -> bool {
335        matches!(
336            self,
337            Value::PathVec(_)
338                | Value::PathListOp(_)
339                | Value::Relocates(_)
340                | Value::ReferenceListOp(_)
341                | Value::PayloadListOp(_)
342        )
343    }
344
345    /// Returns a copy of this value with every embedded namespace path rewritten
346    /// through `remap`. Paths live in `PathVec`, `PathListOp` (relationship
347    /// targets, attribute connections, `inheritPaths`, `specializes`),
348    /// `Relocates` (source/target pairs), and the prim paths of internal
349    /// (same-layer) `ReferenceListOp` / `PayloadListOp` entries. An external
350    /// reference or payload carries its prim path in the referenced layer's
351    /// namespace, and an empty prim path is the defaultPrim selector — both
352    /// are left untouched; every other value kind is cloned unchanged.
353    ///
354    /// The path-rewriting core behind [`copy_spec`](crate::sdf::copy_spec),
355    /// exposed so callers building a custom copy value policy (flatten, namespace
356    /// editing, rename) can remap paths with their own mapping rather than the
357    /// default root-to-root prefix swap.
358    pub fn remap_paths(&self, remap: impl Fn(&Path) -> Path) -> Value {
359        self.filter_map_paths(|path| Some(remap(path)))
360    }
361
362    /// Returns a copy of this value with every embedded namespace path mapped
363    /// through `remap`, dropping any path it maps to `None`. The dropping form
364    /// of [`remap_paths`](Self::remap_paths): namespace-edit deletion fixup maps
365    /// the targets/connections/internal references that point at a removed object
366    /// to `None` so the entries vanish, while moves map to `Some(new_path)`.
367    ///
368    /// A `PathVec` element or list-op item that maps to `None` is removed. An
369    /// external reference or payload (whose prim path lives in the referenced
370    /// layer's namespace) is never offered to `remap` and is always kept; a
371    /// relocate pair is dropped when its source maps to `None`.
372    pub fn filter_map_paths(&self, remap: impl Fn(&Path) -> Option<Path>) -> Value {
373        let remap = &remap;
374        match self {
375            Value::PathVec(paths) => Value::PathVec(paths.iter().filter_map(remap).collect()),
376            Value::PathListOp(op) => Value::PathListOp(op.clone().filter_map(|p| remap(&p))),
377            Value::Relocates(relocates) => Value::Relocates(
378                relocates
379                    .iter()
380                    // Drop a pair whose source maps away; a target that maps away
381                    // keeps its original (a relocate must keep both endpoints).
382                    .filter_map(|(s, t)| remap(s).map(|s| (s, remap(t).unwrap_or_else(|| t.clone()))))
383                    .collect(),
384            ),
385            Value::ReferenceListOp(op) => Value::ReferenceListOp(op.clone().filter_map(|mut reference| {
386                if reference.asset_path.is_empty() && !reference.prim_path.is_empty() {
387                    reference.prim_path = remap(&reference.prim_path)?;
388                }
389                Some(reference)
390            })),
391            Value::PayloadListOp(op) => Value::PayloadListOp(op.clone().filter_map(|mut payload| {
392                if payload.asset_path.is_empty() && !payload.prim_path.is_empty() {
393                    payload.prim_path = remap(&payload.prim_path)?;
394                }
395                Some(payload)
396            })),
397            other => other.clone(),
398        }
399    }
400}
401
402// Exact extraction: owned conversions that move data out of `Value` without
403// cloning, each requiring the exact held variant. They delegate to the
404// strum-generated `try_as_*()` methods (from the `EnumTryAs` derive);
405// cross-type coercion (`token` → `string`, numeric and vector precision) is
406// the separate, opt-in `Value::cast` tier.
407
408macro_rules! impl_try_from_value {
409    // Exact: unwrap the matching variant.
410    ($target:ty, $method:ident, $label:literal) => {
411        impl TryFrom<Value> for $target {
412            type Error = ValueConversionError;
413
414            fn try_from(value: Value) -> Result<Self, Self::Error> {
415                let tag: &'static str = (&value).into();
416                value.$method().ok_or(ValueConversionError {
417                    expected: $label,
418                    actual: tag,
419                })
420            }
421        }
422    };
423    // Unwrap the matching variant, then convert each element via `Into` — used
424    // by the `gf`-vector-array conversions (`Vec<Vec3f>` → `Vec<[f32; 3]>`).
425    ($target:ty, $method:ident, $label:literal, map_into) => {
426        impl_try_from_value!($target, $method, $label, |v| v
427            .into_iter()
428            .map(Into::into)
429            .collect());
430    };
431    // Unwrap the matching variant, then map it through `$transform` — used by
432    // the `gf`-vector-to-fixed-array conversions (`Vec3f` → `[f32; 3]`).
433    ($target:ty, $method:ident, $label:literal, $transform:expr) => {
434        impl TryFrom<Value> for $target {
435            type Error = ValueConversionError;
436
437            fn try_from(value: Value) -> Result<Self, Self::Error> {
438                let tag: &'static str = (&value).into();
439                value.$method().map($transform).ok_or(ValueConversionError {
440                    expected: $label,
441                    actual: tag,
442                })
443            }
444        }
445    };
446}
447
448impl_try_from_value!(bool, try_as_bool, "Bool");
449impl_try_from_value!(i32, try_as_int, "Int");
450impl_try_from_value!(u32, try_as_uint, "Uint");
451impl_try_from_value!(i64, try_as_int_64, "Int64");
452impl_try_from_value!(u64, try_as_uint_64, "Uint64");
453impl_try_from_value!(f32, try_as_float, "Float");
454impl_try_from_value!(f64, try_as_double, "Double");
455impl_try_from_value!(Specifier, try_as_specifier, "Specifier");
456impl_try_from_value!(Variability, try_as_variability, "Variability");
457impl_try_from_value!(ReferenceListOp, try_as_reference_list_op, "ReferenceListOp");
458impl_try_from_value!(PayloadListOp, try_as_payload_list_op, "PayloadListOp");
459impl_try_from_value!(PathListOp, try_as_path_list_op, "PathListOp");
460
461// gf scalar types — trivially unwrap the matching variant.
462impl_try_from_value!(gf::Vec2f, try_as_vec_2f, "gf::Vec2f");
463impl_try_from_value!(gf::Vec2d, try_as_vec_2d, "gf::Vec2d");
464impl_try_from_value!(gf::Vec2i, try_as_vec_2i, "gf::Vec2i");
465impl_try_from_value!(gf::Vec2h, try_as_vec_2h, "gf::Vec2h");
466impl_try_from_value!(gf::Vec3f, try_as_vec_3f, "gf::Vec3f");
467impl_try_from_value!(gf::Vec3d, try_as_vec_3d, "gf::Vec3d");
468impl_try_from_value!(gf::Vec3i, try_as_vec_3i, "gf::Vec3i");
469impl_try_from_value!(gf::Vec3h, try_as_vec_3h, "gf::Vec3h");
470impl_try_from_value!(gf::Vec4f, try_as_vec_4f, "gf::Vec4f");
471impl_try_from_value!(gf::Vec4d, try_as_vec_4d, "gf::Vec4d");
472impl_try_from_value!(gf::Vec4i, try_as_vec_4i, "gf::Vec4i");
473impl_try_from_value!(gf::Vec4h, try_as_vec_4h, "gf::Vec4h");
474impl_try_from_value!(gf::Quatf, try_as_quatf, "gf::Quatf");
475impl_try_from_value!(gf::Quatd, try_as_quatd, "gf::Quatd");
476impl_try_from_value!(gf::Quath, try_as_quath, "gf::Quath");
477impl_try_from_value!(gf::Mat2d, try_as_matrix_2d, "gf::Mat2d");
478impl_try_from_value!(gf::Mat3d, try_as_matrix_3d, "gf::Mat3d");
479impl_try_from_value!(gf::Matrix4d, try_as_matrix_4d, "gf::Matrix4d");
480
481// gf array types.
482impl_try_from_value!(Vec<gf::Vec2f>, try_as_vec_2f_vec, "Vec2fVec");
483impl_try_from_value!(Vec<gf::Vec2d>, try_as_vec_2d_vec, "Vec2dVec");
484impl_try_from_value!(Vec<gf::Vec2i>, try_as_vec_2i_vec, "Vec2iVec");
485impl_try_from_value!(Vec<gf::Vec2h>, try_as_vec_2h_vec, "Vec2hVec");
486impl_try_from_value!(Vec<gf::Vec3f>, try_as_vec_3f_vec, "Vec3fVec");
487impl_try_from_value!(Vec<gf::Vec3d>, try_as_vec_3d_vec, "Vec3dVec");
488impl_try_from_value!(Vec<gf::Vec3i>, try_as_vec_3i_vec, "Vec3iVec");
489impl_try_from_value!(Vec<gf::Vec3h>, try_as_vec_3h_vec, "Vec3hVec");
490impl_try_from_value!(Vec<gf::Vec4f>, try_as_vec_4f_vec, "Vec4fVec");
491impl_try_from_value!(Vec<gf::Vec4d>, try_as_vec_4d_vec, "Vec4dVec");
492impl_try_from_value!(Vec<gf::Vec4i>, try_as_vec_4i_vec, "Vec4iVec");
493impl_try_from_value!(Vec<gf::Vec4h>, try_as_vec_4h_vec, "Vec4hVec");
494impl_try_from_value!(Vec<gf::Quatf>, try_as_quatf_vec, "QuatfVec");
495impl_try_from_value!(Vec<gf::Quatd>, try_as_quatd_vec, "QuatdVec");
496impl_try_from_value!(Vec<gf::Quath>, try_as_quath_vec, "QuathVec");
497impl_try_from_value!(Vec<gf::Mat2d>, try_as_matrix_2d_vec, "Matrix2dVec");
498impl_try_from_value!(Vec<gf::Mat3d>, try_as_matrix_3d_vec, "Matrix3dVec");
499impl_try_from_value!(Vec<gf::Matrix4d>, try_as_matrix_4d_vec, "Matrix4dVec");
500
501// Single-variant string and asset extraction. Coercing `token` → `string`
502// is [`Value::cast`]'s job.
503impl_try_from_value!(String, try_as_string, "String");
504impl_try_from_value!(Token, try_as_token, "Token");
505impl_try_from_value!(Vec<Token>, try_as_token_vec, "TokenVec");
506impl_try_from_value!(Vec<String>, try_as_string_vec, "StringVec");
507impl_try_from_value!(AssetPath, try_as_asset_path, "AssetPath");
508impl_try_from_value!(Vec<AssetPath>, try_as_asset_path_vec, "AssetPathVec");
509
510// List ops, relocations, time samples, and layer offsets — composite payloads
511// read through the typed spec accessors.
512impl_try_from_value!(TokenListOp, try_as_token_list_op, "TokenListOp");
513impl_try_from_value!(RelocateList, try_as_relocates, "Relocates");
514impl_try_from_value!(TimeSampleMap, try_as_time_samples, "TimeSamples");
515impl_try_from_value!(Vec<LayerOffset>, try_as_layer_offset_vec, "LayerOffsetVec");
516
517// Exact numeric arrays — `float[]` / `double[]`. Flattening a single vector
518// into a scalar array is a coercion, so it lives in [`Value::cast`].
519impl_try_from_value!(Vec<f32>, try_as_float_vec, "FloatVec");
520impl_try_from_value!(Vec<f64>, try_as_double_vec, "DoubleVec");
521
522// `gf` vector/quaternion variants as fixed-size arrays, via the type's `Into`.
523// Coercing across element precisions is [`Value::cast`]'s job.
524impl_try_from_value!([f32; 2], try_as_vec_2f, "gf::Vec2f", Into::into);
525impl_try_from_value!([f32; 3], try_as_vec_3f, "gf::Vec3f", Into::into);
526impl_try_from_value!([f32; 4], try_as_vec_4f, "gf::Vec4f", Into::into);
527impl_try_from_value!([f64; 2], try_as_vec_2d, "gf::Vec2d", Into::into);
528impl_try_from_value!([f64; 3], try_as_vec_3d, "gf::Vec3d", Into::into);
529impl_try_from_value!([f64; 4], try_as_vec_4d, "gf::Vec4d", Into::into);
530
531// `gf` vector arrays as arrays of fixed-size arrays.
532impl_try_from_value!(Vec<[f32; 2]>, try_as_vec_2f_vec, "Vec2fVec", map_into);
533impl_try_from_value!(Vec<[f32; 3]>, try_as_vec_3f_vec, "Vec3fVec", map_into);
534impl_try_from_value!(Vec<[f32; 4]>, try_as_vec_4f_vec, "Vec4fVec", map_into);
535
536/// Convert from `&str` to `Value`.
537///
538/// Used a lot in text parser since all tokens are basically strings.
539impl<'a> From<&'a str> for Value {
540    fn from(value: &'a str) -> Self {
541        Value::String(value.to_string())
542    }
543}
544
545impl From<[f32; 2]> for Value {
546    fn from(v: [f32; 2]) -> Self {
547        Value::Vec2f(v.into())
548    }
549}
550impl From<[f32; 3]> for Value {
551    fn from(v: [f32; 3]) -> Self {
552        Value::Vec3f(v.into())
553    }
554}
555impl From<[f64; 2]> for Value {
556    fn from(v: [f64; 2]) -> Self {
557        Value::Vec2d(v.into())
558    }
559}
560impl From<[f64; 3]> for Value {
561    fn from(v: [f64; 3]) -> Self {
562        Value::Vec3d(v.into())
563    }
564}
565impl From<[i32; 2]> for Value {
566    fn from(v: [i32; 2]) -> Self {
567        Value::Vec2i(v.into())
568    }
569}
570impl From<[i32; 3]> for Value {
571    fn from(v: [i32; 3]) -> Self {
572        Value::Vec3i(v.into())
573    }
574}
575impl From<[i32; 4]> for Value {
576    fn from(v: [i32; 4]) -> Self {
577        Value::Vec4i(v.into())
578    }
579}
580impl From<[f64; 16]> for Value {
581    fn from(v: [f64; 16]) -> Self {
582        Value::Matrix4d(gf::Matrix4d(v))
583    }
584}
585
586// Shorthand constructors for the gf vector and quaternion variants,
587// mirroring the `gf::vec3f(x, y, z)` free-function convention.
588impl Value {
589    pub fn vec2f(x: f32, y: f32) -> Self {
590        Self::Vec2f(gf::vec2f(x, y))
591    }
592    pub fn vec3f(x: f32, y: f32, z: f32) -> Self {
593        Self::Vec3f(gf::vec3f(x, y, z))
594    }
595    pub fn vec4f(x: f32, y: f32, z: f32, w: f32) -> Self {
596        Self::Vec4f(gf::vec4f(x, y, z, w))
597    }
598    pub fn vec2d(x: f64, y: f64) -> Self {
599        Self::Vec2d(gf::vec2d(x, y))
600    }
601    pub fn vec3d(x: f64, y: f64, z: f64) -> Self {
602        Self::Vec3d(gf::vec3d(x, y, z))
603    }
604    pub fn vec4d(x: f64, y: f64, z: f64, w: f64) -> Self {
605        Self::Vec4d(gf::vec4d(x, y, z, w))
606    }
607    pub fn vec2i(x: i32, y: i32) -> Self {
608        Self::Vec2i(gf::vec2i(x, y))
609    }
610    pub fn vec3i(x: i32, y: i32, z: i32) -> Self {
611        Self::Vec3i(gf::vec3i(x, y, z))
612    }
613    pub fn vec4i(x: i32, y: i32, z: i32, w: i32) -> Self {
614        Self::Vec4i(gf::vec4i(x, y, z, w))
615    }
616    pub fn vec2h(x: f16, y: f16) -> Self {
617        Self::Vec2h(gf::vec2h(x, y))
618    }
619    pub fn vec3h(x: f16, y: f16, z: f16) -> Self {
620        Self::Vec3h(gf::vec3h(x, y, z))
621    }
622    pub fn vec4h(x: f16, y: f16, z: f16, w: f16) -> Self {
623        Self::Vec4h(gf::vec4h(x, y, z, w))
624    }
625    pub fn quatf(w: f32, x: f32, y: f32, z: f32) -> Self {
626        Self::Quatf(gf::quatf(w, x, y, z))
627    }
628    pub fn quatd(w: f64, x: f64, y: f64, z: f64) -> Self {
629        Self::Quatd(gf::quatd(w, x, y, z))
630    }
631    pub fn quath(w: f16, x: f16, y: f16, z: f16) -> Self {
632        Self::Quath(gf::quath(w, x, y, z))
633    }
634
635    /// Builds a [`Value::Token`] from any string-like value, so callers need not
636    /// name [`Token`] at the construction site (`Value::token("Mesh")`).
637    pub fn token(name: impl Into<Token>) -> Self {
638        Self::Token(name.into())
639    }
640
641    /// Builds a [`Value::TokenVec`] from any iterator of string-like items —
642    /// the counterpart of [`Value::token`] for name lists such as
643    /// `primChildren` and `allowedTokens`.
644    pub fn token_vec(items: impl IntoIterator<Item = impl Into<Token>>) -> Self {
645        Self::TokenVec(items.into_iter().map(Into::into).collect())
646    }
647
648    /// Borrows the inner string of a string-like scalar — `asset`, `string`,
649    /// or `token` — and `None` for any other variant.
650    ///
651    /// An asset path may be authored as a plain string or token, so reading
652    /// one coerces across all three (the borrowing counterpart to the owned
653    /// [`TryFrom<Value>`] for [`String`]).
654    pub fn as_str(&self) -> Option<&str> {
655        match self {
656            Value::String(s) => Some(s),
657            Value::Token(s) => Some(s.as_str()),
658            Value::AssetPath(s) => Some(s.as_str()),
659            _ => None,
660        }
661    }
662
663    /// Coerces this value to `T` through the registered casts, mirroring C++
664    /// `VtValue::Cast`: numeric scalars cross-convert (range-checked), `string`
665    /// ↔ `token`, and same-dimension vectors/quaternions change precision.
666    ///
667    /// Unlike `T::try_from` / [`Attribute::get`](crate::usd::Attribute::get),
668    /// which require the exact held variant, `cast` is the coercing tier — read
669    /// the raw value and cast when the authored type may differ from the wanted
670    /// one: `attr.get::<Value>()?.map(|v| v.cast::<f32>()).transpose()?`.
671    pub fn cast<T: FromValueCast>(self) -> Result<T, CastError> {
672        T::cast_from(self)
673    }
674}
675
676/// A type that [`Value::cast`] can coerce a [`Value`] into — the Rust side of
677/// C++ `VtValue`'s registered cast set. Takes the value by move so a
678/// same-variant cast (e.g. `StringVec` to `Vec<String>`) reuses its buffer
679/// rather than cloning, matching the owned [`TryFrom<Value>`] it complements.
680///
681/// The implemented targets are demand-driven — the scalar, string/token, and
682/// the specific array/vector shapes current callers read. The set is not the
683/// full precision×dimension matrix: a missing target (e.g. `cast::<gf::Vec2f>`)
684/// is a compile error, signalling "add the impl when a caller needs it" rather
685/// than a silent gap. Add new targets here as the need arises.
686pub trait FromValueCast: Sized {
687    /// Coerces `value` to `Self`, or reports why it cannot.
688    fn cast_from(value: Value) -> Result<Self, CastError>;
689}
690
691/// Error returned by [`Value::cast`].
692#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
693pub enum CastError {
694    /// The held variant has no registered cast to the target type.
695    #[error("cannot cast {actual} to {target}")]
696    TypeMismatch {
697        /// The requested target type.
698        target: &'static str,
699        /// The actual held variant.
700        actual: &'static str,
701    },
702    /// The numeric value is outside the target type's representable range
703    /// (faithful to C++ `numeric_cast`).
704    #[error("{actual} value is out of range for {target}")]
705    OutOfRange {
706        /// The requested target type.
707        target: &'static str,
708        /// The actual held variant.
709        actual: &'static str,
710    },
711}
712
713impl CastError {
714    fn mismatch<T>(actual: &'static str) -> Self {
715        Self::TypeMismatch {
716            target: std::any::type_name::<T>(),
717            actual,
718        }
719    }
720
721    fn out_of_range<T>(actual: &'static str) -> Self {
722        Self::OutOfRange {
723            target: std::any::type_name::<T>(),
724            actual,
725        }
726    }
727}
728
729/// Range-checked numeric coercion shared by every scalar `FromValueCast` target.
730/// `num_traits::NumCast` is `boost::numeric_cast`: it returns `None` when an
731/// integer target overflows, which maps to [`CastError::OutOfRange`].
732///
733/// Narrowing float conversions (`f64` → `f32`/`f16`) are not range-checked by
734/// `NumCast` — they saturate to infinity instead of returning `None` — so a
735/// finite source that produces a non-finite result is reported as out of range
736/// too. An already-infinite source legitimately stays infinite.
737fn cast_numeric<T: num_traits::NumCast>(value: Value) -> Result<T, CastError> {
738    use num_traits::NumCast;
739    let actual: &'static str = (&value).into();
740    let src_finite = match &value {
741        Value::Half(v) => v.is_finite(),
742        Value::Float(v) => v.is_finite(),
743        Value::Double(v) => v.is_finite(),
744        _ => true,
745    };
746    let out: T = match value {
747        Value::Uchar(v) => NumCast::from(v),
748        Value::Int(v) => NumCast::from(v),
749        Value::Uint(v) => NumCast::from(v),
750        Value::Int64(v) => NumCast::from(v),
751        Value::Uint64(v) => NumCast::from(v),
752        Value::Half(v) => NumCast::from(v),
753        Value::Float(v) => NumCast::from(v),
754        Value::Double(v) => NumCast::from(v),
755        Value::Bool(v) => NumCast::from(v as u8),
756        _ => return Err(CastError::mismatch::<T>(actual)),
757    }
758    .ok_or_else(|| CastError::out_of_range::<T>(actual))?;
759    if src_finite && out.to_f64().is_some_and(|f| !f.is_finite()) {
760        return Err(CastError::out_of_range::<T>(actual));
761    }
762    Ok(out)
763}
764
765macro_rules! impl_cast_numeric {
766    ($($t:ty),+ $(,)?) => {$(
767        impl FromValueCast for $t {
768            fn cast_from(value: Value) -> Result<Self, CastError> {
769                cast_numeric(value)
770            }
771        }
772    )+};
773}
774
775impl_cast_numeric!(u8, i32, u32, i64, u64, f16, f32, f64);
776
777impl FromValueCast for bool {
778    fn cast_from(value: Value) -> Result<Self, CastError> {
779        match value {
780            Value::Bool(b) => Ok(b),
781            other => Err(CastError::mismatch::<bool>((&other).into())),
782        }
783    }
784}
785
786impl FromValueCast for String {
787    fn cast_from(value: Value) -> Result<Self, CastError> {
788        match value {
789            Value::String(s) => Ok(s),
790            Value::Token(t) => Ok(t.into()),
791            Value::AssetPath(a) => Ok(a.authored_path),
792            other => Err(CastError::mismatch::<String>((&other).into())),
793        }
794    }
795}
796
797impl FromValueCast for Token {
798    fn cast_from(value: Value) -> Result<Self, CastError> {
799        match value {
800            Value::Token(t) => Ok(t),
801            Value::String(s) => Ok(Token::from(s)),
802            other => Err(CastError::mismatch::<Token>((&other).into())),
803        }
804    }
805}
806
807impl FromValueCast for Vec<String> {
808    fn cast_from(value: Value) -> Result<Self, CastError> {
809        match value {
810            Value::StringVec(v) => Ok(v),
811            Value::TokenVec(v) => Ok(v.into_iter().map(String::from).collect()),
812            Value::AssetPathVec(v) => Ok(v.into_iter().map(|a| a.authored_path).collect()),
813            other => Err(CastError::mismatch::<Vec<String>>((&other).into())),
814        }
815    }
816}
817
818/// Lifts a `Value`-inspecting helper's `Option` into a cast result, reporting a
819/// type mismatch against `R` (the cast's target type) when no conversion applied.
820fn require<R, U>(value: &Value, converted: Option<U>) -> Result<U, CastError> {
821    converted.ok_or_else(|| CastError::mismatch::<R>(value.into()))
822}
823
824/// Widens a 3-component vector (`f`/`d`/`h`/`i`) to `[f64; 3]`. The `f`/`d`
825/// arms reuse gf's `Into<[f64; 3]>`; `h`/`i` have no such impl and widen here.
826fn vec3_as_f64(value: &Value) -> Option<[f64; 3]> {
827    Some(match value {
828        Value::Vec3f(v) => (*v).into(),
829        Value::Vec3d(v) => (*v).into(),
830        Value::Vec3h(v) => [f64::from(v.x), f64::from(v.y), f64::from(v.z)],
831        Value::Vec3i(v) => [v.x as f64, v.y as f64, v.z as f64],
832        _ => return None,
833    })
834}
835
836/// Widens a 4-component vector or quaternion to `[f64; 4]`. Quaternions extract
837/// in `(w, x, y, z)` order; the `Vec4d`/`Quatf`/`Quatd` arms reuse gf's
838/// `Into<[f64; 4]>` (which encodes that order), and the rest widen here.
839fn vec4_as_f64(value: &Value) -> Option<[f64; 4]> {
840    Some(match value {
841        Value::Vec4f(v) => [v.x as f64, v.y as f64, v.z as f64, v.w as f64],
842        Value::Vec4d(v) => (*v).into(),
843        Value::Vec4h(v) => [f64::from(v.x), f64::from(v.y), f64::from(v.z), f64::from(v.w)],
844        Value::Vec4i(v) => [v.x as f64, v.y as f64, v.z as f64, v.w as f64],
845        Value::Quatf(q) => (*q).into(),
846        Value::Quatd(q) => (*q).into(),
847        Value::Quath(q) => [f64::from(q.w), f64::from(q.x), f64::from(q.y), f64::from(q.z)],
848        _ => return None,
849    })
850}
851
852impl FromValueCast for [f64; 3] {
853    fn cast_from(value: Value) -> Result<Self, CastError> {
854        require::<Self, _>(&value, vec3_as_f64(&value))
855    }
856}
857
858impl FromValueCast for [f64; 4] {
859    fn cast_from(value: Value) -> Result<Self, CastError> {
860        require::<Self, _>(&value, vec4_as_f64(&value))
861    }
862}
863
864impl FromValueCast for [f32; 4] {
865    fn cast_from(value: Value) -> Result<Self, CastError> {
866        let a = require::<Self, _>(&value, vec4_as_f64(&value))?;
867        Ok([a[0] as f32, a[1] as f32, a[2] as f32, a[3] as f32])
868    }
869}
870
871impl FromValueCast for gf::Vec3f {
872    fn cast_from(value: Value) -> Result<Self, CastError> {
873        let [x, y, z] = require::<Self, _>(&value, vec3_as_f64(&value))?;
874        Ok(gf::vec3f(x as f32, y as f32, z as f32))
875    }
876}
877
878impl FromValueCast for Vec<f32> {
879    fn cast_from(value: Value) -> Result<Self, CastError> {
880        match value {
881            Value::FloatVec(v) => Ok(v),
882            Value::DoubleVec(v) => Ok(v.into_iter().map(|d| d as f32).collect()),
883            Value::HalfVec(v) => Ok(v.into_iter().map(f32::from).collect()),
884            Value::Vec2f(v) => Ok(vec![v.x, v.y]),
885            Value::Vec3f(v) => Ok(vec![v.x, v.y, v.z]),
886            Value::Vec4f(v) => Ok(vec![v.x, v.y, v.z, v.w]),
887            other => Err(CastError::mismatch::<Vec<f32>>((&other).into())),
888        }
889    }
890}
891
892impl FromValueCast for Vec<gf::Vec3f> {
893    fn cast_from(value: Value) -> Result<Self, CastError> {
894        match value {
895            Value::Vec3fVec(v) => Ok(v),
896            Value::Vec3dVec(v) => Ok(v
897                .into_iter()
898                .map(|d| gf::vec3f(d.x as f32, d.y as f32, d.z as f32))
899                .collect()),
900            Value::Vec3hVec(v) => Ok(v
901                .into_iter()
902                .map(|h| gf::vec3f(f32::from(h.x), f32::from(h.y), f32::from(h.z)))
903                .collect()),
904            other => Err(CastError::mismatch::<Vec<gf::Vec3f>>((&other).into())),
905        }
906    }
907}
908
909#[cfg(test)]
910mod tests {
911    use super::*;
912
913    #[test]
914    fn test_is() {
915        // Basic sanity checks
916        assert!(Value::Bool(true).is_bool());
917        assert!(!Value::Bool(true).is_bool_vec());
918
919        assert!(Value::Float(1.44).is_float());
920        assert!(!Value::Float(1.44).is_bool());
921        assert!(!Value::Float(1.44).is_float_vec());
922
923        assert!(Value::PayloadListOp(Default::default()).is_payload_list_op());
924        assert!(Value::UnregisteredValue(String::new()).is_unregistered_value());
925    }
926
927    #[test]
928    fn remap_paths_rewrites_embedded() {
929        let p = |s: &str| crate::sdf::path(s).unwrap();
930        let remap = |path: &Path| path.replace_prefix(&p("/A"), &p("/B")).unwrap_or_else(|| path.clone());
931
932        // PathListOp: an in-subtree path re-roots, an outside path is untouched.
933        let op = Value::PathListOp(PathListOp::explicit([p("/A/Child"), p("/Other")]));
934        let items: Vec<String> = op
935            .remap_paths(remap)
936            .try_as_path_list_op()
937            .unwrap()
938            .explicit_items
939            .iter()
940            .map(|p| p.as_str().to_owned())
941            .collect();
942        assert_eq!(items, vec!["/B/Child", "/Other"]);
943
944        // PathVec and both endpoints of a Relocates remap.
945        let pv = Value::PathVec(vec![p("/A/X")]).remap_paths(remap);
946        assert_eq!(pv.try_as_path_vec().unwrap()[0].as_str(), "/B/X");
947        let relocates = Value::Relocates(vec![(p("/A/From"), p("/A/To"))]).remap_paths(remap);
948        let pairs = relocates.try_as_relocates().unwrap();
949        assert_eq!((pairs[0].0.as_str(), pairs[0].1.as_str()), ("/B/From", "/B/To"));
950
951        // A value with no embedded paths is returned unchanged.
952        assert_eq!(Value::Int(7).remap_paths(remap), Value::Int(7));
953    }
954
955    #[test]
956    fn try_from_scalars() {
957        assert!(bool::try_from(Value::Bool(true)).unwrap());
958        assert_eq!(i32::try_from(Value::Int(42)).unwrap(), 42);
959        assert_eq!(f32::try_from(Value::Float(1.5)).unwrap(), 1.5);
960        assert_eq!(f64::try_from(Value::Double(2.5)).unwrap(), 2.5);
961        assert_eq!(String::try_from(Value::String("hello".into())).unwrap(), "hello");
962        // Exact extraction does not coerce a token to a string — that is `cast`.
963        assert!(String::try_from(Value::Token("tok".into())).is_err());
964    }
965
966    #[test]
967    fn try_from_gf_types() {
968        let v3 = gf::vec3f(1.0, 2.0, 3.0);
969        assert_eq!(gf::Vec3f::try_from(Value::Vec3f(v3)).unwrap(), v3);
970
971        let q = gf::quatf(1.0, 0.0, 0.0, 0.0);
972        assert_eq!(gf::Quatf::try_from(Value::Quatf(q)).unwrap(), q);
973
974        let m = gf::Matrix4d::IDENTITY;
975        assert_eq!(gf::Matrix4d::try_from(Value::Matrix4d(m)).unwrap(), m);
976    }
977
978    #[test]
979    fn try_from_fixed_arrays() {
980        let v = gf::vec2f(1.0, 2.0);
981        assert_eq!(<[f32; 2]>::try_from(Value::Vec2f(v)).unwrap(), [1.0, 2.0]);
982
983        let v = gf::vec3f(1.0, 2.0, 3.0);
984        assert_eq!(<[f32; 3]>::try_from(Value::Vec3f(v)).unwrap(), [1.0, 2.0, 3.0]);
985
986        let v = gf::vec4f(1.0, 2.0, 3.0, 4.0);
987        assert_eq!(<[f32; 4]>::try_from(Value::Vec4f(v)).unwrap(), [1.0, 2.0, 3.0, 4.0]);
988
989        let v = gf::vec3d(1.0, 2.0, 3.0);
990        assert_eq!(<[f64; 3]>::try_from(Value::Vec3d(v)).unwrap(), [1.0, 2.0, 3.0]);
991    }
992
993    #[test]
994    fn try_from_vec() {
995        // Exact extraction takes the array variant only; flattening a single
996        // vector into a scalar array is `cast`, not `try_from`.
997        assert_eq!(Vec::<f32>::try_from(Value::FloatVec(vec![1.0])).unwrap(), vec![1.0]);
998        assert!(Vec::<f32>::try_from(Value::Vec3f(gf::vec3f(1.0, 2.0, 3.0))).is_err());
999
1000        assert_eq!(Vec::<f64>::try_from(Value::DoubleVec(vec![1.0])).unwrap(), vec![1.0]);
1001        assert!(Vec::<f64>::try_from(Value::Vec2d(gf::vec2d(1.0, 2.0))).is_err());
1002
1003        // The coercing tier still flattens.
1004        assert_eq!(
1005            Value::Vec3f(gf::vec3f(1.0, 2.0, 3.0)).cast::<Vec<f32>>().unwrap(),
1006            vec![1.0, 2.0, 3.0]
1007        );
1008    }
1009
1010    #[test]
1011    fn try_from_asset_path() {
1012        let scalar = AssetPath::try_from(Value::AssetPath("./tex.png".into())).unwrap();
1013        assert_eq!(scalar, AssetPath::new("./tex.png"));
1014
1015        let array = Vec::<AssetPath>::try_from(Value::AssetPathVec(vec!["a.png".into(), "b.png".into()])).unwrap();
1016        assert_eq!(array, vec![AssetPath::new("a.png"), AssetPath::new("b.png")]);
1017
1018        // A plain string is not an asset path.
1019        assert!(AssetPath::try_from(Value::String("a.png".into())).is_err());
1020
1021        // Round-trips back through the authoring `From` impls.
1022        assert_eq!(Value::from(scalar), Value::AssetPath("./tex.png".into()));
1023        assert_eq!(
1024            Value::from(array),
1025            Value::AssetPathVec(vec!["a.png".into(), "b.png".into()])
1026        );
1027    }
1028
1029    #[test]
1030    fn try_from_wrong_variant() {
1031        let err = f32::try_from(Value::Int(1)).unwrap_err();
1032        assert_eq!(err.to_string(), "expected Float, got Int");
1033
1034        let err = gf::Vec3f::try_from(Value::Bool(true)).unwrap_err();
1035        assert_eq!(err.to_string(), "expected gf::Vec3f, got Bool");
1036    }
1037
1038    #[test]
1039    fn cast_numeric_widen_narrow() {
1040        // Cross-type numeric coercion, both widening and narrowing.
1041        assert_eq!(Value::Int(42).cast::<f64>().unwrap(), 42.0);
1042        assert_eq!(Value::Double(2.0).cast::<i32>().unwrap(), 2);
1043        assert_eq!(Value::Uchar(255).cast::<i32>().unwrap(), 255);
1044        assert_eq!(Value::Half(f16::from_f32(1.5)).cast::<f32>().unwrap(), 1.5);
1045    }
1046
1047    #[test]
1048    fn cast_out_of_range() {
1049        // A double beyond i32's range is a range error, not a type error.
1050        assert!(matches!(
1051            Value::Double(1e40).cast::<i32>(),
1052            Err(CastError::OutOfRange { .. })
1053        ));
1054        // Narrowing a finite double past f32/f16's range saturates to infinity
1055        // in `NumCast`; we report it as out of range, not a silent infinity.
1056        assert!(matches!(
1057            Value::Double(f64::MAX).cast::<f32>(),
1058            Err(CastError::OutOfRange { .. })
1059        ));
1060        assert!(matches!(
1061            Value::Float(f32::MAX).cast::<f16>(),
1062            Err(CastError::OutOfRange { .. })
1063        ));
1064        // An already-infinite source stays infinite — that is not an overflow.
1065        assert_eq!(Value::Double(f64::INFINITY).cast::<f32>().unwrap(), f32::INFINITY);
1066        // A non-numeric variant is a type mismatch.
1067        assert!(matches!(
1068            Value::Bool(true).cast::<gf::Vec3f>(),
1069            Err(CastError::TypeMismatch { .. })
1070        ));
1071    }
1072
1073    #[test]
1074    fn cast_string_token() {
1075        assert_eq!(Value::Token("t".into()).cast::<String>().unwrap(), "t");
1076        assert_eq!(Value::String("s".into()).cast::<Token>().unwrap(), Token::new("s"));
1077        assert_eq!(
1078            Value::TokenVec(vec!["a".into(), "b".into()])
1079                .cast::<Vec<String>>()
1080                .unwrap(),
1081            vec!["a".to_string(), "b".to_string()]
1082        );
1083    }
1084
1085    #[test]
1086    fn cast_vec_precision() {
1087        // Same-dimension precision cast: a double vector reads as Vec3f.
1088        assert_eq!(
1089            Value::Vec3d(gf::vec3d(1.0, 2.0, 3.0)).cast::<gf::Vec3f>().unwrap(),
1090            gf::vec3f(1.0, 2.0, 3.0)
1091        );
1092        // [f64; 3] widens a float vector.
1093        assert_eq!(
1094            Value::Vec3f(gf::vec3f(1.0, 2.0, 3.0)).cast::<[f64; 3]>().unwrap(),
1095            [1.0, 2.0, 3.0]
1096        );
1097    }
1098
1099    #[test]
1100    fn cast_quat_wxyz_order() {
1101        // [f64; 4] from a quaternion is (w, x, y, z).
1102        let q = gf::quatf(1.0, 2.0, 3.0, 4.0);
1103        assert_eq!(Value::Quatf(q).cast::<[f64; 4]>().unwrap(), [1.0, 2.0, 3.0, 4.0]);
1104    }
1105
1106    #[test]
1107    fn from_gf_roundtrip() {
1108        let v = gf::vec3f(1.0, 2.0, 3.0);
1109        assert_eq!(Value::from(v), Value::Vec3f(v));
1110
1111        let m = gf::Matrix4d::IDENTITY;
1112        assert_eq!(Value::from(m), Value::Matrix4d(m));
1113    }
1114}