@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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The runtime poser's load-time half (issue #1591, stage 0): a skin view of
2
+ * The runtime poser's load-time half (issue #1591, stage 1): a skin view of
3
3
  * the compiled model (`readModel`, then `underSkin` / `underNoSkin` — the
4
4
  * oracle's reader and its views, read and never written) compiled once into
5
5
  * index-resolved bones and slots, a constraint plan, compiled timelines and
@@ -12,8 +12,7 @@
12
12
  * (`RuntimeMissing`), and:
13
13
  *
14
14
  * - **refuses the model** (`RuntimeRefusal`, every missing feature listed)
15
- * when one is a property of the whole model — a skin other than `default`, a
16
- * path, physics or slider constraint, a transform constraint reading or
15
+ * when one is a property of the whole model — a path, physics or slider constraint, a transform constraint reading or
17
16
  * writing local values or adding (`localSource`, `localTarget`,
18
17
  * `additive`), an ik or transform constraint over a bone the view leaves
19
18
  * inactive, a world-space transform onto a `noScale` or
@@ -23,16 +22,15 @@
23
22
  * - **refuses an animation** (`RuntimeModel.refused`; `animation(name)`
24
23
  * throws the same `RuntimeRefusal`) when the construct is the animation's
25
24
  * — an `inherit` timeline, a deform or sequence timeline, a draw-order
26
- * timeline, event keys, a dark-colour (`rgba2`, `rgb2`) timeline, or an
25
+ * timeline, a dark-colour (`rgba2`, `rgb2`) timeline, or an
27
26
  * attachment key showing a clipping attachment or a region with no atlas
28
27
  * rectangle. Every other animation of the model is posed.
29
28
  *
30
- * `RUNTIME_FEATURES` is the closed list of what can be named. Events are the
31
- * one construct that moves no pose, so they alone can be ignored on request
32
- * (`RuntimeOptions.ignore`): a consumer that reads no event says so, and is
33
- * then handed a poser that fires none. Without the request an animation with
34
- * event keys is refused, because a poser that dropped them silently would be
35
- * a player that misses its cues and reports nothing.
29
+ * `RUNTIME_FEATURES` is the closed list of what can be named. Stage 1 reads
30
+ * named/no-skin views and delivers event keys, with immutable payloads compiled
31
+ * at load. `ignore: ['events']` remains an explicit diagnostic opt-out; it is
32
+ * never the default. A merged view with conflicting attachments still refuses
33
+ * by name, as the oracle itself cannot resolve that view without file order.
36
34
  *
37
35
  * ## What the oracle decides, the runtime reads
38
36
  *
@@ -48,18 +46,16 @@
48
46
  * no randomness, no I/O.
49
47
  */
50
48
  import type { ModelVertices } from '../model.ts';
51
- import { activeBones, constraintRecords, CORE_REGION_KINDS, foldInheritMode, readBlend, readColour, shownAttachment, shownRow, sourceOfDoc, type CompiledDocument, type CoreAnimation, type CoreBlendMode } from '../core/index.ts';
49
+ import { activeBones, constraintRecords, CORE_REGION_KINDS, foldInheritMode, readBlend, readColour, shownAttachment, shownRow, sourceOfDoc, underSkin, type CompiledDocument, type CoreAnimation, type CoreBlendMode } from '../core/index.ts';
52
50
  import { constraintInactiveWhy, constraintsAbsentWhy, TRANSFORM_PROPERTIES, type CoreIkRecord, type CoreTransformRecord } from '../core/constraints.ts';
53
- import { CORE_DEFAULT_SKIN } from '../core/skins.ts';
54
51
  import { RUNTIME_PI } from '../core/world.ts';
55
52
  import { compileCurve, type RuntimeCurve } from './curves.ts';
56
53
 
57
54
  /** The stage of issue #1591 this poser implements. */
58
- export const RUNTIME_STAGE = 0;
55
+ export const RUNTIME_STAGE = 1;
59
56
 
60
57
  /** Every construct the runtime poser can refuse, by the name its refusals carry. */
61
58
  export const RUNTIME_FEATURES = [
62
- 'skin',
63
59
  'path-constraint',
64
60
  'physics-constraint',
65
61
  'slider-constraint',
@@ -75,7 +71,6 @@ export const RUNTIME_FEATURES = [
75
71
  'deform-timeline',
76
72
  'sequence-timeline',
77
73
  'draw-order-timeline',
78
- 'events',
79
74
  'dark-colour-timeline',
80
75
  ] as const;
81
76
  export type RuntimeFeature = (typeof RUNTIME_FEATURES)[number];
@@ -124,6 +119,12 @@ export interface RuntimePlant {
124
119
  readonly admit?: readonly RuntimeFeature[];
125
120
  /** The update order's scratch sized by the bone count instead of the longest target list: the short-buffer plant. */
126
121
  readonly scratchByBones?: boolean;
122
+ /** Wrong-skin source selected by a control, never by a consumer. */
123
+ readonly attachmentSkin?: string;
124
+ /** Drops a key at zero on a wrap, the event boundary plant. */
125
+ readonly eventWrapOffByOne?: boolean;
126
+ /** The old restore behavior, which repeats keys up to the restored time. */
127
+ readonly restoreEventsFromStart?: boolean;
127
128
  }
128
129
 
129
130
  // A bone's local values, seven per bone.
@@ -277,6 +278,17 @@ export interface RuntimeConstraintTimeline {
277
278
  readonly flags: Uint8Array | null;
278
279
  }
279
280
 
281
+ /** One immutable event delivery, reused by every pose of this model. */
282
+ export interface RuntimeEvent {
283
+ readonly name: string;
284
+ readonly time: number;
285
+ readonly int: number;
286
+ readonly float: number;
287
+ readonly string: string;
288
+ readonly volume: number;
289
+ readonly balance: number;
290
+ }
291
+
280
292
  /** One covered animation, compiled. */
281
293
  export interface RuntimeAnimation {
282
294
  readonly name: string;
@@ -287,12 +299,16 @@ export interface RuntimeAnimation {
287
299
  readonly constraints: ReadonlyArray<RuntimeConstraintTimeline | null>;
288
300
  /** How many key cursors a pose keeps for it. */
289
301
  readonly cursors: number;
290
- /** Event keys it holds — 0 unless the consumer asked for events to be ignored. */
302
+ /** Number of authored event keys (including an explicitly muted diagnostic). */
291
303
  readonly events: number;
304
+ readonly eventKeys: readonly RuntimeEvent[];
292
305
  }
293
306
 
294
307
  /** The compiled model a pose is made of (`compileRuntime`). Read-only once built. */
295
308
  export interface RuntimeModel {
309
+ /** Control-only boundary defect; fixed at load. */
310
+ readonly eventWrapOffByOne: boolean;
311
+ readonly restoreEventsFromStart: boolean;
296
312
  readonly doc: CompiledDocument;
297
313
  readonly boneNames: readonly string[];
298
314
  readonly parents: Int32Array;
@@ -365,7 +381,6 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
365
381
  const refusals = new Refusals(new Set(plant.admit ?? []));
366
382
  const ignoreEvents = (options.ignore ?? []).includes('events');
367
383
 
368
- if (doc.skins.some((k) => k.name !== CORE_DEFAULT_SKIN)) refusals.note('skin', `the document declares skins [${doc.skins.map((k) => k.name).join(', ')}]; stage ${RUNTIME_STAGE} poses the default skin alone`);
369
384
  const absentWhy = constraintsAbsentWhy(doc);
370
385
  if (absentWhy !== null) refusals.note('oracle-absent', `the oracle leaves the bones out: ${absentWhy}`);
371
386
 
@@ -440,6 +455,7 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
440
455
  const attachments: RuntimeAttachment[] = [];
441
456
  let vertexCount = 0;
442
457
  const sourceOf = sourceOfDoc(doc);
458
+ const attachmentView = plant.attachmentSkin === undefined ? doc : underSkin(doc, plant.attachmentSkin);
443
459
  /** Per slot, each placeholder's attachment index (−1 shows nothing) and what it misses, resolved once and its refusal noted for every animation that shows it. */
444
460
  const resolvedBySlot = doc.slots.map(() => new Map<string | null, { index: number; missing: readonly [RuntimeFeature, string] | null }>());
445
461
  const resolve = (si: number, placeholder: string | null, animation: string | null): number => {
@@ -454,7 +470,7 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
454
470
  };
455
471
  const resolveRecord = (si: number, placeholder: string | null): { index: number; missing: readonly [RuntimeFeature, string] | null } => {
456
472
  const slot = doc.slots[si];
457
- const shown = placeholder === null ? null : shownAttachment(doc, slot, placeholder);
473
+ const shown = placeholder === null ? null : shownAttachment(attachmentView, slot, placeholder);
458
474
  if (shown === null) return { index: -1, missing: null };
459
475
  if ('conflict' in shown) return { index: -1, missing: ['skin-conflict', `slot "${slot.name}" placeholder "${placeholder}" is filled by skins that disagree`] };
460
476
  const row = shownRow(shown);
@@ -538,6 +554,8 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
538
554
  if (refusals.model.length > 0) throw new RuntimeRefusal(refusals.model);
539
555
  const names = doc.animations.map((a) => a.name);
540
556
  return {
557
+ eventWrapOffByOne: plant.eventWrapOffByOne === true,
558
+ restoreEventsFromStart: plant.restoreEventsFromStart === true,
541
559
  doc,
542
560
  boneNames: doc.bones.map((b) => b.name),
543
561
  parents,
@@ -667,7 +685,6 @@ function compileAnimation(
667
685
  if (deform.length > 0) refusals.note('deform-timeline', `${deform.length} deform timeline(s): ${deform.slice(0, 3).map((a) => `${a.skin}/${a.slot}/${a.attachment}`).join(', ')}${deform.length > 3 ? ', …' : ''}`, name);
668
686
  if (sequence.length > 0) refusals.note('sequence-timeline', `${sequence.length} sequence timeline(s): ${sequence.slice(0, 3).map((a) => `${a.skin}/${a.slot}/${a.attachment}`).join(', ')}${sequence.length > 3 ? ', …' : ''}`, name);
669
687
  if (tl.drawOrder.length > 0) refusals.note('draw-order-timeline', `${tl.drawOrder.length} draw-order key(s)`, name);
670
- if (tl.events.length > 0 && !ignoreEvents) refusals.note('events', `${tl.events.length} event key(s); a poser that fires none is handed out only on request (ignore: ['events'])`, name);
671
688
 
672
689
  const lastBy = <T extends { name: string }>(targets: readonly T[]): Map<string, T> => new Map(targets.map((t) => [t.name, t]));
673
690
  const bones: RuntimeBoneTimeline[] = [];
@@ -715,5 +732,5 @@ function compileAnimation(
715
732
  constraintTimelines[c.index] = { constraint: c.index, curve: compileCurve(keyed.keys, TRANSFORM_PROPERTIES.length), cursor: cursors++, flags: null };
716
733
  }
717
734
  }
718
- return { name, duration: tl.duration, bones, slots: slotTimelines, constraints: constraintTimelines, cursors, events: tl.events.length };
735
+ return { name, duration: tl.duration, bones, slots: slotTimelines, constraints: constraintTimelines, cursors, events: tl.events.length, eventKeys: Object.freeze(ignoreEvents ? [] : tl.events.map(key => Object.freeze({ name: key.name, time: key.time, int: key.int, float: key.float, string: key.string, volume: key.volume ?? 0, balance: key.balance ?? 0 }))) };
719
736
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The runtime poser's per-instance half (issue #1591, stage 0): one
2
+ * The runtime poser's per-instance half (issue #1591, stage 1): one
3
3
  * `RuntimePose` per skeleton on screen, every buffer allocated when it is
4
4
  * made and overwritten in place on every pose, so that posing a frame
5
5
  * allocates nothing.
@@ -71,6 +71,7 @@ import {
71
71
  W_Y,
72
72
  WORLDS,
73
73
  type RuntimeAnimation,
74
+ type RuntimeEvent,
74
75
  type RuntimeIk,
75
76
  type RuntimeModel,
76
77
  type RuntimeTransform,
@@ -114,6 +115,11 @@ export class RuntimePose {
114
115
  trackTime = 0;
115
116
  time = 0;
116
117
 
118
+ private readonly fired: RuntimeEvent[] = [];
119
+ /** Borrowed immutable event records, in delivery order, until the next apply/step. */
120
+ get events(): readonly RuntimeEvent[] { return this.fired; }
121
+ private eventTime = -1;
122
+
117
123
  private current: RuntimeAnimation | null = null;
118
124
  private readonly cursors: Int32Array;
119
125
  // The constraints' values as posed: an ik's mix, softness and flags, a transform's six mixes.
@@ -160,12 +166,30 @@ export class RuntimePose {
160
166
  this.cursors.fill(-1);
161
167
  this.trackTime = 0;
162
168
  this.time = 0;
169
+ this.eventTime = -1;
170
+ this.fired.length = 0;
171
+ }
172
+
173
+ /**
174
+ * Restore one animation's two clocks without delivering keys already passed.
175
+ * A skin/view replacement uses the old pose's trackTime and wrapped time.
176
+ * Unlike setAnimation + apply, restoration never replays the initial interval.
177
+ */
178
+ restore(name: string, trackTime: number, animationTime: number): void {
179
+ if (!Number.isFinite(trackTime)) throw new RuntimeInputError(`restore: track time ${trackTime} is not finite`);
180
+ if (!Number.isFinite(animationTime)) throw new RuntimeInputError(`restore: animation time ${animationTime} is not finite`);
181
+ this.setAnimation(name);
182
+ this.trackTime = trackTime;
183
+ if (!this.model.restoreEventsFromStart) this.eventTime = animationTime;
184
+ this.apply(animationTime);
163
185
  }
164
186
 
165
187
  /** The setup pose: every constraint applied with its own values, every slot as it is set up. */
166
188
  setupPose(): void {
167
189
  this.current = null;
168
190
  this.time = 0;
191
+ this.eventTime = -1;
192
+ this.fired.length = 0;
169
193
  this.locals.set(this.model.setup);
170
194
  this.hierarchy();
171
195
  this.constraintValues(null, 0);
@@ -178,6 +202,7 @@ export class RuntimePose {
178
202
  const anim = this.current;
179
203
  if (anim === null) throw new RuntimeInputError('apply: no animation is set (setAnimation first)');
180
204
  if (!Number.isFinite(t)) throw new RuntimeInputError(`apply: the time ${t} is not finite`);
205
+ this.collectEvents(anim, t);
181
206
  this.time = t;
182
207
  this.boneTimelines(anim, t);
183
208
  this.hierarchy();
@@ -194,6 +219,21 @@ export class RuntimePose {
194
219
  this.apply(anim.duration === 0 ? 0 : this.trackTime % anim.duration);
195
220
  }
196
221
 
222
+ /** The oracle interval (last,t], tail then head on a wrap; never copies a payload. */
223
+ private collectEvents(anim: RuntimeAnimation, t: number): void {
224
+ const last = this.eventTime;
225
+ const wrapped = t < last;
226
+ const keys = anim.eventKeys;
227
+ let count = 0;
228
+ if (wrapped) for (let i = 0; i < keys.length; i++) if (keys[i].time > last) this.fired[count++] = keys[i];
229
+ for (let i = 0; i < keys.length; i++) {
230
+ const key = keys[i];
231
+ if (key.time <= t && (wrapped ? !this.model.eventWrapOffByOne || key.time > 0 : key.time > last)) this.fired[count++] = key;
232
+ }
233
+ this.fired.length = count;
234
+ this.eventTime = t;
235
+ }
236
+
197
237
  // ---------------------------------------------------------------------------
198
238
  // the bones
199
239
  // ---------------------------------------------------------------------------
package/src/types.ts CHANGED
@@ -1562,7 +1562,33 @@ export interface DroppedState {
1562
1562
  why?: string;
1563
1563
  }
1564
1564
 
1565
+ /** Alpha-derived clipping fit, on the source plate's pixel-centre grid. */
1566
+ export interface AlphaClipReport {
1567
+ target: string;
1568
+ source: string;
1569
+ alpha: number;
1570
+ tolerance: number;
1571
+ maxVertices: number;
1572
+ tracedVertices: number;
1573
+ simplifiedVertices: number;
1574
+ vertices: number;
1575
+ artPixels: number;
1576
+ coveredArt: number;
1577
+ coverage: number;
1578
+ falsePositive: number;
1579
+ falseNegative: number;
1580
+ /** Original trace-corner distance to simplified outline, in drawing pixels, before binding. */
1581
+ maxTraceDeviation: number;
1582
+ /** Maximum setup-world displacement caused by final float32 bind/world rounding, in world units. */
1583
+ maxBindingDeviationWorld: number;
1584
+ overshoot: number;
1585
+ /** source normalized UVs in the emitted clipping vertex order, including carrier crossings. */
1586
+ uvs: Array<[number, number]>;
1587
+ }
1588
+
1565
1589
  export interface CompileResult {
1590
+ /** Present only for generated clipping; does not alter emitted model/skeleton geometry. */
1591
+ alphaClips?: AlphaClipReport[];
1566
1592
  /**
1567
1593
  * The compiled model (`src/model.ts`): what the skeleton below was emitted
1568
1594
  * from. This cut fills its `bones` and `setupWorld`; every other field is