Skip to main content

animsmith_core/
lib.rs

1//! Engine-agnostic animation linting primitives for Rust pipelines.
2//!
3//! This crate is the embedding boundary for animsmith. It owns the core
4//! data model ([`Document`], [`Skeleton`], [`Clip`], [`Track`]), rig-role
5//! resolution ([`detect_profile`], [`ResolvedRoles::from_names`]),
6//! typed configuration ([`Config`]), measurement generation
7//! ([`measure::measure_document`]), versioned result envelopes
8//! ([`contract::MeasureEnvelope`], [`contract::LintEnvelope`]), measurement diffs
9//! ([`diff::diff_measurements`]), structured findings ([`Finding`]), and
10//! check execution ([`CheckCtx`], [`all_checks`], [`evaluate_checks`]).
11//! The [`source_facts`] module owns the bounded, format-neutral V1 vocabulary
12//! that format loaders bind to the exact primary bytes in an immutable
13//! [`LoadedSource`]. A mutable normalized [`Document`] does not reconstruct
14//! importer-sensitive source declarations; consuming the wrapper as a document
15//! deliberately discards those facts. The separate [`dependency_closure`]
16//! sidecar records bounded, same-load primary/external content identities over
17//! the raw resource-declaration domain; format crates own rooted I/O while core
18//! owns its validated value and canonical digest contract. The borrowing facts
19//! view reuses the canonical [`model::SourceSkeletonAssets`] table and remains
20//! separate from scale's operation-specific capability and proof ledgers.
21//! The opt-in [`bake_static_mesh_transforms`] operation canonicalizes supported
22//! unanimated, unskinned mesh scenes into identity-root geometry and returns
23//! deterministic producer evidence.
24//! The opt-in [`transform::prune_constant_tracks`] helper removes only
25//! interpolation-aware constant-track candidates whose sampled local and
26//! model-space pose evidence remains within its documented tolerances.
27//! The [`scale`] module owns the format-neutral plan/proof contracts for the
28//! two distinct DESIGN.md Appendix D scale operations —
29//! [`scale::ScaleOperation::WholeDocumentLinearUnits`] and
30//! [`scale::ScaleOperation::RestBindUniformScale`] — through pure, fail-closed
31//! [`scale::plan_scale`] and independent [`scale::prove_scale`]. A format
32//! frontend owns exact source rewriting and hands the reloaded emitted
33//! document back through [`scale::ScaleCandidate::from_document`]; core does
34//! not expose a production candidate builder, choose named versus indexed
35//! selectors, publish artifacts, or write files. Core does own the
36//! format-neutral mapping from an already chosen exact named assembly selector
37//! to its source root and fully governed skin through
38//! [`scale::resolve_assembly_scale_named_selector`].
39//! The [`animsmith-gltf`] and [`animsmith-fbx`] loader crates translate file
40//! formats into this model; their docs.rs pages continue the library path for
41//! format-specific loading and, for glTF, writing.
42//!
43//! The [embedding guide] explains crate selection and integration
44//! boundaries. The [pipeline scenario guide] shows where an embedded gate
45//! fits in marketplace intake, mocap cleanup, outsourced acceptance, and CI.
46//! A [runnable example] exercises the complete library flow.
47//!
48//! [embedding guide]: https://github.com/mmannerm/animsmith/blob/main/docs/embedding.md
49//! [pipeline scenario guide]: https://github.com/mmannerm/animsmith/blob/main/docs/pipeline-scenarios.md
50//! [runnable example]: https://github.com/mmannerm/animsmith/blob/main/crates/animsmith/examples/embed.rs
51//! [`animsmith-gltf`]: https://docs.rs/animsmith-gltf
52//! [`animsmith-fbx`]: https://docs.rs/animsmith-fbx
53//!
54//! # Quick start
55//!
56//! After a format crate has loaded a [`Document`], resolve rig roles, build
57//! a [`Config`] from the host pipeline's contract, and share one
58//! [`MetricGrids`] between measurements, checks, and optional report
59//! generation:
60//!
61//! ```
62//! use animsmith_core::{
63//!     CheckCtx, CheckSelection, Config, Document, MetricGrids, all_checks,
64//!     evaluate_checks, resolve_configured_roles,
65//! };
66//! use animsmith_core::measure::measure_document;
67//!
68//! let doc = Document::default();
69//! let config = Config::default();
70//! config.validate()?;
71//! let roles = resolve_configured_roles(&doc.skeleton, &config.rig);
72//! let grids = MetricGrids::new(&doc);
73//!
74//! let measurements = measure_document(&grids, &roles, &config);
75//! let ctx = CheckCtx::new(&grids, &roles, &config);
76//! let results = evaluate_checks(&ctx, &all_checks(), CheckSelection::All)?;
77//!
78//! assert!(measurements.is_empty());
79//! assert!(results.iter().all(|result| result.findings().is_empty()));
80//! # Ok::<(), animsmith_core::EvaluationError>(())
81//! ```
82//!
83//! [`CheckCtx::new`] consumes already-resolved roles; it does not interpret
84//! [`Config::rig`] automatically. Frontends may use [`detect_profile`],
85//! [`resolve_configured_roles`] for the same named-profile plus inline-override
86//! policy as the CLI. Missing prerequisites are represented as typed coverage
87//! gaps rather than false findings.
88//!
89//! # API status
90//!
91//! The Rust API is pre-1.0 and may still change before the first stable
92//! release. The intended extension points are the data model,
93//! configuration types, measurement and diff APIs, rig-profile APIs, the
94//! [`Check`] trait for custom checks, and the check catalog functions
95//! re-exported from this crate root. Built-in check ids, CLI exit-code
96//! semantics, and the shared versioned JSON envelope/schema ids are treated
97//! as the most stable automation contracts. The [`contract`] module owns the
98//! same envelope types and immutable identities for CLI and embedded
99//! producers. The scene-asset
100//! structs in [`model`] and the pipeline-mechanical helpers in
101//! [`transform`] and [`static_bake`] are public so the loader, writer, and CLI crates can
102//! share the same model, but they are less settled than the
103//! measurement/check embedding flow while the crate is pre-1.0. Metric
104//! formulas and individual Rust symbols are still subject to pre-1.0
105//! refinement.
106//!
107//! Public APIs that return [`Result`] document their `# Errors` cases.
108//! Index-based accessors and transform helpers that rely on
109//! loader-established invariants document their `# Panics` contracts.
110//! Loader-valid documents from the format crates should flow through
111//! checking, sampling, and measurement without panicking on untrusted
112//! input.
113
114#![warn(missing_docs)]
115
116pub mod assembly;
117mod bounded_deserialize;
118pub mod check;
119mod checks;
120pub mod collection;
121pub mod config;
122pub mod contract;
123pub mod dependency_closure;
124pub mod diff;
125pub mod engine_contract;
126pub mod evaluation;
127pub mod finding;
128#[cfg(feature = "fixtures")]
129pub mod fixtures;
130pub mod measure;
131pub mod metrics;
132pub mod model;
133pub mod prediction;
134pub mod profile;
135pub mod sample;
136pub mod scale;
137pub mod skinned_canonical;
138pub mod source_facts;
139pub mod static_bake;
140pub mod transform;
141
142pub use check::{Check, CheckCtx, all_checks, mechanical_checks};
143pub use collection::{
144    COLLECTION_MANIFEST_V1_BUDGET_ID, COLLECTION_MANIFEST_V1_ID,
145    COLLECTION_MANIFEST_V1_MAX_AGGREGATE_MEMBERS, COLLECTION_MANIFEST_V1_MAX_AGGREGATE_WORK,
146    COLLECTION_MANIFEST_V1_MAX_CLIPS, COLLECTION_MANIFEST_V1_MAX_IDENTIFIER_BYTES,
147    COLLECTION_MANIFEST_V1_MAX_MANIFEST_BYTES, COLLECTION_MANIFEST_V1_MAX_RUNTIME_SETS,
148    COLLECTION_MANIFEST_V1_MAX_SOURCES, COLLECTION_MANIFEST_V1_MAX_TAKE_NAME_BYTES,
149    COLLECTION_MANIFEST_V1_SCHEMA_VERSION, CollectionClipV1, CollectionDigestPinV1, CollectionIdV1,
150    CollectionLogicalIdV1, CollectionManifestBudgetV1, CollectionManifestError,
151    CollectionManifestV1, CollectionRuntimeSetKindV1, CollectionRuntimeSetV1,
152    CollectionSourceKeyV1, CollectionSourceV1,
153};
154pub use config::{
155    ClipExpectations, Config, ConfigValidationError, GaitGroup, MovementOwner, Pinned,
156    RuntimeNodeSelectorResolution, RuntimeNodeSelectors, RuntimeNodesConfig, SeveritySetting,
157    SyncGroup, TimeComplementSettings,
158};
159pub use contract::{
160    DiffEnvelope, InputIdentity, LintEnvelope, LintFileReport, MEASUREMENTS_SCHEMA_ID,
161    MEASUREMENTS_SCHEMA_VERSION, MeasureEnvelope, MeasureFileReport, MeasurementContract,
162    MeasurementContractError, MeasurementFileError, MeasurementReportError, MeasurementReportFile,
163    MeasurementReportInput, MeasurementReportReadError, OUTPUT_SCHEMA_ID, OUTPUT_SCHEMA_VERSION,
164    OUTPUT_V10_SCHEMA_ID, OUTPUT_V11_MAX_CHECKS_PER_FILE, OUTPUT_V11_MAX_FILES,
165    OUTPUT_V11_MAX_REPORT_BYTES, OutputContractError, RigInfo, RigInfoError, ToolInfo, ToolSource,
166    sha256_hex,
167};
168pub use dependency_closure::{
169    DEPENDENCY_CLOSURE_BUDGET_V1_ID, DEPENDENCY_CLOSURE_V1_ID,
170    DEPENDENCY_CLOSURE_V1_MAX_DEDUP_PROBES, DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES,
171    DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES, DEPENDENCY_CLOSURE_V1_MAX_NORMALIZATION_BYTES,
172    DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS, DEPENDENCY_CLOSURE_V1_MAX_REFERENCES,
173    DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES, DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES,
174    DependencyClosureBuilderV1, DependencyClosureCoverageReasonV1, DependencyClosureCoverageV1,
175    DependencyClosureError, DependencyClosureIdentityV1, DependencyClosureReferenceV1,
176    DependencyClosureV1, DependencyClosureWorkV1, DependencyReferenceTargetV1,
177    DependencyResourceKeyV1, DependencyResourcePurposeV1, DependencyResourceRefusalReasonV1,
178    DependencyResourceUnavailableReasonV1, ExternalResourceIdentityV1, ResourceClosureBudgetV1,
179    ResourceKeySyntaxV1,
180};
181pub use engine_contract::{
182    ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS, ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
183    ENGINE_CONTRACT_V1_MAX_TEXT_BYTES, ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES,
184    ENGINE_PROFILE_FACTS_V1_ID, EngineAnimationAddressabilityV1, EngineBakeOrExtractV1,
185    EngineClipSettingsV1, EngineContractError, EngineConversionControlV1, EngineCoordinateBasisV1,
186    EngineDefaultStatusV1, EngineFactIdV1, EngineFactStateV1, EngineFactValueV1,
187    EngineForwardAxisV1, EngineHandednessV1, EngineImportHandlingV1, EngineLinearUnitV1,
188    EnginePrimarySourceV1, EngineProfileFactV1, EngineProfileSelectionV1,
189    EngineRootMotionAddressabilityV1, EngineSettingApplicabilityV1, EngineSettingDescriptorV1,
190    EngineSettingDomainV1, EngineSettingIdV1, EngineSettingRowV1, EngineSettingScopeV1,
191    EngineSettingValueV1, EngineTargetAddressabilityV1, EngineUpAxisV1,
192    RESOLVED_ENGINE_SETTINGS_V1_ID, ResolvedEngineProfileV1, ResolvedEngineSettingsV1,
193};
194pub use evaluation::{
195    Applicability, BUILTIN_COVERAGE_GAP_CODES, BUILTIN_EVALUATION_SCOPE_CODES, CheckEvaluation,
196    CheckOutput, CheckSelection, ConfigurationState, CoverageGap, CoverageGapCode, EvaluationError,
197    EvaluationScope, EvaluationScopeCode, EvaluationState, SelectionState, evaluate_checks,
198    lint_requires_failure,
199};
200pub use finding::{Finding, MemberMeasurement, Severity, Value};
201/// Re-export of the exact `glam` version used by animsmith's public math
202/// types, so embedders can construct [`Transform`] values without a
203/// cross-version type mismatch.
204pub use glam;
205pub use metrics::MetricGrids;
206pub use model::{
207    AdditionalInfluenceSet, AffineDomainViolation, Bone, BoneId, Clip, DecodedImageColorType,
208    Document, DocumentShapeError, ImageContainerFormat, ImageSourceKind, ImageUnavailableReason,
209    Interpolation, MaterialResourceAssets, MaterialResourceCoverage, MaterialTextureSlot,
210    MeshInstanceShapeViolation, Property, Skeleton, SourceImageAsset, SourceImageInspection,
211    SourceInfo, SourceInverseBindAccessor, SourceInverseBindAccessorStatus, SourceMaterialAsset,
212    SourceMaterialTextureBinding, SourceNodeAsset, SourceNodeLocalRest, SourceProjectionViolation,
213    SourceSkeletonAssets, SourceSkeletonCoverage, SourceSkinAsset, SourceSkinAttachment,
214    SourceTextureAsset, Track, TrackShapeViolation, TrackValues, Transform,
215    validate_document_shape,
216};
217pub use prediction::{
218    ENGINE_PREDICTION_V1_ID, EnginePredictionBasisV1, EnginePredictionFacetStateV1,
219    EnginePredictionFacetV1, EnginePredictionV1, FinitePredictionNumberV1, MeasurementPointerV1,
220    PREDICTION_PROVENANCE_V1_ID, PREDICTION_V1_MAX_AGGREGATE_PROVENANCE_ROWS,
221    PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET, PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
222    PREDICTION_V1_MAX_FACETS_PER_FILE, PREDICTION_V1_MAX_MEASUREMENT_POINTER_COMPONENTS,
223    PREDICTION_V1_MAX_REASONS_PER_FACET, PREDICTION_V1_MAX_TEXT_BYTES,
224    PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE, PredictionBasisIdentityV1,
225    PredictionBasisReferenceV1, PredictionContractError, PredictionProvenanceIdentityV1,
226    PredictionProvenanceV1, PredictionScalarV1, PredictionUnavailableReasonV1, RawSourceAxisV1,
227    RawSourceBasisReferenceV1, RawSourceBindingV1, RawSourceCoordinateBasisV1,
228    RawSourceDispositionV1, RawSourceDomainV1, RawSourceFieldIdV1, RawSourceKeyV1,
229    RawSourceObservationStateWireV1, RawSourceObservationWireV1, RawSourceProjectionWorkWireV1,
230    RawSourceProvenanceKindV1, RawSourceProvenanceV1, RawSourceSetCoverageStateV1,
231    RawSourceSetCoverageV1, RawSourceUnavailableReasonV1, ResolvedSettingLocationV1,
232    SourceSkeletonRowKindV1,
233};
234pub use profile::{
235    ResolutionOutcome, ResolvedRoles, RigProfile, Role, RoleResolutionPolicy, builtin_profiles,
236    detect_profile, detect_profile_detailed, resolve_configured_roles, resolve_named,
237    resolve_named_detailed,
238};
239pub use sample::{PoseGrid, TrackSample, default_frame_count, sample_clip, sample_track};
240pub use scale::{
241    ProofResidualKind, ScaleBoneRestField, ScaleCandidate, ScaleCapabilityCoverage,
242    ScaleCapabilityFacts, ScaleError, ScaleFieldDisposition, ScaleFieldPlan, ScaleFieldTarget,
243    ScaleOperation, ScalePayloadShapeRow, ScalePlan, ScalePlanLedger, ScaleProjectedRole,
244    ScaleProof, ScaleProofObligation, ScaleProofResidual, ScaleRequest, ScaleRewriteRule,
245    ScaleSourceNodeKind, ScaleSourceRestField, ScaleSourceTopologyRow, ScaleTolerancePolicy,
246    plan_scale, prove_scale,
247};
248pub use skinned_canonical::{
249    SkinnedBindPoseCanonicalization, SkinnedBindPoseCanonicalizationError,
250    SkinnedBindPoseCanonicalizationOptions, SkinnedBindPosePlacement,
251    canonicalize_skinned_bind_pose,
252};
253pub use source_facts::{
254    LoadedSource, RAW_SOURCE_FACTS_V1_ID, RAW_SOURCE_V1_MAX_CLIPS, RAW_SOURCE_V1_MAX_OBSERVATIONS,
255    RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES, RAW_SOURCE_V1_MAX_TEXT_BYTES,
256    RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES, RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH, RawSourceFactsBuilderV1,
257    RawSourceFactsV1, SourceAxisV1, SourceChannelFactV1, SourceChannelPropertyV1, SourceClipFactV1,
258    SourceComponentMaskV1, SourceConstructFactV1, SourceConstructKindV1, SourceCoordinateBasisV1,
259    SourceFactDomainV1, SourceFactSetV1, SourceFactsError, SourceFactsViewV1, SourceFormatV1,
260    SourceFramesPerSecondV1, SourceHandednessV1, SourceInterpolationV1, SourceLinearUnitV1,
261    SourceLoaderDispositionV1, SourceLogicalLocatorV1, SourceObservationStateV1,
262    SourceObservationV1, SourceProjectionWorkV1, SourceProvenanceKindV1, SourceProvenanceV1,
263    SourceRelativeLocatorV1, SourceResourceKindV1, SourceResourceLocatorV1,
264    SourceResourceReferenceV1, SourceSetCoverageStateV1, SourceSetCoverageV1, SourceTargetKindV1,
265    SourceTargetV1, SourceTextV1, SourceTimeRangeV1, SourceUnavailableReasonV1,
266};
267pub use static_bake::{
268    StaticMeshBake, StaticMeshBakeError, StaticMeshBakeEvidence, StaticMeshBakeInstanceEvidence,
269    bake_static_mesh_transforms,
270};