@pose_flow/babylon 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -3
  3. package/dist/AI_USAGE.md +291 -0
  4. package/dist/PUBLISHING.md +108 -0
  5. package/dist/babylon.d.ts +38 -0
  6. package/dist/babylon.js +201 -0
  7. package/dist/core/controller.d.ts +33 -0
  8. package/dist/core/controller.js +97 -0
  9. package/dist/core/database.d.ts +12 -0
  10. package/dist/core/database.js +95 -0
  11. package/dist/core/features.d.ts +15 -0
  12. package/dist/core/features.js +41 -0
  13. package/dist/core/profile.d.ts +54 -0
  14. package/dist/core/profile.js +89 -0
  15. package/dist/core/root-motion.d.ts +24 -0
  16. package/dist/core/root-motion.js +78 -0
  17. package/dist/core/schema.d.ts +56 -0
  18. package/dist/core/schema.js +123 -0
  19. package/dist/core/search.d.ts +17 -0
  20. package/dist/core/search.js +97 -0
  21. package/dist/core/types.d.ts +75 -0
  22. package/dist/core/types.js +3 -0
  23. package/dist/index.d.ts +8 -0
  24. package/dist/index.js +8 -0
  25. package/dist/offline/src/babylon.js +201 -0
  26. package/dist/offline/src/core/controller.js +97 -0
  27. package/dist/offline/src/core/database.js +95 -0
  28. package/dist/offline/src/core/features.js +41 -0
  29. package/dist/offline/src/core/profile.js +89 -0
  30. package/dist/offline/src/core/root-motion.js +78 -0
  31. package/dist/offline/src/core/schema.js +123 -0
  32. package/dist/offline/src/core/search.js +97 -0
  33. package/dist/offline/src/core/types.js +3 -0
  34. package/dist/offline/src/index.js +8 -0
  35. package/dist/offline/tools/bake.js +86 -0
  36. package/dist/offline/tools/cli.js +30 -0
  37. package/dist/offline/tools/glb.js +54 -0
  38. package/dist/offline/tools/prepare.js +165 -0
  39. package/dist/offline/tools/profile-assets.js +90 -0
  40. package/dist/offline/tools/rig.js +47 -0
  41. package/dist/offline/tools/validate-assets.js +43 -0
  42. package/package.json +48 -6
@@ -0,0 +1,24 @@
1
+ import { type TrajectoryPoint } from './types.js';
2
+ export interface RootMotionSample {
3
+ time: number;
4
+ x: number;
5
+ z: number;
6
+ yaw: number;
7
+ }
8
+ /** Unwrapped yaw in radians. Root track is a bake input, never a movement command. */
9
+ export interface RootMotionTrack {
10
+ version: 1;
11
+ duration: number;
12
+ source: 'hips' | 'root';
13
+ samples: RootMotionSample[];
14
+ }
15
+ export declare function validateRootMotion(track: RootMotionTrack): void;
16
+ /** Clamped sampling for non-looping clips; completed motion stays at its endpoint. */
17
+ export declare function sampleRootMotion(track: RootMotionTrack, time: number, looping?: boolean): RootMotionSample;
18
+ export declare function rootMotionFeatures(track: RootMotionTrack, time: number, derivativeStep?: number, horizons?: readonly number[], looping?: boolean): {
19
+ velocity: readonly [number, number, number];
20
+ angularVelocity: number;
21
+ trajectory: TrajectoryPoint[];
22
+ };
23
+ /** Crop to the same source interval as playback; preserve the source coordinate frame. */
24
+ export declare function cropRootMotion(track: RootMotionTrack, start: number, end: number): RootMotionTrack;
@@ -0,0 +1,78 @@
1
+ import { HORIZONS } from './types.js';
2
+ import { toLocal } from './features.js';
3
+ import { validateHorizons } from './schema.js';
4
+ export function validateRootMotion(track) {
5
+ if (!track || track.version !== 1 || !['hips', 'root'].includes(track.source) || !Number.isFinite(track.duration) || track.duration <= 0 || !Array.isArray(track.samples) || track.samples.length < 2)
6
+ throw new Error('Invalid root motion track');
7
+ let previous = -Infinity;
8
+ for (const sample of track.samples) {
9
+ if (!sample || ![sample.time, sample.x, sample.z, sample.yaw].every(Number.isFinite) || sample.time <= previous || sample.time < 0 || sample.time > track.duration + 1e-6)
10
+ throw new Error('Invalid root motion sample');
11
+ previous = sample.time;
12
+ }
13
+ if (track.samples[0].time !== 0 || Math.abs(track.samples.at(-1).time - track.duration) > 1e-6)
14
+ throw new Error('Incomplete root motion track');
15
+ }
16
+ /** Clamped sampling for non-looping clips; completed motion stays at its endpoint. */
17
+ export function sampleRootMotion(track, time, looping = false) {
18
+ if (!Number.isFinite(time))
19
+ throw new Error('Invalid root motion time');
20
+ if (looping) {
21
+ const cycle = Math.floor(time / track.duration), phase = time - cycle * track.duration;
22
+ if (!Number.isSafeInteger(cycle))
23
+ throw new Error('Root motion cycle out of range');
24
+ const first = track.samples[0], last = track.samples.at(-1);
25
+ const delta = toLocal(last.x - first.x, last.z - first.z, first.yaw), angle = last.yaw - first.yaw;
26
+ // Exponentiate planar rigid transforms, including negative cycles.
27
+ const compose = (a, b) => ({ time,
28
+ x: a.x + Math.cos(a.yaw) * b.x + Math.sin(a.yaw) * b.z,
29
+ z: a.z - Math.sin(a.yaw) * b.x + Math.cos(a.yaw) * b.z, yaw: a.yaw + b.yaw });
30
+ let power = { time, x: delta[0], z: delta[1], yaw: angle };
31
+ if (cycle < 0) {
32
+ const v = toLocal(-power.x, -power.z, angle);
33
+ power = { time, x: v[0], z: v[1], yaw: -angle };
34
+ }
35
+ let result = { time, x: 0, z: 0, yaw: 0 }, count = Math.abs(cycle);
36
+ while (count > 0) {
37
+ if (count % 2 === 1)
38
+ result = compose(result, power);
39
+ power = compose(power, power);
40
+ count = Math.floor(count / 2);
41
+ }
42
+ const sample = sampleRootMotion(track, phase), local = toLocal(sample.x - first.x, sample.z - first.z, first.yaw);
43
+ return compose(first, compose(result, { time, x: local[0], z: local[1], yaw: sample.yaw - first.yaw }));
44
+ }
45
+ const t = Math.max(0, Math.min(time, track.duration)), samples = track.samples;
46
+ let lo = 0, hi = samples.length - 1;
47
+ while (hi - lo > 1) {
48
+ const mid = (lo + hi) >>> 1;
49
+ if (samples[mid].time <= t)
50
+ lo = mid;
51
+ else
52
+ hi = mid;
53
+ }
54
+ const a = samples[lo], b = samples[hi], alpha = (t - a.time) / (b.time - a.time);
55
+ return { time: t, x: a.x + (b.x - a.x) * alpha, z: a.z + (b.z - a.z) * alpha, yaw: a.yaw + (b.yaw - a.yaw) * alpha };
56
+ }
57
+ export function rootMotionFeatures(track, time, derivativeStep = 1 / 30, horizons = HORIZONS, looping = false) {
58
+ validateHorizons(horizons);
59
+ if (!Number.isFinite(derivativeStep) || derivativeStep <= 0)
60
+ throw new Error('Invalid derivative step');
61
+ const now = sampleRootMotion(track, time, looping), a = sampleRootMotion(track, time - derivativeStep, looping), b = sampleRootMotion(track, time + derivativeStep, looping);
62
+ const elapsed = b.time - a.time;
63
+ const v = elapsed > 0 ? toLocal((b.x - a.x) / elapsed, (b.z - a.z) / elapsed, now.yaw) : [0, 0];
64
+ return { velocity: [v[0], 0, v[1]], angularVelocity: elapsed > 0 ? (b.yaw - a.yaw) / elapsed : 0,
65
+ trajectory: horizons.map(horizon => {
66
+ const future = sampleRootMotion(track, time + horizon, looping), yaw = future.yaw - now.yaw;
67
+ return { position: toLocal(future.x - now.x, future.z - now.z, now.yaw), facing: [Math.sin(yaw), Math.cos(yaw)] };
68
+ }) };
69
+ }
70
+ /** Crop to the same source interval as playback; preserve the source coordinate frame. */
71
+ export function cropRootMotion(track, start, end) {
72
+ validateRootMotion(track);
73
+ if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end <= start || end > track.duration)
74
+ throw new Error('Invalid root motion range');
75
+ const samples = [sampleRootMotion(track, start), ...track.samples.filter(s => s.time > start && s.time < end), sampleRootMotion(track, end)]
76
+ .map(s => ({ ...s, time: s.time - start }));
77
+ return { version: 1, source: track.source, duration: end - start, samples };
78
+ }
@@ -0,0 +1,56 @@
1
+ import type { Query, Vec3, TrajectoryPoint } from './types.js';
2
+ export type Normalization = 'standard' | 'none';
3
+ interface ChannelBase {
4
+ id: string;
5
+ weight: number;
6
+ normalization: Normalization;
7
+ }
8
+ export type FeatureChannel = ChannelBase & ({
9
+ kind: 'bone-position' | 'bone-velocity';
10
+ bone: string;
11
+ } | {
12
+ kind: 'root-velocity';
13
+ } | {
14
+ kind: 'trajectory-position' | 'trajectory-facing';
15
+ horizons: readonly number[];
16
+ });
17
+ export interface PoseSchema {
18
+ version: 1;
19
+ id: string;
20
+ channels: readonly FeatureChannel[];
21
+ }
22
+ export interface SchemaPose {
23
+ bones: Readonly<Record<string, {
24
+ position: Vec3;
25
+ velocity: Vec3;
26
+ }>>;
27
+ rootVelocity: Vec3;
28
+ }
29
+ export interface ChannelLayout {
30
+ channel: FeatureChannel;
31
+ offset: number;
32
+ dimensions: number;
33
+ }
34
+ export interface CompiledSchema {
35
+ schema: PoseSchema;
36
+ signature: string;
37
+ dimensions: number;
38
+ channels: readonly ChannelLayout[];
39
+ bones: readonly string[];
40
+ horizons: readonly number[];
41
+ }
42
+ export declare const LOCOMOTION_SCHEMA: PoseSchema;
43
+ export declare function validateHorizons(input: readonly number[]): void;
44
+ /** Strict JSON parsing: unsupported channels/options fail instead of being ignored. */
45
+ export declare function parseSchema(input: unknown): PoseSchema;
46
+ /** Canonical content identity, not a lossy hash. Order of channels is significant. */
47
+ export declare function compileSchema(input: PoseSchema): CompiledSchema;
48
+ /** One encoder shared by bake and runtime. Trajectory points follow the sorted union of horizons. */
49
+ export declare class SchemaQueryBuilder {
50
+ readonly layout: CompiledSchema;
51
+ private horizonIndices;
52
+ constructor(schema?: PoseSchema);
53
+ features(pose: SchemaPose, trajectory: readonly TrajectoryPoint[], out?: Float32Array<ArrayBuffer>): Float32Array;
54
+ build(pose: SchemaPose, trajectory: readonly TrajectoryPoint[]): Query;
55
+ }
56
+ export {};
@@ -0,0 +1,123 @@
1
+ export const LOCOMOTION_SCHEMA = {
2
+ version: 1, id: 'biped-locomotion', channels: [
3
+ { id: 'left-position', kind: 'bone-position', bone: 'leftFoot', weight: 1, normalization: 'standard' },
4
+ { id: 'right-position', kind: 'bone-position', bone: 'rightFoot', weight: 1, normalization: 'standard' },
5
+ { id: 'left-velocity', kind: 'bone-velocity', bone: 'leftFoot', weight: 1, normalization: 'standard' },
6
+ { id: 'right-velocity', kind: 'bone-velocity', bone: 'rightFoot', weight: 1, normalization: 'standard' },
7
+ { id: 'root-velocity', kind: 'root-velocity', weight: 1, normalization: 'standard' },
8
+ { id: 'future-position', kind: 'trajectory-position', horizons: [0.2, 0.4, 0.6], weight: 3, normalization: 'standard' },
9
+ { id: 'future-facing', kind: 'trajectory-facing', horizons: [0.2, 0.4, 0.6], weight: 3, normalization: 'standard' },
10
+ ],
11
+ };
12
+ for (const channel of LOCOMOTION_SCHEMA.channels) {
13
+ if ('horizons' in channel)
14
+ Object.freeze(channel.horizons);
15
+ Object.freeze(channel);
16
+ }
17
+ Object.freeze(LOCOMOTION_SCHEMA.channels);
18
+ Object.freeze(LOCOMOTION_SCHEMA);
19
+ const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
20
+ const name = (v) => typeof v === 'string' && /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(v);
21
+ export function validateHorizons(input) {
22
+ if (!Array.isArray(input) || input.length > 64 || [...input].some((v, i) => !Number.isFinite(v) || v <= 0 || v > 10 || (i > 0 && v <= input[i - 1])))
23
+ throw new Error('Schema: at most 64 horizons, increasing within (0, 10] seconds');
24
+ }
25
+ /** Strict JSON parsing: unsupported channels/options fail instead of being ignored. */
26
+ export function parseSchema(input) {
27
+ if (!record(input) || input.version !== 1 || !name(input.id) || Object.keys(input).some(k => !['version', 'id', 'channels'].includes(k))
28
+ || !Array.isArray(input.channels) || !input.channels.length || input.channels.length > 128)
29
+ throw new Error('Schema: invalid version/id/channels');
30
+ const ids = new Set();
31
+ const channels = [...input.channels].map(value => {
32
+ if (!record(value) || !name(value.id) || ids.has(value.id) || typeof value.weight !== 'number' || !Number.isFinite(Math.fround(value.weight)) || value.weight < 0 || (value.weight > 0 && Math.fround(value.weight) === 0)
33
+ || !['standard', 'none'].includes(String(value.normalization)))
34
+ throw new Error('Schema: invalid channel id/weight/normalization');
35
+ ids.add(value.id);
36
+ const common = { id: value.id, kind: value.kind, weight: value.weight, normalization: value.normalization };
37
+ let result;
38
+ if (value.kind === 'bone-position' || value.kind === 'bone-velocity') {
39
+ if (!name(value.bone) || value.bone === 'root')
40
+ throw new Error('Schema: invalid semantic bone role');
41
+ result = { ...common, bone: value.bone };
42
+ }
43
+ else if (value.kind === 'root-velocity')
44
+ result = common;
45
+ else if (value.kind === 'trajectory-position' || value.kind === 'trajectory-facing') {
46
+ if (!Array.isArray(value.horizons) || !value.horizons.length)
47
+ throw new Error('Schema: missing horizons');
48
+ validateHorizons(value.horizons);
49
+ result = { ...common, horizons: [...value.horizons] };
50
+ }
51
+ else
52
+ throw new Error('Schema: unsupported channel kind');
53
+ if (Object.keys(value).some(k => !Object.hasOwn(result, k)))
54
+ throw new Error('Schema: unsupported channel option');
55
+ return result;
56
+ });
57
+ if (!channels.some(c => c.weight > 0))
58
+ throw new Error('Schema: at least one positive weight required');
59
+ validateHorizons([...new Set(channels.flatMap(c => 'horizons' in c ? [...c.horizons] : []))].sort((a, b) => a - b));
60
+ return { version: 1, id: input.id, channels };
61
+ }
62
+ /** Canonical content identity, not a lossy hash. Order of channels is significant. */
63
+ export function compileSchema(input) {
64
+ const schema = parseSchema(input), bones = new Set(), horizons = new Set();
65
+ let dimensions = 0;
66
+ const channels = schema.channels.map(channel => {
67
+ if ('bone' in channel)
68
+ bones.add(channel.bone);
69
+ if ('horizons' in channel)
70
+ channel.horizons.forEach(h => horizons.add(h));
71
+ const width = 'horizons' in channel ? 2 * channel.horizons.length : 3;
72
+ const layout = Object.freeze({ channel, offset: dimensions, dimensions: width });
73
+ dimensions += width;
74
+ if ('horizons' in channel)
75
+ Object.freeze(channel.horizons);
76
+ Object.freeze(channel);
77
+ return layout;
78
+ });
79
+ Object.freeze(schema.channels);
80
+ Object.freeze(schema);
81
+ return Object.freeze({ schema, signature: 'pose-schema-v1:' + JSON.stringify(schema), dimensions,
82
+ channels: Object.freeze(channels), bones: Object.freeze([...bones]), horizons: Object.freeze([...horizons].sort((a, b) => a - b)) });
83
+ }
84
+ /** One encoder shared by bake and runtime. Trajectory points follow the sorted union of horizons. */
85
+ export class SchemaQueryBuilder {
86
+ layout;
87
+ horizonIndices;
88
+ constructor(schema = LOCOMOTION_SCHEMA) {
89
+ this.layout = compileSchema(schema);
90
+ this.horizonIndices = new Map(this.layout.horizons.map((h, i) => [h, i]));
91
+ }
92
+ features(pose, trajectory, out = new Float32Array(this.layout.dimensions)) {
93
+ if (trajectory.length !== this.layout.horizons.length || out.length !== this.layout.dimensions)
94
+ throw new Error('Invalid schema pose/trajectory dimensions');
95
+ const put = (values, offset, length) => {
96
+ if (!values || values.length !== length || !values.every(Number.isFinite))
97
+ throw new Error('Invalid schema feature values');
98
+ out.set(values, offset);
99
+ };
100
+ for (const { channel, offset } of this.layout.channels) {
101
+ if ('bone' in channel) {
102
+ const bone = Object.hasOwn(pose.bones, channel.bone) ? pose.bones[channel.bone] : undefined;
103
+ if (!bone)
104
+ throw new Error('Missing semantic bone: ' + channel.bone);
105
+ put(channel.kind === 'bone-position' ? bone.position : bone.velocity, offset, 3);
106
+ }
107
+ else if ('horizons' in channel) {
108
+ channel.horizons.forEach((h, i) => {
109
+ const point = trajectory[this.horizonIndices.get(h)];
110
+ put(channel.kind === 'trajectory-position' ? point?.position : point?.facing, offset + i * 2, 2);
111
+ });
112
+ }
113
+ else
114
+ put(pose.rootVelocity, offset, 3);
115
+ }
116
+ if (!out.every(Number.isFinite))
117
+ throw new Error('Invalid schema float32 feature values');
118
+ return out;
119
+ }
120
+ build(pose, trajectory) {
121
+ return { features: this.features(pose, trajectory), schemaSignature: this.layout.signature };
122
+ }
123
+ }
@@ -0,0 +1,17 @@
1
+ import { type Database, type Match, type Query } from './types.js';
2
+ import { type PoseSchema } from './schema.js';
3
+ export declare function defaultWeights(schema?: PoseSchema): Float32Array;
4
+ export declare class PoseSearch {
5
+ readonly database: Database;
6
+ private normalized;
7
+ private dimensions;
8
+ private rootOffset?;
9
+ readonly weights: Float32Array;
10
+ constructor(database: Database, weights?: Float32Array);
11
+ validateQuery(query: Query): void;
12
+ normalize(query: Query): void;
13
+ cost(index: number): number;
14
+ /** Physical error, independent of normalization/pose weights. Query must have been validated. */
15
+ rootVelocityError(index: number, query: Query): number;
16
+ search(query: Query): Match | null;
17
+ }
@@ -0,0 +1,97 @@
1
+ import { validateDatabase } from './database.js';
2
+ import { compileSchema, LOCOMOTION_SCHEMA } from './schema.js';
3
+ export function defaultWeights(schema = LOCOMOTION_SCHEMA) {
4
+ const layout = compileSchema(schema), weights = new Float32Array(layout.dimensions);
5
+ for (const { channel, offset, dimensions } of layout.channels)
6
+ weights.fill(channel.weight, offset, offset + dimensions);
7
+ if (!weights.every(Number.isFinite))
8
+ throw new Error('Invalid feature weights');
9
+ return weights;
10
+ }
11
+ const legacySignature = compileSchema(LOCOMOTION_SCHEMA).signature;
12
+ export class PoseSearch {
13
+ database;
14
+ normalized;
15
+ dimensions;
16
+ rootOffset;
17
+ weights;
18
+ constructor(database, weights) {
19
+ this.database = database;
20
+ validateDatabase(database);
21
+ this.dimensions = database.manifest.dimensions;
22
+ this.rootOffset = compileSchema(database.manifest.schema).channels.find(c => c.channel.kind === 'root-velocity')?.offset;
23
+ this.normalized = new Float32Array(this.dimensions);
24
+ weights ??= defaultWeights(database.manifest.schema);
25
+ if (weights.length !== this.dimensions || !weights.every(v => Number.isFinite(v) && v >= 0))
26
+ throw new Error('Invalid feature weights');
27
+ this.weights = weights.slice();
28
+ }
29
+ validateQuery(query) {
30
+ if (query.schemaSignature !== this.database.manifest.schemaSignature && !(query.schemaSignature === undefined && this.database.manifest.schemaSignature === legacySignature))
31
+ throw new Error('Incompatible query schema');
32
+ if (query.features.length !== this.dimensions || !query.features.every(Number.isFinite))
33
+ throw new Error('Invalid pose query');
34
+ if (query.allowTemporalReselection !== undefined && typeof query.allowTemporalReselection !== 'boolean')
35
+ throw new Error('Invalid temporal reselection option');
36
+ if (query.velocityTolerance !== undefined && (!Number.isFinite(query.velocityTolerance) || query.velocityTolerance < 0 || this.rootOffset === undefined))
37
+ throw new Error('Velocity tolerance requires a root-velocity channel and a finite nonnegative value');
38
+ }
39
+ normalize(query) {
40
+ this.validateQuery(query);
41
+ const { mean, std } = this.database.manifest;
42
+ for (let j = 0; j < this.dimensions; j++)
43
+ this.normalized[j] = (query.features[j] - mean[j]) / std[j];
44
+ if (!this.normalized.every(Number.isFinite))
45
+ throw new Error('Invalid normalized pose query');
46
+ }
47
+ cost(index) {
48
+ const { features } = this.database;
49
+ let sum = 0;
50
+ for (let j = 0; j < this.dimensions; j++)
51
+ sum += this.weights[j] * (this.normalized[j] - features[index * this.dimensions + j]) ** 2;
52
+ return sum;
53
+ }
54
+ /** Physical error, independent of normalization/pose weights. Query must have been validated. */
55
+ rootVelocityError(index, query) {
56
+ if (this.rootOffset === undefined || !Number.isInteger(index) || index < 0 || index >= this.database.manifest.frames.length)
57
+ return Infinity;
58
+ const { mean, std } = this.database.manifest;
59
+ let squared = 0;
60
+ for (let axis = 0; axis < 3; axis++) {
61
+ const j = this.rootOffset + axis, value = this.database.features[index * this.dimensions + j] * std[j] + mean[j];
62
+ squared += (value - query.features[j]) ** 2;
63
+ }
64
+ return Math.sqrt(squared);
65
+ }
66
+ search(query) {
67
+ this.normalize(query);
68
+ const { frames, clips } = this.database.manifest;
69
+ let index = -1, cost = Infinity;
70
+ let velocityLimit = Infinity;
71
+ if (query.velocityTolerance !== undefined) {
72
+ let nearest = Infinity;
73
+ for (let i = 0; i < frames.length; i++) {
74
+ if (query.excludedClips?.has(frames[i].clip) || (query.gaits && !query.gaits.has(clips[frames[i].clip].gait)))
75
+ continue;
76
+ nearest = Math.min(nearest, this.rootVelocityError(i, query));
77
+ }
78
+ // Sparse datasets retain a deterministic nearest-velocity fallback. Never
79
+ // expand the pool to arbitrary stationary poses just because coverage is missing.
80
+ velocityLimit = Math.max(query.velocityTolerance, nearest + 1e-6);
81
+ }
82
+ for (let i = 0; i < frames.length; i++) {
83
+ if (query.excludedClips?.has(frames[i].clip))
84
+ continue;
85
+ if (query.gaits && !query.gaits.has(clips[frames[i].clip].gait))
86
+ continue;
87
+ if (query.velocityTolerance !== undefined && this.rootVelocityError(i, query) > velocityLimit)
88
+ continue;
89
+ const score = this.cost(i);
90
+ if (score < cost) {
91
+ index = i;
92
+ cost = score;
93
+ }
94
+ }
95
+ return index < 0 ? null : { ...frames[index], index, cost };
96
+ }
97
+ }
@@ -0,0 +1,75 @@
1
+ import type { PoseSchema } from './schema.js';
2
+ /** Legacy preset constants; dynamic consumers must use compileSchema. */
3
+ export declare const DIMENSIONS = 27;
4
+ export declare const HORIZONS: readonly [0.2, 0.4, 0.6];
5
+ export type Vec3 = readonly [number, number, number];
6
+ export type Quat = readonly [number, number, number, number];
7
+ export type Gait = 'idle' | 'walk' | 'run' | 'strafe' | 'turn' | 'stop';
8
+ export interface Clip {
9
+ id: string;
10
+ file: string;
11
+ animation: string;
12
+ duration: number;
13
+ looping: boolean;
14
+ gait: Gait;
15
+ /** Explicit authoring velocity for in-place clips, never inferred root motion. */
16
+ nominalVelocity?: Vec3;
17
+ /** Hips is a measured pelvis proxy, not a dedicated animation root. */
18
+ motionSource?: 'nominal' | 'hips' | 'root';
19
+ /** Mean of measured local velocities; reference for demo controls, not gameplay authority. */
20
+ averageVelocity?: Vec3;
21
+ /** Source start in seconds; duration is the cropped duration. */
22
+ sourceStart?: number;
23
+ /** Optional absolute local orientation of the stripped rig root, as a static pose correction. */
24
+ rootOrientation?: Quat;
25
+ }
26
+ export interface Frame {
27
+ clip: number;
28
+ time: number;
29
+ }
30
+ export interface DatabaseManifest {
31
+ profileId?: string;
32
+ profileSignature?: string;
33
+ /** SHA-256 by public URL; verified by the loader. */
34
+ assetHashes?: Record<string, string>;
35
+ version: 2;
36
+ dimensions: number;
37
+ schema: PoseSchema;
38
+ schemaSignature: string;
39
+ sampleRate: number;
40
+ coordinates: 'meters-y-up-z-forward';
41
+ horizons: readonly number[];
42
+ clips: Clip[];
43
+ frames: Frame[];
44
+ mean: number[];
45
+ std: number[];
46
+ }
47
+ export interface Database {
48
+ manifest: DatabaseManifest;
49
+ features: Float32Array;
50
+ }
51
+ export interface Match extends Frame {
52
+ index: number;
53
+ cost: number;
54
+ }
55
+ export interface Query {
56
+ features: Float32Array;
57
+ schemaSignature?: string;
58
+ gaits?: ReadonlySet<Gait>;
59
+ excludedClips?: ReadonlySet<number>;
60
+ /** Optional root-velocity compatibility band in m/s. If uncovered, use nearest velocity before pose cost. */
61
+ velocityTolerance?: number;
62
+ /** False keeps the phase of the current loop; searches may still select another clip. Default true. */
63
+ allowTemporalReselection?: boolean;
64
+ }
65
+ export interface Pose {
66
+ leftPosition: Vec3;
67
+ rightPosition: Vec3;
68
+ leftVelocity: Vec3;
69
+ rightVelocity: Vec3;
70
+ rootVelocity: Vec3;
71
+ }
72
+ export interface TrajectoryPoint {
73
+ position: readonly [number, number];
74
+ facing: readonly [number, number];
75
+ }
@@ -0,0 +1,3 @@
1
+ /** Legacy preset constants; dynamic consumers must use compileSchema. */
2
+ export const DIMENSIONS = 27;
3
+ export const HORIZONS = [0.2, 0.4, 0.6];
@@ -0,0 +1,8 @@
1
+ export * from './core/types.js';
2
+ export * from './core/schema.js';
3
+ export * from './core/features.js';
4
+ export * from './core/database.js';
5
+ export * from './core/search.js';
6
+ export * from './core/controller.js';
7
+ export * from './core/root-motion.js';
8
+ export * from './core/profile.js';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from './core/types.js';
2
+ export * from './core/schema.js';
3
+ export * from './core/features.js';
4
+ export * from './core/database.js';
5
+ export * from './core/search.js';
6
+ export * from './core/controller.js';
7
+ export * from './core/root-motion.js';
8
+ export * from './core/profile.js';