openusd 0.7.0

Rust native USD library
Documentation
//! Clip-manifest generation (spec 12.3.4.1.1.2): builds the layer that declares
//! which attributes a clip set's clips carry time samples for (C++
//! `Usd_GenerateClipManifest`).
//!
//! A clip set that authors no `manifestAssetPath` still resolves through a
//! manifest — one synthesized here from the clips its `active` schedule names —
//! so every clip set is gated the same way. The same generator backs the
//! authoring utility on [`ClipsAPI`](crate::usd::ClipsAPI), which can also write
//! the value blocks that let value resolution skip opening a clip.

use std::collections::HashSet;

use crate::sdf::schema::FieldKey;
use crate::sdf::{self, AttributeSpec, AttributeSpecMut, Path, Value, Variability};

use super::QueryError;

/// Tag carried in the identifier of a manifest synthesized for a clip set that
/// authors none (C++ `Usd_IsAutoGeneratedClipManifest` matches on this string).
pub(crate) const GENERATED_MANIFEST_TAG: &str = "generated_manifest.usda";

/// Tag carried in the identifier of a manifest generated on request, which a
/// caller exports and names from `manifestAssetPath`. Distinct from
/// [`GENERATED_MANIFEST_TAG`] so a synthesized manifest stays recognizable.
pub(crate) const CLIP_MANIFEST_TAG: &str = "clip_manifest.usda";

/// Builds a manifest layer declaring every attribute the `clips` carry time
/// samples for under `clip_prim_path` (C++ `Usd_GenerateClipManifest`).
///
/// Each entry pairs a clip layer with the stage time it activates at, so the
/// list holds one entry per `active` schedule entry: a clip activated twice
/// appears twice, and an asset the schedule never names contributes nothing.
///
/// A declaration carries the attribute's `typeName` and variability and nothing
/// else — no default, no samples, no metadata — at the same path the clip
/// authored it, so the manifest shares the clips' namespace and a query
/// translated into that namespace reads both alike. An attribute a clip only
/// declares, or gives only a default, is left out: the manifest indexes
/// time-varying data.
///
/// An entry whose activation time is `Some` also authors a value block there
/// for every declared attribute that clip carries no samples for (C++
/// `writeBlocksForClipsWithMissingValues`). Value resolution reads those blocks
/// when filling a gap under `interpolateMissingClipValues`, deciding a clip
/// contributes nothing without opening it. The manifest is an anonymous layer
/// carrying `tag` in its identifier.
///
/// A `uniform` declaration is carried through as the clip authored it, and so
/// records the clip's schema faithfully, but never sources a value: clips
/// supply only varying attributes (C++
/// `Usd_ClipSet::ContainsValueForAttribute`).
pub(crate) fn generate_manifest(
    clips: &[(&sdf::Layer, Option<f64>)],
    clip_prim_path: &Path,
    tag: &str,
) -> Result<sdf::Layer, QueryError> {
    let mut manifest = sdf::Layer::new_anonymous(tag);
    manifest.edit(|edit| {
        let data = edit.data_mut();

        // Declarations union across the clips in activation order, so the
        // earliest clip to carry an attribute fixes its type and variability.
        // A clip the schedule activates more than once declares the same
        // attributes each time, so each distinct layer is walked once.
        let mut declared: Vec<Path> = Vec::new();
        let mut walked: HashSet<&str> = HashSet::new();
        for &(clip, _) in clips {
            if !walked.insert(clip.identifier()) {
                continue;
            }
            for (path, type_name, variability) in sampled_attributes(clip, clip_prim_path)? {
                if data.has_spec(&path) {
                    continue;
                }
                // A clip may hold a path the authoring layer cannot represent —
                // a prim name that is not an identifier, or one whose namespace
                // clashes with a spec an earlier clip put there. Such an
                // attribute is simply not declarable, so it is left out rather
                // than failing the whole manifest (C++ likewise reports the
                // spec-creation failure and moves on to the next path).
                if AttributeSpec::new(data, path.clone(), type_name, variability, false).is_ok() {
                    declared.push(path);
                }
            }
        }

        for &(clip, active) in clips {
            let Some(time) = active else {
                continue;
            };
            for path in declared.iter().filter(|path| !has_time_samples(clip, path)) {
                AttributeSpecMut::get(data, path.clone())
                    .expect("declared above")
                    .set_time_sample(time, Value::ValueBlock)?;
            }
        }
        Ok(())
    })?;
    Ok(manifest)
}

/// Identity of one authored clip set: the prim carrying it, and its name in the
/// `clips` dictionary. Distinct from the set's `primPath`, which addresses the
/// clips' own namespace and which unrelated prims routinely share (the C++
/// `Usd_ClipCache` manifest key).
///
/// Deliberately cheap to build — no asset path is resolved to an identifier —
/// because value resolution constructs one on every clipped attribute read.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct ClipSetKey {
    /// The prim the clip set is composed on.
    pub prim: Path,
    /// The clip set's name, the key it holds in the `clips` dictionary.
    pub clip_set: String,
}

/// The attributes `clip` carries time samples for under `clip_prim_path`, each
/// with the `typeName` and variability a manifest declaration copies. Paths are
/// returned in the layer's sorted order, so generation is deterministic.
///
/// An attribute with no authored `typeName` is skipped: a declaration needs one,
/// and every writer here authors it (C++ `Usd_GenerateClipManifest` skips such a
/// spec for the same reason).
//
// TODO(perf): this materializes and sorts every path in the layer before
// filtering to the `clip_prim_path` subtree, so a clip carrying a whole shot's
// namespace is walked in full for one rig. C++ uses `SdfLayer::Traverse` from
// the prim path; the equivalent here needs a prefix-scan on `AbstractData`,
// whose `Data` backing is an unordered map today.
fn sampled_attributes(
    clip: &sdf::Layer,
    clip_prim_path: &Path,
) -> Result<Vec<(Path, sdf::ValueTypeName, Variability)>, sdf::PathParseError> {
    let mut out = Vec::new();
    for path in clip.data().spec_paths() {
        if !path.is_property_path() || !path.has_prefix(clip_prim_path) {
            continue;
        }
        let Some(attr) = clip.attribute(&path)? else {
            continue;
        };
        let Some(type_name) = attr.type_name() else {
            continue;
        };
        let variability = attr.variability();
        if has_time_samples(clip, &path) {
            out.push((path, type_name, variability));
        }
    }
    Ok(out)
}

/// Whether `layer` authors a non-empty `timeSamples` map for the attribute at
/// `path`.
///
/// Reads the field through [`sdf::AbstractData::try_field`] and inspects the
/// borrowed value, so testing a clip that animates dense array attributes does
/// not copy their samples (C++ `SdfLayer::GetNumTimeSamplesForPath`).
//
// TODO(perf): a backend that decodes lazily still materializes the map to hand
// back a `Cow::Owned`. An `AbstractData::num_time_samples` hook, defaulting to
// this and overridden by the crate reader, would make the test O(1) everywhere.
fn has_time_samples(layer: &sdf::Layer, path: &Path) -> bool {
    match layer.data().try_field(path, FieldKey::TimeSamples.as_str()) {
        Ok(Some(field)) => matches!(&*field, Value::TimeSamples(samples) if !samples.is_empty()),
        _ => false,
    }
}

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

    /// Parses `text` as a `usda` clip layer identified as `name`. Distinct
    /// names matter: generation walks each distinct layer once.
    fn clip(name: &str, text: &str) -> sdf::Layer {
        sdf::Layer::new(name, Box::new(usda::parse(text).expect("valid usda")))
    }

    /// Every attribute path the manifest declares, sorted.
    fn declared(manifest: &sdf::Layer) -> Vec<String> {
        manifest
            .data()
            .spec_paths()
            .into_iter()
            .filter(|path| path.is_property_path())
            .map(|path| path.as_str().to_owned())
            .collect()
    }

    /// The union of two clips' sampled attributes is declared, an attribute
    /// with no samples is left out, and so is anything outside `clipPrimPath`.
    /// Ported from the C++ `testUsdValueClips` manifest-generation assets.
    #[test]
    fn declares_sampled_union() -> Result<()> {
        let one = clip(
            "one.usda",
            r#"#usda 1.0
def "Clip"
{
    def "A"
    {
        double a.timeSamples = { 0: 1.0, 1: 2.0 }
        double b.timeSamples = { 0: 3.0, 1: 4.0 }
        double d
        double e = 5.0
    }

    def "B"
    {
        double g.timeSamples = { 0: 1.0, 1: 2.0 }
    }
}
"#,
        );
        let two = clip(
            "two.usda",
            r#"#usda 1.0
def "Clip"
{
    def "A"
    {
        double z.timeSamples = { 0: 1.0 }
    }

    def "B"
    {
        double y.timeSamples = { 0: 1.0 }
    }
}
"#,
        );

        let manifest = generate_manifest(&[(&one, None), (&two, None)], &sdf::path("/Clip/A")?, "test.usda")?;
        assert_eq!(declared(&manifest), ["/Clip/A.a", "/Clip/A.b", "/Clip/A.z"]);
        // Declarations only: no default, no samples carried over.
        let a = manifest.attribute("/Clip/A.a")?.expect("declared");
        assert_eq!(a.type_name(), Some(sdf::ValueTypeName::DOUBLE));
        assert!(a.default().is_none());
        assert!(a.time_samples().is_none());
        Ok(())
    }

    /// A variant-selection property path is declared verbatim, so the manifest
    /// stays in the clips' namespace, and `uniform` variability is carried
    /// through (it decides whether the set sources the attribute at all).
    #[test]
    fn declares_variant_and_uniform() -> Result<()> {
        let one = clip(
            "one.usda",
            r#"#usda 1.0
def "A"
{
    uniform double u.timeSamples = { 0: 1.0 }

    variantSet "v" = {
        "a" {
            double c.timeSamples = { 0: 5.0 }
        }
    }
}
"#,
        );

        let manifest = generate_manifest(&[(&one, None)], &sdf::path("/A")?, "test.usda")?;
        assert_eq!(declared(&manifest), ["/A.u", "/A{v=a}.c"]);
        assert_eq!(
            manifest.attribute("/A.u")?.expect("declared").variability(),
            Variability::Uniform
        );
        assert_eq!(
            manifest.attribute("/A{v=a}.c")?.expect("declared").variability(),
            Variability::Varying
        );
        Ok(())
    }

    /// With activation times supplied, an attribute missing from a clip is
    /// blocked at that clip's activation time — including the same clip
    /// activated twice — while an attribute every clip carries gets no blocks.
    #[test]
    fn blocks_clips_without_samples() -> Result<()> {
        let full = clip(
            "full.usda",
            r#"#usda 1.0
def "A"
{
    double both.timeSamples = { 0: 1.0 }
    double only.timeSamples = { 0: 2.0 }
}
"#,
        );
        let partial = clip(
            "partial.usda",
            r#"#usda 1.0
def "A"
{
    double both.timeSamples = { 0: 3.0 }
}
"#,
        );

        let manifest = generate_manifest(
            &[(&full, Some(0.0)), (&partial, Some(4.0)), (&partial, Some(8.0))],
            &sdf::path("/A")?,
            "test.usda",
        )?;

        assert!(
            manifest
                .attribute("/A.both")?
                .expect("declared")
                .time_samples()
                .is_none()
        );
        let only = manifest
            .attribute("/A.only")?
            .expect("declared")
            .time_samples()
            .expect("blocked");
        assert_eq!(only, vec![(4.0, Value::ValueBlock), (8.0, Value::ValueBlock)]);
        Ok(())
    }

    /// Without activation times no blocks are authored, matching the manifest
    /// synthesized during value resolution.
    #[test]
    fn no_blocks_without_active_times() -> Result<()> {
        let empty = clip("empty.usda", "#usda 1.0\ndef \"A\"\n{\n}\n");
        let sampled = clip(
            "sampled.usda",
            "#usda 1.0\ndef \"A\"\n{\n    double x.timeSamples = { 0: 1.0 }\n}\n",
        );

        let manifest = generate_manifest(&[(&sampled, None), (&empty, None)], &sdf::path("/A")?, "test.usda")?;
        assert!(manifest.attribute("/A.x")?.expect("declared").time_samples().is_none());
        Ok(())
    }
}