Skip to main content

openusd/sdf/
mod.rs

1//! Scene description foundations.
2//!
3//! This module contains common data types used by parsers.
4//! Roughly this correspond to C++ SDF module <https://openusd.org/dev/api/sdf_page_front.html>
5
6use std::{borrow::Cow, collections::HashMap, fmt::Debug};
7
8use anyhow::{anyhow, Result};
9use bytemuck::{Pod, Zeroable};
10use strum::{Display, EnumCount, FromRepr};
11
12mod asset_path;
13mod change;
14mod data;
15pub mod expr;
16mod layer;
17mod ordering;
18mod path;
19pub mod schema;
20mod spec;
21mod value;
22
23pub use asset_path::AssetPath;
24pub use change::{ChangeEntry, ChangeFlags, ChangeList};
25pub use data::Data;
26pub use expr::Expr;
27pub use layer::{AuthoringError, Layer, LayerFormat};
28pub use ordering::{apply_ordering, element_cmp};
29pub use path::{path, Path, PathComponent, PathComponents, PathElement};
30pub use schema::{ChildrenKey, FieldKey};
31pub use spec::{
32    AttributeSpec, AttributeSpecMut, PrimSpec, PrimSpecMut, PseudoRootSpec, PseudoRootSpecMut, RelationshipSpec,
33    RelationshipSpecMut, Spec, SpecError,
34};
35pub use value::{Value, ValueConversionError};
36
37/// An enum that specifies the type of an object.
38/// Objects are entities that have fields and are addressable by path.
39#[repr(u32)]
40#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, FromRepr, EnumCount, Display)]
41pub enum SpecType {
42    // The unknown type has a value of 0 so that SdfSpecType() is unknown.
43    #[default]
44    Unknown = 0,
45
46    // Real concrete types
47    Attribute = 1,
48    Connection = 2,
49    Expression = 3,
50    Mapper = 4,
51    MapperArg = 5,
52    Prim = 6,
53    PseudoRoot = 7,
54    Relationship = 8,
55    RelationshipTarget = 9,
56    Variant = 10,
57    VariantSet = 11,
58}
59
60#[repr(i32)]
61#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromRepr)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize))]
63#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
64pub enum Specifier {
65    Def,
66    Over,
67    Class,
68}
69
70/// An enum that defines permission levels.
71///
72/// Permissions control which layers may refer to or express
73/// opinions about a prim. Opinions expressed about a prim, or
74/// relationships to that prim, by layers that are not allowed
75/// permission to access the prim will be ignored.
76#[repr(i32)]
77#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromRepr)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize))]
79#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
80pub enum Permission {
81    Public,
82    Private,
83}
84
85/// An enum that identifies variability types for attributes.
86/// Variability indicates whether the attribute may vary over time and
87/// value coordinates, and if its value comes through authoring or
88/// or from its owner.
89#[repr(i32)]
90#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromRepr)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize))]
92#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
93pub enum Variability {
94    #[default]
95    Varying,
96    Uniform,
97}
98
99/// A time-coded `double` (C++ `SdfTimeCode`) — the value held by a
100/// `timecode`-typed attribute (e.g. `UsdMediaSpatialAudio.startTime`). Unlike
101/// a plain `double`, a `TimeCode` value is retimed by layer offsets during
102/// composition.
103///
104/// This is the authored *value* type, distinct from a time-query *parameter*
105/// (C++ `UsdTimeCode`, passed to `Attribute::get_at`). Read it with
106/// `Attribute::get::<sdf::TimeCode>()` and author it with
107/// `set(sdf::TimeCode(..))`; it round-trips through [`Value::TimeCode`].
108#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
109pub struct TimeCode(pub f64);
110
111impl TimeCode {
112    /// The wrapped time value.
113    #[inline]
114    pub fn value(self) -> f64 {
115        self.0
116    }
117}
118
119impl From<f64> for TimeCode {
120    fn from(v: f64) -> Self {
121        TimeCode(v)
122    }
123}
124
125impl From<TimeCode> for f64 {
126    fn from(t: TimeCode) -> Self {
127        t.0
128    }
129}
130
131impl From<TimeCode> for Value {
132    fn from(t: TimeCode) -> Self {
133        Value::TimeCode(t.0)
134    }
135}
136
137impl TryFrom<Value> for TimeCode {
138    type Error = ValueConversionError;
139
140    fn try_from(value: Value) -> Result<Self, Self::Error> {
141        match value {
142            Value::TimeCode(v) => Ok(TimeCode(v)),
143            other => ValueConversionError::err("TimeCode", &other),
144        }
145    }
146}
147
148/// Represents a time offset and scale between layers.
149#[repr(C)]
150#[derive(Debug, Clone, Copy, PartialEq, Pod, Zeroable)]
151#[cfg_attr(feature = "serde", derive(serde::Serialize))]
152pub struct LayerOffset {
153    /// Time offset.
154    pub offset: f64,
155    /// Scale factor.
156    pub scale: f64,
157}
158
159impl Default for LayerOffset {
160    fn default() -> Self {
161        Self {
162            offset: 0.0,
163            scale: 1.0,
164        }
165    }
166}
167
168impl LayerOffset {
169    /// Identity layer offset: offset 0, scale 1.
170    pub const IDENTITY: LayerOffset = LayerOffset {
171        offset: 0.0,
172        scale: 1.0,
173    };
174
175    #[inline]
176    pub fn new(offset: f64, scale: f64) -> Self {
177        Self { offset, scale }
178    }
179
180    /// A pure time scaling `(0.0, scale)` with no offset. Composed onto another
181    /// offset it scales without shifting — used to fold a `timeCodesPerSecond`
182    /// retiming ratio into an arc's offset (spec 12.3.2).
183    #[inline]
184    pub fn scale_only(scale: f64) -> Self {
185        Self { offset: 0.0, scale }
186    }
187
188    #[inline]
189    pub fn is_valid(&self) -> bool {
190        self.offset.is_finite() && self.scale.is_finite()
191    }
192
193    /// Returns `true` if this is the identity offset `(0.0, 1.0)`.
194    #[inline]
195    pub fn is_identity(&self) -> bool {
196        self.offset == 0.0 && self.scale == 1.0
197    }
198
199    /// Applies this offset to a time value as `offset + scale * time` — the
200    /// retiming a layer offset performs on the time coordinate of samples and
201    /// clip schedules.
202    #[inline]
203    pub fn apply(&self, time: f64) -> f64 {
204        self.offset + self.scale * time
205    }
206
207    /// Returns `true` if this offset is well-formed for composition:
208    /// finite `offset` and a strictly positive, finite `scale`.
209    ///
210    /// Per spec 10.3.1.1 / 10.3.2.1.2, a non-positive scale is a composition
211    /// error.
212    #[inline]
213    pub fn is_valid_composition(&self) -> bool {
214        self.offset.is_finite() && self.scale.is_finite() && self.scale > 0.0
215    }
216
217    /// Returns this offset if valid for composition, or the identity otherwise.
218    ///
219    /// Matches OpenUSD behaviour of silently dropping back to identity when a
220    /// non-positive or non-finite scale is authored.
221    #[inline]
222    pub fn sanitized(self) -> Self {
223        if self.is_valid_composition() {
224            self
225        } else {
226            Self::IDENTITY
227        }
228    }
229
230    /// Concatenates `self` (outer / closer to root) with `inner` (deeper).
231    ///
232    /// Given two offsets where a time value `t` in the inner frame maps to
233    /// the outer frame as `t * inner.scale + inner.offset`, and outer's own
234    /// transform is `t * outer.scale + outer.offset`, the composed transform
235    /// from the deepest frame to the outermost is:
236    ///
237    /// ```text
238    /// offset = outer.offset + outer.scale * inner.offset
239    /// scale  = outer.scale * inner.scale
240    /// ```
241    #[inline]
242    pub fn concatenate(&self, inner: &LayerOffset) -> LayerOffset {
243        LayerOffset {
244            offset: self.offset + self.scale * inner.offset,
245            scale: self.scale * inner.scale,
246        }
247    }
248}
249
250/// Represents a payload and all its meta data.
251///
252/// A payload represents a prim reference to an external layer. A payload
253/// is similar to a prim reference (see SdfReference) with the major
254/// difference that payloads are explicitly loaded by the user.
255///
256/// Unloaded payloads represent a boundary that lazy composition and
257/// system behaviors will not traverse across, providing a user-visible
258/// way to manage the working set of the scene.
259#[derive(Debug, Default, Clone, PartialEq)]
260#[cfg_attr(feature = "serde", derive(serde::Serialize))]
261pub struct Payload {
262    /// The asset path to the external layer.
263    #[cfg_attr(feature = "serde", serde(rename = "asset", skip_serializing_if = "String::is_empty"))]
264    pub asset_path: String,
265    /// The root prim path to the referenced prim in the external layer.
266    #[cfg_attr(feature = "serde", serde(rename = "path", skip_serializing_if = "Path::is_empty"))]
267    pub prim_path: Path,
268    /// The layer offset to transform time.
269    #[cfg_attr(
270        feature = "serde",
271        serde(rename = "layerOffset", skip_serializing_if = "Option::is_none")
272    )]
273    pub layer_offset: Option<LayerOffset>,
274}
275
276/// Represents a reference and all its meta data.
277///
278/// A reference is expressed on a prim in a given layer and it identifies a
279/// prim in a layer stack. All opinions in the namespace hierarchy
280/// under the referenced prim will be composed with the opinions in the
281/// namespace hierarchy under the referencing prim.
282#[derive(Debug, Default, Clone, PartialEq)]
283#[cfg_attr(feature = "serde", derive(serde::Serialize))]
284pub struct Reference {
285    /// The asset path to the external layer.
286    #[cfg_attr(feature = "serde", serde(rename = "asset", skip_serializing_if = "String::is_empty"))]
287    pub asset_path: String,
288    /// The path to the referenced prim in the external layer.
289    #[cfg_attr(feature = "serde", serde(rename = "path", skip_serializing_if = "Path::is_empty"))]
290    pub prim_path: Path,
291    /// The layer offset to transform time.
292    #[cfg_attr(feature = "serde", serde(rename = "layerOffset"))]
293    pub layer_offset: LayerOffset,
294    /// The custom data associated with the reference.
295    #[cfg_attr(
296        feature = "serde",
297        serde(rename = "customData", skip_serializing_if = "HashMap::is_empty")
298    )]
299    pub custom_data: HashMap<String, Value>,
300}
301
302mod list_op;
303
304pub use list_op::ListOp;
305
306pub type IntListOp = ListOp<i32>;
307pub type UintListOp = ListOp<u32>;
308
309pub type Int64ListOp = ListOp<i64>;
310pub type Uint64ListOp = ListOp<u64>;
311
312pub type StringListOp = ListOp<String>;
313pub type TokenListOp = ListOp<String>;
314pub type PathListOp = ListOp<Path>;
315pub type ReferenceListOp = ListOp<Reference>;
316pub type PayloadListOp = ListOp<Payload>;
317
318pub type TimeSampleMap = Vec<(f64, Value)>;
319
320/// A single namespace relocation `(source, target)`: the prim at `source` is
321/// moved to `target` in composed namespace. An empty `target` is a deletion
322/// that makes `source` a prohibited (invalid) child name. Mirrors C++
323/// `SdfRelocate`, a `std::pair<SdfPath, SdfPath>`.
324pub type Relocate = (Path, Path);
325
326/// The ordered list of [`Relocate`]s authored in a layer's `relocates`
327/// metadata. Mirrors C++ `SdfRelocates`, a `std::vector<SdfRelocate>`.
328pub type RelocateList = Vec<Relocate>;
329
330/// Interface to access scene description data similar to `SdfAbstractData` in C++ version of USD.
331///
332/// `AbstractData` is an anonymous container that owns scene description values.
333///
334/// For now holds read-only portion of the API.
335pub trait AbstractData {
336    /// Returns `true` if this data has a spec for the given path.
337    fn has_spec(&self, path: &Path) -> bool;
338
339    /// Returns `true` if this data has a field for the given path.
340    fn has_field(&self, path: &Path, field: &str) -> bool;
341
342    /// Returns the type of the spec at the given path.
343    fn spec_type(&self, path: &Path) -> Option<SpecType>;
344
345    /// Returns the value for a field, or `None` if not authored.
346    ///
347    /// The value can be either owned or borrowed depending on internals.
348    /// In the binary format, the data is typically compressed and/or encoded,
349    /// so memory allocation is required to store unpacked result, so owned
350    /// values are typically expected. With text parsers, there is a data copy
351    /// already stored, so a borrowed value is returned to avoid unnecessary copies.
352    ///
353    /// Errors propagate I/O or decoding failures; a missing spec or field is
354    /// signalled by `Ok(None)`.
355    fn try_get(&self, path: &Path, field: &str) -> Result<Option<Cow<'_, Value>>>;
356
357    /// Returns the value for a field, erroring if not authored.
358    ///
359    /// Use [`AbstractData::try_get`] when absence is an expected condition.
360    fn get(&self, path: &Path, field: &str) -> Result<Cow<'_, Value>> {
361        self.try_get(path, field)?
362            .ok_or_else(|| anyhow!("No field '{field}' at path '{path}'"))
363    }
364
365    /// Returns the field names for a given path in authored order.
366    fn list(&self, path: &Path) -> Option<Vec<String>>;
367
368    /// Returns every authored path, sorted lexicographically.
369    ///
370    /// The order is deterministic and stable across repeated calls. Emitters
371    /// rely on this for reproducible output.
372    fn paths(&self) -> Vec<Path>;
373
374    /// Returns a reference to the underlying [`Data`] backend, if this impl
375    /// is a writable in-memory store. Read-only file-backed impls return
376    /// `None`. The default implementation returns `None`, so adding a new
377    /// `AbstractData` impl does not require opting in.
378    fn as_data(&self) -> Option<&Data> {
379        None
380    }
381
382    /// Returns a mutable reference to the underlying [`Data`] backend, if this
383    /// impl is a writable in-memory store.
384    ///
385    /// Read-only backends (USDA text readers, USDC binary readers) return
386    /// `None`. The default implementation returns `None`, so adding a new
387    /// `AbstractData` impl does not require opting in. Authoring code that
388    /// needs to mutate a layer uses this hook to reach the writable store.
389    fn as_data_mut(&mut self) -> Option<&mut Data> {
390        None
391    }
392}
393
394/// A boxed layer data source, used throughout the layer stack.
395pub type LayerData = Box<dyn AbstractData>;
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn layer_offset_identity_is_identity() {
403        assert!(LayerOffset::IDENTITY.is_identity());
404        assert!(LayerOffset::default().is_identity());
405        assert!(!LayerOffset::new(0.0, 2.0).is_identity());
406        assert!(!LayerOffset::new(1.0, 1.0).is_identity());
407    }
408
409    #[test]
410    fn layer_offset_valid_composition_rejects_non_positive_scale() {
411        assert!(LayerOffset::new(10.0, 1.0).is_valid_composition());
412        assert!(!LayerOffset::new(10.0, 0.0).is_valid_composition());
413        assert!(!LayerOffset::new(10.0, -1.0).is_valid_composition());
414        assert!(!LayerOffset::new(f64::INFINITY, 1.0).is_valid_composition());
415        assert!(!LayerOffset::new(0.0, f64::NAN).is_valid_composition());
416    }
417
418    #[test]
419    fn layer_offset_sanitized_drops_invalid_to_identity() {
420        assert_eq!(LayerOffset::new(10.0, 2.0).sanitized(), LayerOffset::new(10.0, 2.0));
421        assert_eq!(LayerOffset::new(5.0, -1.0).sanitized(), LayerOffset::IDENTITY);
422        assert_eq!(LayerOffset::new(5.0, 0.0).sanitized(), LayerOffset::IDENTITY);
423    }
424
425    #[test]
426    fn layer_offset_concatenate_matches_spec_formula() {
427        let outer = LayerOffset::new(10.0, 2.0);
428        let inner = LayerOffset::new(20.0, 1.0);
429        // Matches BasicTimeOffset_root pcp.txt: (10,2) concat (20,1) = (50, 2).
430        assert_eq!(outer.concatenate(&inner), LayerOffset::new(50.0, 2.0));
431    }
432
433    #[test]
434    fn layer_offset_concatenate_is_associative() {
435        let a = LayerOffset::new(10.0, 2.0);
436        let b = LayerOffset::new(20.0, 0.5);
437        let c = LayerOffset::new(5.0, 3.0);
438        let ab_c = a.concatenate(&b).concatenate(&c);
439        let a_bc = a.concatenate(&b.concatenate(&c));
440        assert!((ab_c.offset - a_bc.offset).abs() < 1e-12);
441        assert!((ab_c.scale - a_bc.scale).abs() < 1e-12);
442    }
443
444    #[test]
445    fn layer_offset_identity_is_neutral() {
446        let a = LayerOffset::new(10.0, 2.0);
447        assert_eq!(a.concatenate(&LayerOffset::IDENTITY), a);
448        assert_eq!(LayerOffset::IDENTITY.concatenate(&a), a);
449    }
450}