@rig-c/core 2.71.0 → 2.73.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.
- package/README.md +10 -0
- package/docs/AUTHORING.md +172 -11
- package/docs/FACE.md +19 -0
- package/package.json +1 -1
- package/src/alpha_clip.ts +255 -0
- package/src/cli/core_commands.ts +2 -1
- package/src/cli/shared.ts +14 -1
- package/src/compile.ts +364 -27
- package/src/core/clipping.ts +5 -1
- package/src/core/events.ts +17 -5
- package/src/deformgen.ts +2 -2
- package/src/mesh.ts +3 -1
- package/src/meshcompare.ts +146 -22
- package/src/rig.ts +32 -7
- package/src/runtime/index.ts +8 -4
- package/src/runtime/model.ts +37 -20
- package/src/runtime/pose.ts +41 -1
- package/src/types.ts +26 -0
package/src/core/events.ts
CHANGED
|
@@ -33,6 +33,10 @@ import { gridRound } from './index.ts';
|
|
|
33
33
|
|
|
34
34
|
/** An event definition, as the model states it (`ModelEvent`): the payload a key falls back to. */
|
|
35
35
|
export interface CoreEventDef {
|
|
36
|
+
/** Audio metadata retained additively; the five-column oracle row is unchanged. */
|
|
37
|
+
audio?: string;
|
|
38
|
+
volume?: number;
|
|
39
|
+
balance?: number;
|
|
36
40
|
name: string;
|
|
37
41
|
int: number;
|
|
38
42
|
float: number;
|
|
@@ -41,6 +45,9 @@ export interface CoreEventDef {
|
|
|
41
45
|
|
|
42
46
|
/** One event key, as read: its time (float32) and the payload it fires with, each field resolved. */
|
|
43
47
|
export interface CoreEventKey {
|
|
48
|
+
/** Audio delivery values, as observed from the public EventTimeline API. */
|
|
49
|
+
volume?: number;
|
|
50
|
+
balance?: number;
|
|
44
51
|
time: number;
|
|
45
52
|
name: string;
|
|
46
53
|
int: number;
|
|
@@ -55,8 +62,8 @@ const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'obj
|
|
|
55
62
|
const EVENT_DEF_FIELDS = ['name', 'int', 'float', 'string', 'audio', 'volume', 'balance'] as const;
|
|
56
63
|
const EVENT_KEY_FIELDS = ['time', 'name', 'int', 'float', 'string', 'volume', 'balance'] as const;
|
|
57
64
|
|
|
58
|
-
function payload(raw: Record<string, unknown>, at: string, problems: string[]): { int?: number; float?: number; string?: string } {
|
|
59
|
-
const out: { int?: number; float?: number; string?: string } = {};
|
|
65
|
+
function payload(raw: Record<string, unknown>, at: string, problems: string[]): { int?: number; float?: number; string?: string; volume?: number; balance?: number } {
|
|
66
|
+
const out: { int?: number; float?: number; string?: string; volume?: number; balance?: number } = {};
|
|
60
67
|
if (raw.int !== undefined) {
|
|
61
68
|
if (typeof raw.int !== 'number' || !Number.isInteger(raw.int)) problems.push(`${at}: int is ${JSON.stringify(raw.int)}, not a whole number`);
|
|
62
69
|
else out.int = raw.int;
|
|
@@ -69,7 +76,10 @@ function payload(raw: Record<string, unknown>, at: string, problems: string[]):
|
|
|
69
76
|
if (typeof raw.string !== 'string') problems.push(`${at}: string is ${JSON.stringify(raw.string)}, not a string`);
|
|
70
77
|
else out.string = raw.string;
|
|
71
78
|
}
|
|
72
|
-
for (const key of ['volume', 'balance'] as const) if (raw[key] !== undefined
|
|
79
|
+
for (const key of ['volume', 'balance'] as const) if (raw[key] !== undefined) {
|
|
80
|
+
if (typeof raw[key] !== 'number' || !Number.isFinite(raw[key])) problems.push(`${at}: ${key} is ${JSON.stringify(raw[key])}, not a finite number`);
|
|
81
|
+
else out[key] = raw[key];
|
|
82
|
+
}
|
|
73
83
|
return out;
|
|
74
84
|
}
|
|
75
85
|
|
|
@@ -90,7 +100,7 @@ export function readEventDefs(value: unknown, problems: string[]): Map<string, C
|
|
|
90
100
|
if (raw.audio !== undefined && typeof raw.audio !== 'string') problems.push(`${at} "${raw.name}": audio is ${JSON.stringify(raw.audio)}, not a string`);
|
|
91
101
|
if (out.has(raw.name)) problems.push(`${at}: event "${raw.name}" is declared twice`);
|
|
92
102
|
const p = payload(raw, `${at} "${raw.name}"`, problems);
|
|
93
|
-
out.set(raw.name, { name: raw.name, int: p.int ?? 0, float: p.float ?? 0, string: p.string ?? '' });
|
|
103
|
+
out.set(raw.name, { audio: typeof raw.audio === 'string' ? raw.audio : '', volume: p.volume ?? 0, balance: p.balance ?? 0, name: raw.name, int: p.int ?? 0, float: p.float ?? 0, string: p.string ?? '' });
|
|
94
104
|
});
|
|
95
105
|
return out;
|
|
96
106
|
}
|
|
@@ -121,7 +131,9 @@ export function readEventKeys(value: unknown, label: string, defs: ReadonlyMap<s
|
|
|
121
131
|
return;
|
|
122
132
|
}
|
|
123
133
|
const p = payload(raw, at, problems);
|
|
124
|
-
|
|
134
|
+
// Observed oracle behavior: a voiced key's omitted balance falls back to
|
|
135
|
+
// the definition's volume, not its balance. A silent event keeps both at 0.
|
|
136
|
+
out.push({ volume: def.audio ? p.volume ?? def.volume ?? 0 : 0, balance: def.audio ? p.balance ?? def.volume ?? 0 : 0, time: Math.fround(typeof t === 'number' && Number.isFinite(t) ? t : 0), name: def.name, int: p.int ?? def.int, float: p.float ?? def.float, string: p.string ?? def.string });
|
|
125
137
|
});
|
|
126
138
|
return out;
|
|
127
139
|
}
|
package/src/deformgen.ts
CHANGED
|
@@ -374,7 +374,7 @@ export function evaluateDeformTransform(
|
|
|
374
374
|
round: Rounder,
|
|
375
375
|
where: string,
|
|
376
376
|
/**
|
|
377
|
-
* Per-vertex `z` for this attachment, when its generator named a depth map.
|
|
377
|
+
* Per-vertex `z` for this attachment, when it or its generator named a depth map.
|
|
378
378
|
* Null when it named none — which is what makes `"depth": true` refusable by
|
|
379
379
|
* name rather than silently falling back to a cylinder.
|
|
380
380
|
*/
|
|
@@ -448,7 +448,7 @@ export function evaluateDeformTransform(
|
|
|
448
448
|
if (fromDepth && depth === null) {
|
|
449
449
|
throw new CompileError(
|
|
450
450
|
`${where}: transform ${kind} says "depth": true and this attachment has no depth map. The map is named on ` +
|
|
451
|
-
'
|
|
451
|
+
'an authored attachment or its contour/grid generator, as `"depth": { "image": …, "near": …, "zScale": … }`, because it describes ' +
|
|
452
452
|
'the art rather than this key. Name it there, or give this key a radius.', 'CE_DEFORM_DEPTH_MAP_REQUIRED',
|
|
453
453
|
);
|
|
454
454
|
}
|
package/src/mesh.ts
CHANGED
|
@@ -930,7 +930,9 @@ export function segmentsMeet(
|
|
|
930
930
|
const o2 = o(a, b, d);
|
|
931
931
|
const o3 = o(c, d, a);
|
|
932
932
|
const o4 = o(c, d, b);
|
|
933
|
-
|
|
933
|
+
// An epsilon-zero orientation is not a strict side of a line. In that case
|
|
934
|
+
// only the endpoint tests below can establish contact (including the bounds).
|
|
935
|
+
if (o1 !== 0 && o2 !== 0 && o3 !== 0 && o4 !== 0 && o1 !== o2 && o3 !== o4) return true;
|
|
934
936
|
return onSegment(a, b, c) || onSegment(a, b, d) || onSegment(c, d, a) || onSegment(c, d, b);
|
|
935
937
|
}
|
|
936
938
|
|
package/src/meshcompare.ts
CHANGED
|
@@ -107,6 +107,7 @@ import {
|
|
|
107
107
|
type SourceMesh,
|
|
108
108
|
type WorstSample,
|
|
109
109
|
} from './meshquality.ts';
|
|
110
|
+
import { createHash } from 'node:crypto';
|
|
110
111
|
import { areaBand, stretchSingularValues, triangleAreas } from './areaband.ts';
|
|
111
112
|
import { uvCarriers, uvCarriersWith, type Carrier, type CarrierPlant } from './meshcarriers.ts';
|
|
112
113
|
import { CoreInputError, readModel, underNoSkin, underSkin, type CompiledDocument } from './core/index.ts';
|
|
@@ -187,6 +188,12 @@ export interface MotionComparisonInput {
|
|
|
187
188
|
overBound?: OverBoundRequest;
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
/** The candidate list is supplied on each use; every other input is bound to the reference. */
|
|
192
|
+
export type MotionReferenceInput = Omit<MotionComparisonInput, 'candidates'>;
|
|
193
|
+
|
|
194
|
+
/** An opaque reference prepared by {@link prepareMotionReference}. Its data is held outside the returned object. */
|
|
195
|
+
export interface PreparedMotionReference { readonly kind: 'prepared-motion-reference/1' }
|
|
196
|
+
|
|
190
197
|
/**
|
|
191
198
|
* Issue #1291's faults in carrying `motionAmplitude` into the setup measurement, for the `mesh-compare` suite's
|
|
192
199
|
* negative controls: the amplitude not handed over; `null` handed over as the field left out; the field not validated
|
|
@@ -896,6 +903,110 @@ function foldingSlots(raw: unknown, who: string): Set<string> {
|
|
|
896
903
|
return new Set(list as string[]);
|
|
897
904
|
}
|
|
898
905
|
|
|
906
|
+
type GeometryRead = { geometry: EvidenceSection | null; counts: MeshCounts | null };
|
|
907
|
+
type MotionRead = { section: EvidenceSection & { schedule: ScheduleUsed }; perFrame: NonNullable<CandidateReport['perFrame']>; overBound: OverBoundReport | null } | null;
|
|
908
|
+
interface PreparedMotionData {
|
|
909
|
+
frames: Planned[];
|
|
910
|
+
walks: Walk[];
|
|
911
|
+
folding: Set<string>;
|
|
912
|
+
scheduleUsed: ScheduleUsed;
|
|
913
|
+
samplesOf: Array<{ samples: Sample[]; art: number }>;
|
|
914
|
+
refCarriers: Array<Array<Carrier | null>>;
|
|
915
|
+
posedRef: Posed | { refused: string };
|
|
916
|
+
}
|
|
917
|
+
interface PreparedState {
|
|
918
|
+
reference: Build;
|
|
919
|
+
refs: AttachmentRef[];
|
|
920
|
+
order: number[];
|
|
921
|
+
refGeometry: GeometryRead;
|
|
922
|
+
motionData: PreparedMotionData | null;
|
|
923
|
+
referenceMotion: MotionRead;
|
|
924
|
+
digests: Record<string, string>;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const preparedReferences = new WeakMap<PreparedMotionReference, PreparedState>();
|
|
928
|
+
|
|
929
|
+
/** One digest per bound field, so a mismatch names the field instead of accepting a stale pose or mask. */
|
|
930
|
+
function boundDigests(input: MotionReferenceInput): Record<string, string> {
|
|
931
|
+
// JSON.stringify maps -0 to 0, drops undefined object fields, and maps non-finite numbers to null. A bound
|
|
932
|
+
// option's signed zero can survive validation and is echoed in a report, so these are not safe digest aliases.
|
|
933
|
+
const exact = (value: unknown, visiting = new Set<object>()): unknown => {
|
|
934
|
+
if (value === null) return ['null'];
|
|
935
|
+
if (typeof value === 'number') return ['number', Object.is(value, -0) ? '-0' : String(value)];
|
|
936
|
+
if (typeof value === 'string' || typeof value === 'boolean' || typeof value === 'undefined') return [typeof value, value ?? null];
|
|
937
|
+
if (typeof value !== 'object') return [typeof value, String(value)];
|
|
938
|
+
if (visiting.has(value)) throw new Error('cyclic bound input');
|
|
939
|
+
visiting.add(value);
|
|
940
|
+
const encoded = Array.isArray(value)
|
|
941
|
+
? ['array', Array.from({ length: value.length }, (_, i) => Object.prototype.hasOwnProperty.call(value, i) ? exact(value[i], visiting) : ['hole'])]
|
|
942
|
+
: ['object', Object.keys(value).map((key) => [key, exact((value as Record<string, unknown>)[key], visiting)])];
|
|
943
|
+
visiting.delete(value);
|
|
944
|
+
return encoded;
|
|
945
|
+
};
|
|
946
|
+
const digest = (value: unknown): string => createHash('sha256').update(JSON.stringify(exact(value))).digest('hex');
|
|
947
|
+
return {
|
|
948
|
+
reference: digest(input.reference),
|
|
949
|
+
schedule: digest(input.schedule),
|
|
950
|
+
attachments: digest(input.attachments.map((a) => ({
|
|
951
|
+
attachment: a.attachment,
|
|
952
|
+
art: { threshold: a.art.threshold, frame: a.art.frame, mask: {
|
|
953
|
+
width: a.art.mask.width,
|
|
954
|
+
height: a.art.mask.height,
|
|
955
|
+
alpha: createHash('sha256').update(a.art.mask.alpha).digest('hex'),
|
|
956
|
+
} },
|
|
957
|
+
finalThreshold: a.finalThreshold,
|
|
958
|
+
minArtSamples: a.minArtSamples,
|
|
959
|
+
regions: a.regions,
|
|
960
|
+
}))),
|
|
961
|
+
referenceArtFit: digest(input.referenceArtFit),
|
|
962
|
+
candidateArtFit: digest(input.candidateArtFit),
|
|
963
|
+
bounds: digest(input.bounds),
|
|
964
|
+
motionRequired: digest(input.motionRequired),
|
|
965
|
+
perFrame: digest(input.perFrame),
|
|
966
|
+
motionAmplitude: digest({ present: Object.prototype.hasOwnProperty.call(input, 'motionAmplitude'), value: input.motionAmplitude }),
|
|
967
|
+
overBound: digest({ present: Object.prototype.hasOwnProperty.call(input, 'overBound'), value: input.overBound }),
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** Prepare the source once for sequential candidates on the same schedule and the same art. */
|
|
972
|
+
export function prepareMotionReference(input: MotionReferenceInput): PreparedMotionReference {
|
|
973
|
+
if (!isObject(input)) missing('the prepared reference input', input, 'a MotionReferenceInput object');
|
|
974
|
+
const ref: unknown = input.reference;
|
|
975
|
+
let id = 'prepared-candidate';
|
|
976
|
+
while (isObject(ref) && id === ref.id) id += '-';
|
|
977
|
+
const model = isObject(ref) && typeof ref.model === 'string' ? ref.model : '';
|
|
978
|
+
validateInput({ ...input, candidates: [{ id, model }] }, null);
|
|
979
|
+
// No caller-owned array enters the cache: a later mutation is detected against this snapshot at use.
|
|
980
|
+
let snapshot: MotionReferenceInput;
|
|
981
|
+
try { snapshot = structuredClone(input); }
|
|
982
|
+
catch (e) { refuse('COMPARE_INPUT_MISSING', `the prepared reference input cannot be copied (${(e as Error).message}); required cloneable model, schedule, art and options`); }
|
|
983
|
+
let state: PreparedState | null = null;
|
|
984
|
+
const asComparison: MotionComparisonInput = { ...snapshot, candidates: [{ id, model: snapshot.reference.model }] };
|
|
985
|
+
compareMeshesInMotionWith(asComparison, null, undefined, (ready) => { state = ready; });
|
|
986
|
+
if (state === null) throw new Error('prepareMotionReference: reference preparation produced no state');
|
|
987
|
+
const ready: PreparedState = state;
|
|
988
|
+
try { ready.digests = boundDigests(snapshot); }
|
|
989
|
+
catch (e) { refuse('COMPARE_INPUT_MISSING', `the prepared reference input cannot be digested (${(e as Error).message}); required a complete mask and comparison options`); }
|
|
990
|
+
const handle: PreparedMotionReference = Object.freeze({ kind: 'prepared-motion-reference/1' });
|
|
991
|
+
preparedReferences.set(handle, ready);
|
|
992
|
+
return handle;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** Compare a candidate against the prepared source; every bound input must still have the prepared bytes. */
|
|
996
|
+
export function comparePreparedMotion(handle: PreparedMotionReference, input: MotionComparisonInput): MeshQualityReport {
|
|
997
|
+
const ready = preparedReferences.get(handle);
|
|
998
|
+
if (ready === undefined) refuse('COMPARE_PREPARED_HANDLE', 'the prepared reference handle was not made by prepareMotionReference; required its opaque handle');
|
|
999
|
+
let now: Record<string, string>;
|
|
1000
|
+
try { now = boundDigests(input); }
|
|
1001
|
+
catch { refuse('COMPARE_PREPARED_MISMATCH', 'the bound input cannot be digested; required the reference, schedule, attachments and options used to prepare the handle'); }
|
|
1002
|
+
for (const [field, digest] of Object.entries(ready.digests)) {
|
|
1003
|
+
if (now[field] !== digest) refuse('COMPARE_PREPARED_MISMATCH', `${field} changed after prepareMotionReference; required the same reference, schedule, art and options for this handle`);
|
|
1004
|
+
}
|
|
1005
|
+
const report = compareMeshesInMotionWith(input, null, ready);
|
|
1006
|
+
if (report === null) throw new Error('comparePreparedMotion: comparison produced no report');
|
|
1007
|
+
return report;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
899
1010
|
/**
|
|
900
1011
|
* Compare one or more candidate builds with a reference build in motion — §3
|
|
901
1012
|
* and §4's motion rows — and measure each build's art fit at the setup pose
|
|
@@ -907,7 +1018,9 @@ function foldingSlots(raw: unknown, who: string): Set<string> {
|
|
|
907
1018
|
* undercut at the setup pose fails `referenceArtFit`, and `measureMeshQuality`'s own codes for the art).
|
|
908
1019
|
*/
|
|
909
1020
|
export function compareMeshesInMotion(input: MotionComparisonInput): MeshQualityReport {
|
|
910
|
-
|
|
1021
|
+
const report = compareMeshesInMotionWith(input, null);
|
|
1022
|
+
if (report === null) throw new Error('compareMeshesInMotion: comparison produced no report');
|
|
1023
|
+
return report;
|
|
911
1024
|
}
|
|
912
1025
|
|
|
913
1026
|
/**
|
|
@@ -916,18 +1029,20 @@ export function compareMeshesInMotion(input: MotionComparisonInput): MeshQuality
|
|
|
916
1029
|
* surface*).
|
|
917
1030
|
*/
|
|
918
1031
|
export function compareMeshesInMotionPlanted(input: MotionComparisonInput, plant: ComparePlant): MeshQualityReport {
|
|
919
|
-
|
|
1032
|
+
const report = compareMeshesInMotionWith(input, plant);
|
|
1033
|
+
if (report === null) throw new Error('compareMeshesInMotionPlanted: comparison produced no report');
|
|
1034
|
+
return report;
|
|
920
1035
|
}
|
|
921
1036
|
|
|
922
|
-
function compareMeshesInMotionWith(input: MotionComparisonInput, plant: ComparePlant | null): MeshQualityReport {
|
|
1037
|
+
function compareMeshesInMotionWith(input: MotionComparisonInput, plant: ComparePlant | null, prepared?: PreparedState, capture?: (state: PreparedState) => void): MeshQualityReport | null {
|
|
923
1038
|
validateInput(input, plant);
|
|
924
1039
|
const amplitude = input.motionAmplitude;
|
|
925
1040
|
// What the setup measurement is handed: the field as the caller set it — left out as left out, `null` as `null`.
|
|
926
1041
|
const handed: { motionAmplitude?: MotionAmplitude | null } =
|
|
927
1042
|
plant === 'amplitude-not-carried' || amplitude === undefined || (plant === 'null-read-as-left-out' && amplitude === null) ? {} : { motionAmplitude: amplitude };
|
|
928
|
-
const refs = input.attachments.map((a) => a.attachment);
|
|
929
|
-
const reference = readBuild(input.reference, 'reference', refs);
|
|
930
|
-
const candidates = input.candidates.map((c, i) => readBuild(c, `candidates[${i}]`, refs));
|
|
1043
|
+
const refs = prepared?.refs ?? input.attachments.map((a) => a.attachment);
|
|
1044
|
+
const reference = prepared?.reference ?? readBuild(input.reference, 'reference', refs);
|
|
1045
|
+
const candidates = capture === undefined ? input.candidates.map((c, i) => readBuild(c, `candidates[${i}]`, refs)) : [];
|
|
931
1046
|
for (const c of candidates) {
|
|
932
1047
|
const d = firstDifference(reference.raw, c.raw, [], refs);
|
|
933
1048
|
if (d !== null) {
|
|
@@ -939,13 +1054,13 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
|
|
|
939
1054
|
}
|
|
940
1055
|
// The slots in skeleton order fix the order attachments are reported in (§2).
|
|
941
1056
|
const slotRank = new Map(reference.doc.slots.map((s, i) => [s.name, i]));
|
|
942
|
-
const order = input.attachments.map((_a, i) => i).sort((p, q) => (slotRank.get(refs[p].slot) ?? 0) - (slotRank.get(refs[q].slot) ?? 0) || (refs[p].attachment < refs[q].attachment ? -1 : refs[p].attachment > refs[q].attachment ? 1 : 0));
|
|
1057
|
+
const order = prepared?.order ?? input.attachments.map((_a, i) => i).sort((p, q) => (slotRank.get(refs[p].slot) ?? 0) - (slotRank.get(refs[q].slot) ?? 0) || (refs[p].attachment < refs[q].attachment ? -1 : refs[p].attachment > refs[q].attachment ? 1 : 0));
|
|
943
1058
|
for (const ref of refs) {
|
|
944
1059
|
if (!slotRank.has(ref.slot)) missing(`attachment ${nameOf(ref)}'s slot`, ref.slot, `one of the reference's slots [${[...slotRank.keys()].join(', ')}]`);
|
|
945
1060
|
}
|
|
946
1061
|
|
|
947
1062
|
// --- the setup art fit, each build under its own bounds (§3 (a)) ----------------------------
|
|
948
|
-
const geometryOf = (b: Build, fit: ArtFitBounds):
|
|
1063
|
+
const geometryOf = (b: Build, fit: ArtFitBounds): GeometryRead => {
|
|
949
1064
|
const sections: EvidenceSection[] = [];
|
|
950
1065
|
const counts: MeshCounts[] = [];
|
|
951
1066
|
for (const i of order) {
|
|
@@ -987,7 +1102,7 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
|
|
|
987
1102
|
};
|
|
988
1103
|
return { geometry: joinSections(sections), counts: total };
|
|
989
1104
|
};
|
|
990
|
-
const refGeometry = geometryOf(reference, input.referenceArtFit);
|
|
1105
|
+
const refGeometry = prepared?.refGeometry ?? geometryOf(reference, input.referenceArtFit);
|
|
991
1106
|
// §3 *Independent evidence* (a), P8: a reference that fails its own art fit — coverage, overshoot or undercut against
|
|
992
1107
|
// its own mask under `referenceArtFit` — is refused AS A REFERENCE: a deviation from a reference that does not carry
|
|
993
1108
|
// its own art is not evidence, so no candidate is measured against it. (a) is those three rows and no other: winding
|
|
@@ -1006,24 +1121,26 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
|
|
|
1006
1121
|
|
|
1007
1122
|
// --- motion ---------------------------------------------------------------------------------
|
|
1008
1123
|
const schedule = input.schedule;
|
|
1009
|
-
type Motion = { section: EvidenceSection & { schedule: ScheduleUsed }; perFrame: NonNullable<CandidateReport['perFrame']>; overBound: OverBoundReport | null } | null;
|
|
1010
1124
|
// Issue #1315: the request as the caller set it; `?? undefined` only matters under the unvalidated plant.
|
|
1011
1125
|
const overBound: OverBoundRequest | undefined = input.overBound ?? undefined;
|
|
1012
|
-
let motionOf: (b: Build, isReference: boolean) =>
|
|
1126
|
+
let motionOf: (b: Build, isReference: boolean) => MotionRead = () => null;
|
|
1127
|
+
let motionData: PreparedMotionData | null = null;
|
|
1013
1128
|
if (schedule !== null) {
|
|
1014
|
-
const
|
|
1015
|
-
const
|
|
1016
|
-
const
|
|
1017
|
-
const
|
|
1129
|
+
const existing = prepared?.motionData;
|
|
1130
|
+
const planned = existing === undefined ? planSchedule(schedule, reference.doc, reference.doc.constraints.filter((c) => c.kind === 'physics').length) : null;
|
|
1131
|
+
const frames = existing?.frames ?? planned!.frames;
|
|
1132
|
+
const walks = existing?.walks ?? planned!.walks;
|
|
1133
|
+
const folding = existing?.folding ?? foldingSlots(reference.raw, `reference "${reference.id}"`);
|
|
1134
|
+
const scheduleUsed: ScheduleUsed = existing?.scheduleUsed ?? {
|
|
1018
1135
|
...schedule,
|
|
1019
1136
|
walked: frames.map((f) => f.ref),
|
|
1020
1137
|
roles: (['baseline', 'selection', 'held-out'] as const).filter((r) => frames.some((f) => f.ref.role === r)),
|
|
1021
1138
|
heldOutClaim: frames.some((f) => f.ref.role === 'held-out'),
|
|
1022
1139
|
reset: 'physics reset at time 0',
|
|
1023
|
-
walks: walks.map((w, i) => ({ animation: w.animation, phase: w.phase, steps: walkSteps[i] })),
|
|
1140
|
+
walks: walks.map((w, i) => ({ animation: w.animation, phase: w.phase, steps: planned!.walkSteps[i] })),
|
|
1024
1141
|
};
|
|
1025
1142
|
// The samples, per attachment: art pixel centres at the final threshold, then the reference hull's UVs (§3).
|
|
1026
|
-
const samplesOf = order.map((i) => {
|
|
1143
|
+
const samplesOf = existing?.samplesOf ?? order.map((i) => {
|
|
1027
1144
|
const a = input.attachments[i];
|
|
1028
1145
|
const { mask, frame } = a.art;
|
|
1029
1146
|
const bits = artOf(mask, a.finalThreshold);
|
|
@@ -1046,9 +1163,10 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
|
|
|
1046
1163
|
const carrierPlant: CarrierPlant | null = plant === 'slack-support-unread' || plant === 'narrowest-slack' || plant === 'any-shared-vertex' ? plant : null;
|
|
1047
1164
|
const carriersOf = (uvs: readonly number[], triangles: readonly number[], samples: ReadonlyArray<{ uv: [number, number] }>, who: string): Array<Carrier | null> =>
|
|
1048
1165
|
uvCarriersWith(uvs, triangles, samples, who, carrierPlant);
|
|
1049
|
-
const refCarriers = order.map((i, k) => carriersOf(reference.meshes[i].uvs, reference.meshes[i].triangles, samplesOf[k].samples, `reference "${reference.id}", attachment ${nameOf(refs[i])}`));
|
|
1166
|
+
const refCarriers = existing?.refCarriers ?? order.map((i, k) => carriersOf(reference.meshes[i].uvs, reference.meshes[i].triangles, samplesOf[k].samples, `reference "${reference.id}", attachment ${nameOf(refs[i])}`));
|
|
1050
1167
|
const clock = plant === 'frame-physics-clock' ? 'frame' : 'render';
|
|
1051
|
-
const posedRef = poseBuild(reference.doc, order.map((i) => refs[i]), frames, walks, clock);
|
|
1168
|
+
const posedRef = existing?.posedRef ?? poseBuild(reference.doc, order.map((i) => refs[i]), frames, walks, clock);
|
|
1169
|
+
motionData = { frames, walks, folding, scheduleUsed, samplesOf, refCarriers, posedRef };
|
|
1052
1170
|
|
|
1053
1171
|
motionOf = (b: Build, isReference: boolean) => {
|
|
1054
1172
|
const carriers = isReference ? refCarriers : order.map((i, k) => carriersOf(b.meshes[i].uvs, b.meshes[i].triangles, samplesOf[k].samples, `candidate "${b.id}", attachment ${nameOf(refs[i])}`));
|
|
@@ -1311,9 +1429,13 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
|
|
|
1311
1429
|
return { section: { ...sectionOf(grouped), schedule: scheduleUsed }, perFrame, overBound: located };
|
|
1312
1430
|
};
|
|
1313
1431
|
}
|
|
1314
|
-
const referenceMotion = motionOf(reference, true);
|
|
1432
|
+
const referenceMotion = prepared?.referenceMotion ?? motionOf(reference, true);
|
|
1433
|
+
if (capture !== undefined) {
|
|
1434
|
+
capture({ reference, refs, order, refGeometry, motionData, referenceMotion, digests: {} });
|
|
1435
|
+
return null;
|
|
1436
|
+
}
|
|
1315
1437
|
const candidateMotions = candidates.map((c) => motionOf(c, false));
|
|
1316
|
-
const reportOf = (b: Build, geometry:
|
|
1438
|
+
const reportOf = (b: Build, geometry: GeometryRead, measured: MotionRead, isCandidate: boolean): CandidateReport => {
|
|
1317
1439
|
const motion = measured === null ? null : measured.section;
|
|
1318
1440
|
// P6: the geometry verdict pass (every required row measured and passing), and — when motion is required — the motion verdict pass.
|
|
1319
1441
|
const accepted = geometry.geometry !== null && geometry.geometry.verdict === 'pass' && (!input.motionRequired || (motion !== null && motion.verdict === 'pass'));
|
|
@@ -1354,7 +1476,7 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
|
|
|
1354
1476
|
: {}
|
|
1355
1477
|
: { overBound: { maxSamples: overBound.maxSamples, triangles: overBound.triangles } }),
|
|
1356
1478
|
};
|
|
1357
|
-
|
|
1479
|
+
const report: MeshQualityReport = {
|
|
1358
1480
|
spec: MESH_QUALITY_REPORT_SPEC,
|
|
1359
1481
|
operation: 'compare',
|
|
1360
1482
|
effective,
|
|
@@ -1365,4 +1487,6 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
|
|
|
1365
1487
|
candidates: candidates.map((c, i) => reportOf(c, candGeometry[i], candidateMotions[i], true)),
|
|
1366
1488
|
termination: null,
|
|
1367
1489
|
};
|
|
1490
|
+
// The prepared geometry, pose and reference rows live across calls. A caller may edit a returned report.
|
|
1491
|
+
return prepared === undefined ? report : structuredClone(report);
|
|
1368
1492
|
}
|
package/src/rig.ts
CHANGED
|
@@ -471,8 +471,8 @@ export interface RigDepthMap {
|
|
|
471
471
|
*/
|
|
472
472
|
near: 'white' | 'black';
|
|
473
473
|
/**
|
|
474
|
-
* How many
|
|
475
|
-
*
|
|
474
|
+
* How many units the map's full 0..1 range spans: setup world units on an
|
|
475
|
+
* authored mesh, the generator's bind units on a generated mesh.
|
|
476
476
|
*
|
|
477
477
|
* Authored, never measured: 8 bits of level say nothing about scale, so a
|
|
478
478
|
* compiler that picked one would be inventing the depth of the art.
|
|
@@ -971,6 +971,8 @@ export interface RigMeshAttachment {
|
|
|
971
971
|
color?: string;
|
|
972
972
|
/** Build the geometry instead of authoring it — see `RigMeshGenerator`. */
|
|
973
973
|
generator?: RigMeshGenerator;
|
|
974
|
+
/** A setup-world depth turn over authored geometry; sampled in the part's pixel grid. */
|
|
975
|
+
depth?: RigDepthMap;
|
|
974
976
|
/** A numbered image series over this one triangulation — see `RigSequence`. */
|
|
975
977
|
sequence?: RigSequence;
|
|
976
978
|
}
|
|
@@ -1122,7 +1124,26 @@ export interface RigBoundingBoxAttachment extends RigVertexGeometry {
|
|
|
1122
1124
|
* never ends — it runs to the bottom of the draw order and takes every slot
|
|
1123
1125
|
* below it with it. rigc refuses a name the rig does not declare.
|
|
1124
1126
|
*/
|
|
1125
|
-
|
|
1127
|
+
/** A clipping outline measured from one named attachment's plate. No inferred parameters. */
|
|
1128
|
+
export interface RigAlphaClip {
|
|
1129
|
+
slot: string;
|
|
1130
|
+
skin: string;
|
|
1131
|
+
attachment: string;
|
|
1132
|
+
alpha: number;
|
|
1133
|
+
tolerance: number;
|
|
1134
|
+
maxVertices: number;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
export interface RigClippingAttachment {
|
|
1138
|
+
/** Authored geometry uses the same named/raw contract as RigVertexGeometry. */
|
|
1139
|
+
vertices?: number[];
|
|
1140
|
+
weights?: RigMeshBinding[][];
|
|
1141
|
+
boneIndexing?: 'name' | 'raw';
|
|
1142
|
+
color?: string;
|
|
1143
|
+
/** Required for authored geometry; derived for alphaClip. */
|
|
1144
|
+
vertexCount?: number;
|
|
1145
|
+
/** Exclusive with authored vertexCount/vertices/weights/boneIndexing. */
|
|
1146
|
+
alphaClip?: RigAlphaClip;
|
|
1126
1147
|
type: 'clipping';
|
|
1127
1148
|
/** The runtime's attachment name — see `RigAttachmentName`. Absent, it is the placeholder. */
|
|
1128
1149
|
name?: RigAttachmentName;
|
|
@@ -2077,7 +2098,7 @@ export const RIG_KEYS = {
|
|
|
2077
2098
|
RigRegionAttachment: ['type', 'name', 'path', 'image', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'width', 'height', 'color', 'sequence'],
|
|
2078
2099
|
RigMeshAttachment: [
|
|
2079
2100
|
'type', 'name', 'path', 'image', 'uvs', 'triangles', 'vertices', 'weights', 'boneIndexing', 'hull', 'edges',
|
|
2080
|
-
'width', 'height', 'color', 'generator', 'sequence',
|
|
2101
|
+
'width', 'height', 'color', 'generator', 'depth', 'sequence',
|
|
2081
2102
|
],
|
|
2082
2103
|
RigLinkedMeshAttachment: [
|
|
2083
2104
|
'type', 'name', 'path', 'image', 'source', 'slot', 'skin', 'timelines', 'width', 'height', 'color', 'sequence',
|
|
@@ -2087,7 +2108,8 @@ export const RIG_KEYS = {
|
|
|
2087
2108
|
RigMeshBinding: ['bone', 'x', 'y', 'weight'],
|
|
2088
2109
|
RigSequence: ['count', 'start', 'digits', 'setup'],
|
|
2089
2110
|
RigBoundingBoxAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'name'],
|
|
2090
|
-
RigClippingAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'name', 'end', 'convex', 'inverse'],
|
|
2111
|
+
RigClippingAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'name', 'end', 'convex', 'inverse', 'alphaClip'],
|
|
2112
|
+
RigAlphaClip: ['slot', 'skin', 'attachment', 'alpha', 'tolerance', 'maxVertices'],
|
|
2091
2113
|
RigPathAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'name', 'closed', 'constantSpeed', 'lengths'],
|
|
2092
2114
|
RigRingGenerator: ['kind', 'hull', 'center', 'inner', 'size', 'bias', 'controls'],
|
|
2093
2115
|
RigRibbonGenerator: ['kind', 'size', 'rows', 'chain'],
|
|
@@ -2201,7 +2223,7 @@ export const RIG_TYPES = {
|
|
|
2201
2223
|
RigMeshAttachment: {
|
|
2202
2224
|
type: 'enum', name: 'string', path: 'string', image: 'string', uvs: 'number[]', triangles: 'number[]',
|
|
2203
2225
|
vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', hull: 'number', edges: 'number[]',
|
|
2204
|
-
width: 'number', height: 'number', color: 'string', generator: 'object', sequence: 'object',
|
|
2226
|
+
width: 'number', height: 'number', color: 'string', generator: 'object', depth: 'object', sequence: 'object',
|
|
2205
2227
|
},
|
|
2206
2228
|
RigLinkedMeshAttachment: {
|
|
2207
2229
|
type: 'enum', name: 'string', path: 'string', image: 'string', source: 'string', slot: 'string', skin: 'string',
|
|
@@ -2216,8 +2238,9 @@ export const RIG_TYPES = {
|
|
|
2216
2238
|
},
|
|
2217
2239
|
RigClippingAttachment: {
|
|
2218
2240
|
vertexCount: 'number', vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', color: 'string', type: 'enum', name: 'string',
|
|
2219
|
-
end: 'string', convex: 'boolean', inverse: 'boolean',
|
|
2241
|
+
end: 'string', convex: 'boolean', inverse: 'boolean', alphaClip: 'object',
|
|
2220
2242
|
},
|
|
2243
|
+
RigAlphaClip: { slot: 'string', skin: 'string', attachment: 'string', alpha: 'number', tolerance: 'number', maxVertices: 'number' },
|
|
2221
2244
|
RigPathAttachment: {
|
|
2222
2245
|
vertexCount: 'number', vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', color: 'string', type: 'enum', name: 'string',
|
|
2223
2246
|
closed: 'boolean', constantSpeed: 'boolean', lengths: 'number[]',
|
|
@@ -2548,6 +2571,7 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): Array<{
|
|
|
2548
2571
|
}
|
|
2549
2572
|
const here = [...base, slot, placeholder];
|
|
2550
2573
|
at(att, shape, `${who} (${type})`, here);
|
|
2574
|
+
if (type === 'clipping') at(att.alphaClip, 'RigAlphaClip', `${who} alphaClip`, [...here, 'alphaClip']);
|
|
2551
2575
|
if (att.sequence !== undefined) {
|
|
2552
2576
|
at(att.sequence, 'RigSequence', `${who} "sequence"`, [...here, 'sequence']);
|
|
2553
2577
|
checkRigSequence(att, who, where);
|
|
@@ -2557,6 +2581,7 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): Array<{
|
|
|
2557
2581
|
at(binding, 'RigMeshBinding', `${who} weights[${i}][${j}]`, [...here, 'weights', i, j]);
|
|
2558
2582
|
}
|
|
2559
2583
|
}
|
|
2584
|
+
at(att.depth, 'RigDepthMap', `${who} depth`, [...here, 'depth']);
|
|
2560
2585
|
if (!isObj(att.generator)) continue;
|
|
2561
2586
|
const gen = att.generator;
|
|
2562
2587
|
const kind = RIG_GENERATOR_KINDS.find((k) => k === gen.kind);
|
package/src/runtime/index.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* corpus through both at the oracle's samples and compares full doubles at
|
|
22
22
|
* tolerance 0 (*The runtime poser* there). The oracle is not changed for it.
|
|
23
23
|
*
|
|
24
|
-
* ## Stage
|
|
24
|
+
* ## Stage 1: what it poses, and what it refuses
|
|
25
25
|
*
|
|
26
26
|
* Bones and their five inherit modes; the rotate, translate, scale and shear
|
|
27
27
|
* timelines; a slot's attachment and colour (`rgba`, `rgb`, `alpha`)
|
|
@@ -32,10 +32,14 @@
|
|
|
32
32
|
* looping track. Everything else is refused at load by name with the list of
|
|
33
33
|
* what is missing (`./model.ts`'s header and `RUNTIME_FEATURES`); a consumer
|
|
34
34
|
* falls back to the oracle for a refused model or animation, so nothing breaks
|
|
35
|
-
* while coverage grows.
|
|
35
|
+
* while coverage grows. Named skin views and their required bones are posed;
|
|
36
|
+
* to switch, compile the new underSkin/underNoSkin view at load and restore the
|
|
37
|
+
* animation/time and event cursor with newPose.restore(name, old.trackTime,
|
|
38
|
+
* old.time). Restoration delivers no already-passed cue. `pose.events` borrows immutable seven-field
|
|
39
|
+
* deliveries over the last interval, in oracle order, until the next apply/step.
|
|
36
40
|
*
|
|
37
41
|
* ```ts
|
|
38
|
-
* const model = compileRuntime(underNoSkin(readModel(text))
|
|
42
|
+
* const model = compileRuntime(underNoSkin(readModel(text)));
|
|
39
43
|
* const pose = new RuntimePose(model);
|
|
40
44
|
* pose.setAnimation('walk');
|
|
41
45
|
* pose.step(1 / 60); // or pose.apply(t)
|
|
@@ -76,5 +80,5 @@ export {
|
|
|
76
80
|
L_SHEAR_X,
|
|
77
81
|
L_SHEAR_Y,
|
|
78
82
|
} from './model.ts';
|
|
79
|
-
export type { RuntimeAnimation, RuntimeAttachment, RuntimeFeature, RuntimeMissing, RuntimeModel, RuntimeOptions, RuntimePlant, RuntimeSlot } from './model.ts';
|
|
83
|
+
export type { RuntimeAnimation, RuntimeEvent, RuntimeAttachment, RuntimeFeature, RuntimeMissing, RuntimeModel, RuntimeOptions, RuntimePlant, RuntimeSlot } from './model.ts';
|
|
80
84
|
export { RuntimePose } from './pose.ts';
|