openusd 0.5.0

Rust native USD library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
//! Scene description foundations.
//!
//! This module contains common data types used by parsers.
//! Roughly this correspond to C++ SDF module <https://openusd.org/dev/api/sdf_page_front.html>

use std::{borrow::Cow, collections::HashMap, fmt::Debug};

use anyhow::{anyhow, Result};
use bytemuck::{Pod, Zeroable};
use strum::{Display, EnumCount, FromRepr};

mod asset_path;
mod change;
mod data;
pub mod expr;
mod layer;
mod ordering;
mod path;
pub mod schema;
mod spec;
mod value;

pub use asset_path::AssetPath;
pub use change::{ChangeEntry, ChangeFlags, ChangeList};
pub use data::Data;
pub use expr::Expr;
pub use layer::{AuthoringError, Layer, LayerFormat};
pub use ordering::{apply_ordering, element_cmp};
pub use path::{path, Path, PathComponent, PathComponents, PathElement};
pub use schema::{ChildrenKey, FieldKey};
pub use spec::{
    AttributeSpec, AttributeSpecMut, PrimSpec, PrimSpecMut, PseudoRootSpec, PseudoRootSpecMut, RelationshipSpec,
    RelationshipSpecMut, Spec, SpecError,
};
pub use value::{Value, ValueConversionError};

/// An enum that specifies the type of an object.
/// Objects are entities that have fields and are addressable by path.
#[repr(u32)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, FromRepr, EnumCount, Display)]
pub enum SpecType {
    // The unknown type has a value of 0 so that SdfSpecType() is unknown.
    #[default]
    Unknown = 0,

    // Real concrete types
    Attribute = 1,
    Connection = 2,
    Expression = 3,
    Mapper = 4,
    MapperArg = 5,
    Prim = 6,
    PseudoRoot = 7,
    Relationship = 8,
    RelationshipTarget = 9,
    Variant = 10,
    VariantSet = 11,
}

#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromRepr)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub enum Specifier {
    Def,
    Over,
    Class,
}

/// An enum that defines permission levels.
///
/// Permissions control which layers may refer to or express
/// opinions about a prim. Opinions expressed about a prim, or
/// relationships to that prim, by layers that are not allowed
/// permission to access the prim will be ignored.
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromRepr)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub enum Permission {
    Public,
    Private,
}

/// An enum that identifies variability types for attributes.
/// Variability indicates whether the attribute may vary over time and
/// value coordinates, and if its value comes through authoring or
/// or from its owner.
#[repr(i32)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromRepr)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub enum Variability {
    #[default]
    Varying,
    Uniform,
}

/// A time-coded `double` (C++ `SdfTimeCode`) — the value held by a
/// `timecode`-typed attribute (e.g. `UsdMediaSpatialAudio.startTime`). Unlike
/// a plain `double`, a `TimeCode` value is retimed by layer offsets during
/// composition.
///
/// This is the authored *value* type, distinct from a time-query *parameter*
/// (C++ `UsdTimeCode`, passed to `Attribute::get_at`). Read it with
/// `Attribute::get::<sdf::TimeCode>()` and author it with
/// `set(sdf::TimeCode(..))`; it round-trips through [`Value::TimeCode`].
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
pub struct TimeCode(pub f64);

impl TimeCode {
    /// The wrapped time value.
    #[inline]
    pub fn value(self) -> f64 {
        self.0
    }
}

impl From<f64> for TimeCode {
    fn from(v: f64) -> Self {
        TimeCode(v)
    }
}

impl From<TimeCode> for f64 {
    fn from(t: TimeCode) -> Self {
        t.0
    }
}

impl From<TimeCode> for Value {
    fn from(t: TimeCode) -> Self {
        Value::TimeCode(t.0)
    }
}

impl TryFrom<Value> for TimeCode {
    type Error = ValueConversionError;

    fn try_from(value: Value) -> Result<Self, Self::Error> {
        match value {
            Value::TimeCode(v) => Ok(TimeCode(v)),
            other => ValueConversionError::err("TimeCode", &other),
        }
    }
}

/// Represents a time offset and scale between layers.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Pod, Zeroable)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct LayerOffset {
    /// Time offset.
    pub offset: f64,
    /// Scale factor.
    pub scale: f64,
}

impl Default for LayerOffset {
    fn default() -> Self {
        Self {
            offset: 0.0,
            scale: 1.0,
        }
    }
}

impl LayerOffset {
    /// Identity layer offset: offset 0, scale 1.
    pub const IDENTITY: LayerOffset = LayerOffset {
        offset: 0.0,
        scale: 1.0,
    };

    #[inline]
    pub fn new(offset: f64, scale: f64) -> Self {
        Self { offset, scale }
    }

    /// A pure time scaling `(0.0, scale)` with no offset. Composed onto another
    /// offset it scales without shifting — used to fold a `timeCodesPerSecond`
    /// retiming ratio into an arc's offset (spec 12.3.2).
    #[inline]
    pub fn scale_only(scale: f64) -> Self {
        Self { offset: 0.0, scale }
    }

    #[inline]
    pub fn is_valid(&self) -> bool {
        self.offset.is_finite() && self.scale.is_finite()
    }

    /// Returns `true` if this is the identity offset `(0.0, 1.0)`.
    #[inline]
    pub fn is_identity(&self) -> bool {
        self.offset == 0.0 && self.scale == 1.0
    }

    /// Applies this offset to a time value as `offset + scale * time` — the
    /// retiming a layer offset performs on the time coordinate of samples and
    /// clip schedules.
    #[inline]
    pub fn apply(&self, time: f64) -> f64 {
        self.offset + self.scale * time
    }

    /// Returns `true` if this offset is well-formed for composition:
    /// finite `offset` and a strictly positive, finite `scale`.
    ///
    /// Per spec 10.3.1.1 / 10.3.2.1.2, a non-positive scale is a composition
    /// error.
    #[inline]
    pub fn is_valid_composition(&self) -> bool {
        self.offset.is_finite() && self.scale.is_finite() && self.scale > 0.0
    }

    /// Returns this offset if valid for composition, or the identity otherwise.
    ///
    /// Matches OpenUSD behaviour of silently dropping back to identity when a
    /// non-positive or non-finite scale is authored.
    #[inline]
    pub fn sanitized(self) -> Self {
        if self.is_valid_composition() {
            self
        } else {
            Self::IDENTITY
        }
    }

    /// Concatenates `self` (outer / closer to root) with `inner` (deeper).
    ///
    /// Given two offsets where a time value `t` in the inner frame maps to
    /// the outer frame as `t * inner.scale + inner.offset`, and outer's own
    /// transform is `t * outer.scale + outer.offset`, the composed transform
    /// from the deepest frame to the outermost is:
    ///
    /// ```text
    /// offset = outer.offset + outer.scale * inner.offset
    /// scale  = outer.scale * inner.scale
    /// ```
    #[inline]
    pub fn concatenate(&self, inner: &LayerOffset) -> LayerOffset {
        LayerOffset {
            offset: self.offset + self.scale * inner.offset,
            scale: self.scale * inner.scale,
        }
    }
}

/// Represents a payload and all its meta data.
///
/// A payload represents a prim reference to an external layer. A payload
/// is similar to a prim reference (see SdfReference) with the major
/// difference that payloads are explicitly loaded by the user.
///
/// Unloaded payloads represent a boundary that lazy composition and
/// system behaviors will not traverse across, providing a user-visible
/// way to manage the working set of the scene.
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Payload {
    /// The asset path to the external layer.
    #[cfg_attr(feature = "serde", serde(rename = "asset", skip_serializing_if = "String::is_empty"))]
    pub asset_path: String,
    /// The root prim path to the referenced prim in the external layer.
    #[cfg_attr(feature = "serde", serde(rename = "path", skip_serializing_if = "Path::is_empty"))]
    pub prim_path: Path,
    /// The layer offset to transform time.
    #[cfg_attr(
        feature = "serde",
        serde(rename = "layerOffset", skip_serializing_if = "Option::is_none")
    )]
    pub layer_offset: Option<LayerOffset>,
}

/// Represents a reference and all its meta data.
///
/// A reference is expressed on a prim in a given layer and it identifies a
/// prim in a layer stack. All opinions in the namespace hierarchy
/// under the referenced prim will be composed with the opinions in the
/// namespace hierarchy under the referencing prim.
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Reference {
    /// The asset path to the external layer.
    #[cfg_attr(feature = "serde", serde(rename = "asset", skip_serializing_if = "String::is_empty"))]
    pub asset_path: String,
    /// The path to the referenced prim in the external layer.
    #[cfg_attr(feature = "serde", serde(rename = "path", skip_serializing_if = "Path::is_empty"))]
    pub prim_path: Path,
    /// The layer offset to transform time.
    #[cfg_attr(feature = "serde", serde(rename = "layerOffset"))]
    pub layer_offset: LayerOffset,
    /// The custom data associated with the reference.
    #[cfg_attr(
        feature = "serde",
        serde(rename = "customData", skip_serializing_if = "HashMap::is_empty")
    )]
    pub custom_data: HashMap<String, Value>,
}

mod list_op;

pub use list_op::ListOp;

pub type IntListOp = ListOp<i32>;
pub type UintListOp = ListOp<u32>;

pub type Int64ListOp = ListOp<i64>;
pub type Uint64ListOp = ListOp<u64>;

pub type StringListOp = ListOp<String>;
pub type TokenListOp = ListOp<String>;
pub type PathListOp = ListOp<Path>;
pub type ReferenceListOp = ListOp<Reference>;
pub type PayloadListOp = ListOp<Payload>;

pub type TimeSampleMap = Vec<(f64, Value)>;

/// A single namespace relocation `(source, target)`: the prim at `source` is
/// moved to `target` in composed namespace. An empty `target` is a deletion
/// that makes `source` a prohibited (invalid) child name. Mirrors C++
/// `SdfRelocate`, a `std::pair<SdfPath, SdfPath>`.
pub type Relocate = (Path, Path);

/// The ordered list of [`Relocate`]s authored in a layer's `relocates`
/// metadata. Mirrors C++ `SdfRelocates`, a `std::vector<SdfRelocate>`.
pub type RelocateList = Vec<Relocate>;

/// Interface to access scene description data similar to `SdfAbstractData` in C++ version of USD.
///
/// `AbstractData` is an anonymous container that owns scene description values.
///
/// For now holds read-only portion of the API.
pub trait AbstractData {
    /// Returns `true` if this data has a spec for the given path.
    fn has_spec(&self, path: &Path) -> bool;

    /// Returns `true` if this data has a field for the given path.
    fn has_field(&self, path: &Path, field: &str) -> bool;

    /// Returns the type of the spec at the given path.
    fn spec_type(&self, path: &Path) -> Option<SpecType>;

    /// Returns the value for a field, or `None` if not authored.
    ///
    /// The value can be either owned or borrowed depending on internals.
    /// In the binary format, the data is typically compressed and/or encoded,
    /// so memory allocation is required to store unpacked result, so owned
    /// values are typically expected. With text parsers, there is a data copy
    /// already stored, so a borrowed value is returned to avoid unnecessary copies.
    ///
    /// Errors propagate I/O or decoding failures; a missing spec or field is
    /// signalled by `Ok(None)`.
    fn try_get(&self, path: &Path, field: &str) -> Result<Option<Cow<'_, Value>>>;

    /// Returns the value for a field, erroring if not authored.
    ///
    /// Use [`AbstractData::try_get`] when absence is an expected condition.
    fn get(&self, path: &Path, field: &str) -> Result<Cow<'_, Value>> {
        self.try_get(path, field)?
            .ok_or_else(|| anyhow!("No field '{field}' at path '{path}'"))
    }

    /// Returns the field names for a given path in authored order.
    fn list(&self, path: &Path) -> Option<Vec<String>>;

    /// Returns every authored path, sorted lexicographically.
    ///
    /// The order is deterministic and stable across repeated calls. Emitters
    /// rely on this for reproducible output.
    fn paths(&self) -> Vec<Path>;

    /// Returns a reference to the underlying [`Data`] backend, if this impl
    /// is a writable in-memory store. Read-only file-backed impls return
    /// `None`. The default implementation returns `None`, so adding a new
    /// `AbstractData` impl does not require opting in.
    fn as_data(&self) -> Option<&Data> {
        None
    }

    /// Returns a mutable reference to the underlying [`Data`] backend, if this
    /// impl is a writable in-memory store.
    ///
    /// Read-only backends (USDA text readers, USDC binary readers) return
    /// `None`. The default implementation returns `None`, so adding a new
    /// `AbstractData` impl does not require opting in. Authoring code that
    /// needs to mutate a layer uses this hook to reach the writable store.
    fn as_data_mut(&mut self) -> Option<&mut Data> {
        None
    }
}

/// A boxed layer data source, used throughout the layer stack.
pub type LayerData = Box<dyn AbstractData>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn layer_offset_identity_is_identity() {
        assert!(LayerOffset::IDENTITY.is_identity());
        assert!(LayerOffset::default().is_identity());
        assert!(!LayerOffset::new(0.0, 2.0).is_identity());
        assert!(!LayerOffset::new(1.0, 1.0).is_identity());
    }

    #[test]
    fn layer_offset_valid_composition_rejects_non_positive_scale() {
        assert!(LayerOffset::new(10.0, 1.0).is_valid_composition());
        assert!(!LayerOffset::new(10.0, 0.0).is_valid_composition());
        assert!(!LayerOffset::new(10.0, -1.0).is_valid_composition());
        assert!(!LayerOffset::new(f64::INFINITY, 1.0).is_valid_composition());
        assert!(!LayerOffset::new(0.0, f64::NAN).is_valid_composition());
    }

    #[test]
    fn layer_offset_sanitized_drops_invalid_to_identity() {
        assert_eq!(LayerOffset::new(10.0, 2.0).sanitized(), LayerOffset::new(10.0, 2.0));
        assert_eq!(LayerOffset::new(5.0, -1.0).sanitized(), LayerOffset::IDENTITY);
        assert_eq!(LayerOffset::new(5.0, 0.0).sanitized(), LayerOffset::IDENTITY);
    }

    #[test]
    fn layer_offset_concatenate_matches_spec_formula() {
        let outer = LayerOffset::new(10.0, 2.0);
        let inner = LayerOffset::new(20.0, 1.0);
        // Matches BasicTimeOffset_root pcp.txt: (10,2) concat (20,1) = (50, 2).
        assert_eq!(outer.concatenate(&inner), LayerOffset::new(50.0, 2.0));
    }

    #[test]
    fn layer_offset_concatenate_is_associative() {
        let a = LayerOffset::new(10.0, 2.0);
        let b = LayerOffset::new(20.0, 0.5);
        let c = LayerOffset::new(5.0, 3.0);
        let ab_c = a.concatenate(&b).concatenate(&c);
        let a_bc = a.concatenate(&b.concatenate(&c));
        assert!((ab_c.offset - a_bc.offset).abs() < 1e-12);
        assert!((ab_c.scale - a_bc.scale).abs() < 1e-12);
    }

    #[test]
    fn layer_offset_identity_is_neutral() {
        let a = LayerOffset::new(10.0, 2.0);
        assert_eq!(a.concatenate(&LayerOffset::IDENTITY), a);
        assert_eq!(LayerOffset::IDENTITY.concatenate(&a), a);
    }
}