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::{collections::HashMap, fmt::Debug};
7
8use anyhow::Result;
9use bytemuck::{Pod, Zeroable};
10use strum::FromRepr;
11
12use crate::tf::Token;
13
14mod asset_path;
15mod change;
16mod copy;
17mod data;
18pub mod expr;
19mod file_format;
20mod layer;
21pub(crate) mod layer_registry;
22mod ordering;
23mod path;
24mod path_table;
25pub mod schema;
26pub mod sink;
27mod spec;
28mod value;
29
30pub use asset_path::AssetPath;
31pub use change::{ChangeEntry, ChangeFlags, ChangeList};
32pub(crate) use copy::{author_spec, is_children_field};
33pub use copy::{
34    copy_spec, copy_spec_with, copy_spec_within, should_copy_children, should_copy_value, CopyChildren,
35    CopyChildrenArgs, CopyValue, CopyValueArgs,
36};
37pub use data::{AbstractData, CowData, Data, DataError, Patch};
38pub use expr::{Evaluation, EvaluationValue, Expr, StringEvaluation, StringSegment};
39pub use file_format::{FileFormat, FileFormatCaps, WriteSeek};
40pub(crate) use layer::{dry_run_layers, edit_layers};
41pub use layer::{AuthoringError, EditError, Layer, LayerEdit, LayerSink, LayerSinkId, PendingLayerChange};
42pub use layer_registry::LayerRegistry;
43pub use ordering::{apply_ordering, element_cmp};
44pub use path::{path, Path, PathComponent, PathComponents, PathElement};
45pub use path_table::PathTable;
46pub use schema::{ChildrenKey, FieldKey};
47pub use spec::{
48    AttributeSpec, AttributeSpecMut, AttributeSpecRef, PrimSpec, PrimSpecMut, PrimSpecRef, PropertySpec,
49    PropertySpecMut, PropertySpecRef, PseudoRootSpec, PseudoRootSpecMut, PseudoRootSpecRef, RelationshipSpec,
50    RelationshipSpecMut, RelationshipSpecRef, Spec, SpecData, SpecError, SpecMut, SpecRef, SpecType,
51};
52pub use value::{CastError, FromValueCast, Value, ValueConversionError};
53
54#[repr(i32)]
55#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromRepr)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize))]
57#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
58pub enum Specifier {
59    Def,
60    Over,
61    Class,
62}
63
64/// An enum that defines permission levels.
65///
66/// Permissions control which layers may refer to or express
67/// opinions about a prim. Opinions expressed about a prim, or
68/// relationships to that prim, by layers that are not allowed
69/// permission to access the prim will be ignored.
70#[repr(i32)]
71#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromRepr)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize))]
73#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
74pub enum Permission {
75    Public,
76    Private,
77}
78
79/// An enum that identifies variability types for attributes.
80/// Variability indicates whether the attribute may vary over time and
81/// value coordinates, and if its value comes through authoring or
82/// or from its owner.
83#[repr(i32)]
84#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromRepr)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize))]
86#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
87pub enum Variability {
88    #[default]
89    Varying,
90    Uniform,
91}
92
93/// A time-coded `double` (C++ `SdfTimeCode`) — the value held by a
94/// `timecode`-typed attribute (e.g. `UsdMediaSpatialAudio.startTime`). Unlike
95/// a plain `double`, a `TimeCode` value is retimed by layer offsets during
96/// composition.
97///
98/// This is the authored *value* type, distinct from a time-query *parameter*
99/// (C++ `UsdTimeCode`, passed to `Attribute::get_at`). Read it with
100/// `Attribute::get::<sdf::TimeCode>()` and author it with
101/// `set(sdf::TimeCode(..))`; it round-trips through [`Value::TimeCode`].
102#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default, derive_more::From)]
103#[cfg_attr(feature = "serde", derive(serde::Serialize))]
104pub struct TimeCode(pub f64);
105
106impl TimeCode {
107    /// The wrapped time value.
108    #[inline]
109    pub fn value(self) -> f64 {
110        self.0
111    }
112}
113
114impl TryFrom<Value> for TimeCode {
115    type Error = ValueConversionError;
116
117    fn try_from(value: Value) -> Result<Self, Self::Error> {
118        match value {
119            Value::TimeCode(v) => Ok(v),
120            other => ValueConversionError::err("TimeCode", &other),
121        }
122    }
123}
124
125/// Represents a time offset and scale between layers.
126#[repr(C)]
127#[derive(Debug, Clone, Copy, PartialEq, Pod, Zeroable)]
128#[cfg_attr(feature = "serde", derive(serde::Serialize))]
129pub struct LayerOffset {
130    /// Time offset.
131    pub offset: f64,
132    /// Scale factor.
133    pub scale: f64,
134}
135
136impl Default for LayerOffset {
137    fn default() -> Self {
138        Self {
139            offset: 0.0,
140            scale: 1.0,
141        }
142    }
143}
144
145impl LayerOffset {
146    /// Identity layer offset: offset 0, scale 1.
147    pub const IDENTITY: LayerOffset = LayerOffset {
148        offset: 0.0,
149        scale: 1.0,
150    };
151
152    #[inline]
153    pub fn new(offset: f64, scale: f64) -> Self {
154        Self { offset, scale }
155    }
156
157    /// A pure time scaling `(0.0, scale)` with no offset. Composed onto another
158    /// offset it scales without shifting — used to fold a `timeCodesPerSecond`
159    /// retiming ratio into an arc's offset (spec 12.3.2).
160    #[inline]
161    pub fn scale_only(scale: f64) -> Self {
162        Self { offset: 0.0, scale }
163    }
164
165    #[inline]
166    pub fn is_valid(&self) -> bool {
167        self.offset.is_finite() && self.scale.is_finite()
168    }
169
170    /// Returns `true` if this is the identity offset `(0.0, 1.0)`.
171    #[inline]
172    pub fn is_identity(&self) -> bool {
173        self.offset == 0.0 && self.scale == 1.0
174    }
175
176    /// Applies this offset to a time value as `offset + scale * time` — the
177    /// retiming a layer offset performs on the time coordinate of samples and
178    /// clip schedules.
179    #[inline]
180    pub fn apply(&self, time: f64) -> f64 {
181        self.offset + self.scale * time
182    }
183
184    /// Returns the inverse offset, undoing [`apply`](Self::apply): if `self`
185    /// maps a source time `t` to `offset + scale * t`, the inverse maps that
186    /// result back to `t`. The identity inverts to itself; a `scale == 0`
187    /// offset has no inverse and yields the identity.
188    #[inline]
189    pub fn inverse(&self) -> LayerOffset {
190        if self.scale == 0.0 {
191            return LayerOffset::IDENTITY;
192        }
193        LayerOffset {
194            offset: -self.offset / self.scale,
195            scale: 1.0 / self.scale,
196        }
197    }
198
199    /// Returns `true` if this offset is well-formed for composition:
200    /// finite `offset` and a strictly positive, finite `scale`.
201    ///
202    /// Per spec 10.3.1.1 / 10.3.2.1.2, a non-positive scale is a composition
203    /// error.
204    #[inline]
205    pub fn is_valid_composition(&self) -> bool {
206        self.offset.is_finite() && self.scale.is_finite() && self.scale > 0.0
207    }
208
209    /// Returns this offset if valid for composition, or the identity otherwise.
210    ///
211    /// Matches OpenUSD behaviour of silently dropping back to identity when a
212    /// non-positive or non-finite scale is authored.
213    #[inline]
214    pub fn sanitized(self) -> Self {
215        if self.is_valid_composition() {
216            self
217        } else {
218            Self::IDENTITY
219        }
220    }
221
222    /// Concatenates `self` (outer / closer to root) with `inner` (deeper).
223    ///
224    /// Given two offsets where a time value `t` in the inner frame maps to
225    /// the outer frame as `t * inner.scale + inner.offset`, and outer's own
226    /// transform is `t * outer.scale + outer.offset`, the composed transform
227    /// from the deepest frame to the outermost is:
228    ///
229    /// ```text
230    /// offset = outer.offset + outer.scale * inner.offset
231    /// scale  = outer.scale * inner.scale
232    /// ```
233    #[inline]
234    pub fn concatenate(&self, inner: &LayerOffset) -> LayerOffset {
235        LayerOffset {
236            offset: self.offset + self.scale * inner.offset,
237            scale: self.scale * inner.scale,
238        }
239    }
240}
241
242/// Represents a payload and all its meta data.
243///
244/// A payload represents a prim reference to an external layer. A payload
245/// is similar to a prim reference (see SdfReference) with the major
246/// difference that payloads are explicitly loaded by the user.
247///
248/// Unloaded payloads represent a boundary that lazy composition and
249/// system behaviors will not traverse across, providing a user-visible
250/// way to manage the working set of the scene.
251#[derive(Debug, Default, Clone, PartialEq)]
252#[cfg_attr(feature = "serde", derive(serde::Serialize))]
253pub struct Payload {
254    /// The asset path to the external layer.
255    #[cfg_attr(feature = "serde", serde(rename = "asset", skip_serializing_if = "String::is_empty"))]
256    pub asset_path: String,
257    /// The root prim path to the referenced prim in the external layer.
258    #[cfg_attr(feature = "serde", serde(rename = "path", skip_serializing_if = "Path::is_empty"))]
259    pub prim_path: Path,
260    /// The layer offset to transform time.
261    #[cfg_attr(
262        feature = "serde",
263        serde(rename = "layerOffset", skip_serializing_if = "Option::is_none")
264    )]
265    pub layer_offset: Option<LayerOffset>,
266}
267
268/// Represents a reference and all its meta data.
269///
270/// A reference is expressed on a prim in a given layer and it identifies a
271/// prim in a layer stack. All opinions in the namespace hierarchy
272/// under the referenced prim will be composed with the opinions in the
273/// namespace hierarchy under the referencing prim.
274#[derive(Debug, Default, Clone, PartialEq)]
275#[cfg_attr(feature = "serde", derive(serde::Serialize))]
276pub struct Reference {
277    /// The asset path to the external layer.
278    #[cfg_attr(feature = "serde", serde(rename = "asset", skip_serializing_if = "String::is_empty"))]
279    pub asset_path: String,
280    /// The path to the referenced prim in the external layer.
281    #[cfg_attr(feature = "serde", serde(rename = "path", skip_serializing_if = "Path::is_empty"))]
282    pub prim_path: Path,
283    /// The layer offset to transform time.
284    #[cfg_attr(feature = "serde", serde(rename = "layerOffset"))]
285    pub layer_offset: LayerOffset,
286    /// The custom data associated with the reference.
287    #[cfg_attr(
288        feature = "serde",
289        serde(rename = "customData", skip_serializing_if = "HashMap::is_empty")
290    )]
291    pub custom_data: HashMap<String, Value>,
292}
293
294mod list_op;
295
296pub use list_op::ListOp;
297
298/// A USD dictionary value (C++ `VtDictionary`): the payload of
299/// [`Value::Dictionary`], keyed by name.
300pub type Dictionary = std::collections::HashMap<String, Value>;
301
302pub type IntListOp = ListOp<i32>;
303pub type UintListOp = ListOp<u32>;
304
305pub type Int64ListOp = ListOp<i64>;
306pub type Uint64ListOp = ListOp<u64>;
307
308pub type StringListOp = ListOp<String>;
309pub type TokenListOp = ListOp<Token>;
310pub type PathListOp = ListOp<Path>;
311pub type ReferenceListOp = ListOp<Reference>;
312pub type PayloadListOp = ListOp<Payload>;
313
314pub type TimeSampleMap = Vec<(f64, Value)>;
315
316/// A single namespace relocation `(source, target)`: the prim at `source` is
317/// moved to `target` in composed namespace. An empty `target` is a deletion
318/// that makes `source` a prohibited (invalid) child name. Mirrors C++
319/// `SdfRelocate`, a `std::pair<SdfPath, SdfPath>`.
320pub type Relocate = (Path, Path);
321
322/// The ordered list of [`Relocate`]s authored in a layer's `relocates`
323/// metadata. Mirrors C++ `SdfRelocates`, a `std::vector<SdfRelocate>`.
324pub type RelocateList = Vec<Relocate>;
325
326/// A boxed layer data source, used throughout the layer stack.
327pub type LayerData = Box<dyn AbstractData>;
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn layer_offset_identity_is_identity() {
335        assert!(LayerOffset::IDENTITY.is_identity());
336        assert!(LayerOffset::default().is_identity());
337        assert!(!LayerOffset::new(0.0, 2.0).is_identity());
338        assert!(!LayerOffset::new(1.0, 1.0).is_identity());
339    }
340
341    #[test]
342    fn layer_offset_valid_composition_rejects_non_positive_scale() {
343        assert!(LayerOffset::new(10.0, 1.0).is_valid_composition());
344        assert!(!LayerOffset::new(10.0, 0.0).is_valid_composition());
345        assert!(!LayerOffset::new(10.0, -1.0).is_valid_composition());
346        assert!(!LayerOffset::new(f64::INFINITY, 1.0).is_valid_composition());
347        assert!(!LayerOffset::new(0.0, f64::NAN).is_valid_composition());
348    }
349
350    #[test]
351    fn layer_offset_sanitized_drops_invalid_to_identity() {
352        assert_eq!(LayerOffset::new(10.0, 2.0).sanitized(), LayerOffset::new(10.0, 2.0));
353        assert_eq!(LayerOffset::new(5.0, -1.0).sanitized(), LayerOffset::IDENTITY);
354        assert_eq!(LayerOffset::new(5.0, 0.0).sanitized(), LayerOffset::IDENTITY);
355    }
356
357    #[test]
358    fn layer_offset_concatenate_matches_spec_formula() {
359        let outer = LayerOffset::new(10.0, 2.0);
360        let inner = LayerOffset::new(20.0, 1.0);
361        // Matches BasicTimeOffset_root pcp.txt: (10,2) concat (20,1) = (50, 2).
362        assert_eq!(outer.concatenate(&inner), LayerOffset::new(50.0, 2.0));
363    }
364
365    #[test]
366    fn layer_offset_concatenate_is_associative() {
367        let a = LayerOffset::new(10.0, 2.0);
368        let b = LayerOffset::new(20.0, 0.5);
369        let c = LayerOffset::new(5.0, 3.0);
370        let ab_c = a.concatenate(&b).concatenate(&c);
371        let a_bc = a.concatenate(&b.concatenate(&c));
372        assert!((ab_c.offset - a_bc.offset).abs() < 1e-12);
373        assert!((ab_c.scale - a_bc.scale).abs() < 1e-12);
374    }
375
376    #[test]
377    fn layer_offset_identity_is_neutral() {
378        let a = LayerOffset::new(10.0, 2.0);
379        assert_eq!(a.concatenate(&LayerOffset::IDENTITY), a);
380        assert_eq!(LayerOffset::IDENTITY.concatenate(&a), a);
381    }
382
383    #[test]
384    fn layer_offset_inverse_undoes_apply() {
385        // A dyadic scale round-trips exactly.
386        let a = LayerOffset::new(10.0, 2.0);
387        assert_eq!(a.inverse(), LayerOffset::new(-5.0, 0.5));
388        assert_eq!(a.inverse().apply(a.apply(7.0)), 7.0);
389        // A non-dyadic scale (e.g. a 1/3 retiming ratio) round-trips only to
390        // within float rounding, not exactly.
391        let b = LayerOffset::new(1.0, 3.0);
392        assert!((b.inverse().apply(b.apply(7.0)) - 7.0).abs() < 1e-9);
393        // The identity inverts to itself; a zero scale has no inverse.
394        assert_eq!(LayerOffset::IDENTITY.inverse(), LayerOffset::IDENTITY);
395        assert_eq!(LayerOffset::new(3.0, 0.0).inverse(), LayerOffset::IDENTITY);
396    }
397}