@rig-c/core 2.72.0 → 2.74.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 1): a skin view of
2
+ * The runtime poser's load-time half (issue #1591, stage 2): 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
@@ -21,13 +21,13 @@
21
21
  * differently, or a block the oracle itself leaves out;
22
22
  * - **refuses an animation** (`RuntimeModel.refused`; `animation(name)`
23
23
  * throws the same `RuntimeRefusal`) when the construct is the animation's
24
- * — an `inherit` timeline, a deform or sequence timeline, a draw-order
24
+ * — an `inherit` timeline, a sequence timeline, a draw-order
25
25
  * timeline, a dark-colour (`rgba2`, `rgb2`) timeline, or an
26
26
  * attachment key showing a clipping attachment or a region with no atlas
27
27
  * rectangle. Every other animation of the model is posed.
28
28
  *
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
29
+ * `RUNTIME_FEATURES` is the closed list of what can be named. Stage 2 adds mesh deform timelines to the
30
+ * named/no-skin views and event keys, with immutable payloads compiled
31
31
  * at load. `ignore: ['events']` remains an explicit diagnostic opt-out; it is
32
32
  * never the default. A merged view with conflicting attachments still refuses
33
33
  * by name, as the oracle itself cannot resolve that view without file order.
@@ -49,10 +49,11 @@ import type { ModelVertices } from '../model.ts';
49
49
  import { activeBones, constraintRecords, CORE_REGION_KINDS, foldInheritMode, readBlend, readColour, shownAttachment, shownRow, sourceOfDoc, underSkin, type CompiledDocument, type CoreAnimation, type CoreBlendMode } from '../core/index.ts';
50
50
  import { constraintInactiveWhy, constraintsAbsentWhy, TRANSFORM_PROPERTIES, type CoreIkRecord, type CoreTransformRecord } from '../core/constraints.ts';
51
51
  import { RUNTIME_PI } from '../core/world.ts';
52
- import { compileCurve, type RuntimeCurve } from './curves.ts';
52
+ import { heldArray, timelineIdentity } from '../core/deform.ts';
53
+ import { compileCurve, compileDeformCurve, type RuntimeCurve } from './curves.ts';
53
54
 
54
55
  /** The stage of issue #1591 this poser implements. */
55
- export const RUNTIME_STAGE = 1;
56
+ export const RUNTIME_STAGE = 2;
56
57
 
57
58
  /** Every construct the runtime poser can refuse, by the name its refusals carry. */
58
59
  export const RUNTIME_FEATURES = [
@@ -68,7 +69,6 @@ export const RUNTIME_FEATURES = [
68
69
  'skin-conflict',
69
70
  'oracle-absent',
70
71
  'inherit-timeline',
71
- 'deform-timeline',
72
72
  'sequence-timeline',
73
73
  'draw-order-timeline',
74
74
  'dark-colour-timeline',
@@ -125,6 +125,9 @@ export interface RuntimePlant {
125
125
  readonly eventWrapOffByOne?: boolean;
126
126
  /** The old restore behavior, which repeats keys up to the restored time. */
127
127
  readonly restoreEventsFromStart?: boolean;
128
+ /** Omit deform / multiply offsets by weights too early: negative controls only. */
129
+ readonly omitDeform?: boolean;
130
+ readonly deformAfterWeights?: boolean;
128
131
  }
129
132
 
130
133
  // A bone's local values, seven per bone.
@@ -184,6 +187,10 @@ export const RAD = RUNTIME_PI / 180;
184
187
  * bindings (bone index, float32 coordinates, float32 weight) in their order.
185
188
  */
186
189
  export interface RuntimeAttachment {
190
+ /** Source timeline identity, including linked-mesh inheritance, resolved once. */
191
+ readonly timeline: string | null;
192
+ readonly deformOffset: number;
193
+ readonly deformCount: number;
187
194
  readonly slot: number;
188
195
  readonly name: string;
189
196
  /** Exact resolved source table identity; display names need not be unique. */
@@ -289,11 +296,21 @@ export interface RuntimeEvent {
289
296
  readonly balance: number;
290
297
  }
291
298
 
299
+ /** One source record's deform, its held arrays by key, matched after attachment switches. */
300
+ export interface RuntimeDeformTimeline {
301
+ readonly identity: string;
302
+ readonly curve: RuntimeCurve;
303
+ readonly cursor: number;
304
+ readonly held: Float64Array;
305
+ readonly count: number;
306
+ }
307
+
292
308
  /** One covered animation, compiled. */
293
309
  export interface RuntimeAnimation {
294
310
  readonly name: string;
295
311
  readonly duration: number;
296
312
  readonly bones: readonly RuntimeBoneTimeline[];
313
+ readonly deforms: readonly RuntimeDeformTimeline[];
297
314
  readonly slots: readonly RuntimeSlotTimeline[];
298
315
  /** Per constraint of the plan, its timeline in this animation or `null`. */
299
316
  readonly constraints: ReadonlyArray<RuntimeConstraintTimeline | null>;
@@ -307,6 +324,9 @@ export interface RuntimeAnimation {
307
324
  /** The compiled model a pose is made of (`compileRuntime`). Read-only once built. */
308
325
  export interface RuntimeModel {
309
326
  /** Control-only boundary defect; fixed at load. */
327
+ readonly omitDeform: boolean;
328
+ readonly deformAfterWeights: boolean;
329
+ readonly deformCount: number;
310
330
  readonly eventWrapOffByOne: boolean;
311
331
  readonly restoreEventsFromStart: boolean;
312
332
  readonly doc: CompiledDocument;
@@ -454,6 +474,7 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
454
474
  // --- slots and the records they show ---
455
475
  const attachments: RuntimeAttachment[] = [];
456
476
  let vertexCount = 0;
477
+ let deformCount = 0;
457
478
  const sourceOf = sourceOfDoc(doc);
458
479
  const attachmentView = plant.attachmentSkin === undefined ? doc : underSkin(doc, plant.attachmentSkin);
459
480
  /** 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. */
@@ -475,7 +496,7 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
475
496
  if ('conflict' in shown) return { index: -1, missing: ['skin-conflict', `slot "${slot.name}" placeholder "${placeholder}" is filled by skins that disagree`] };
476
497
  const row = shownRow(shown);
477
498
  const g = shown.record.geometry;
478
- const at = { slot: si, name: row.name, skin: shown.skin, placeholder: shown.placeholder, path: CORE_REGION_KINDS.has(shown.record.kind) ? row.path : null, bone: indexOf(slot.bone), offset: vertexCount };
499
+ const at = { timeline: timelineIdentity(slot.name, shown), deformOffset: deformCount, slot: si, name: row.name, skin: shown.skin, placeholder: shown.placeholder, path: CORE_REGION_KINDS.has(shown.record.kind) ? row.path : null, bone: indexOf(slot.bone), offset: vertexCount };
479
500
  let made: RuntimeAttachment | null = null;
480
501
  let missing: readonly [RuntimeFeature, string] | null = null;
481
502
  if (g?.kind === 'clipping') missing = ['clipping', `slot "${slot.name}" shows clipping attachment "${row.name}"`];
@@ -519,6 +540,7 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
519
540
  if (made === null) made = vertexAttachment(at, DRAW_NONE, new Float64Array(0), 0);
520
541
  attachments.push(made);
521
542
  vertexCount += made.count;
543
+ deformCount += made.deformCount;
522
544
  return { index: attachments.length - 1, missing };
523
545
  };
524
546
  const slots: RuntimeSlot[] = doc.slots.map((slot, si) => {
@@ -542,7 +564,7 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
542
564
  const refused: Array<{ animation: string; missing: RuntimeMissing[] }> = [];
543
565
  let cursorMax = 0;
544
566
  for (const anim of doc.animations) {
545
- const compiled = compileAnimation(anim, refusals, ignoreEvents, boneIndex, slotIndex, slots, constraints, (si, ph) => resolve(si, ph, anim.name));
567
+ const compiled = compileAnimation(anim, refusals, ignoreEvents, boneIndex, slotIndex, slots, constraints, doc, (si, ph) => resolve(si, ph, anim.name));
546
568
  const missing = refusals.byAnimation.get(anim.name);
547
569
  if (missing !== undefined && missing.length > 0) refused.push({ animation: anim.name, missing });
548
570
  else {
@@ -554,6 +576,9 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
554
576
  if (refusals.model.length > 0) throw new RuntimeRefusal(refusals.model);
555
577
  const names = doc.animations.map((a) => a.name);
556
578
  return {
579
+ omitDeform: plant.omitDeform === true,
580
+ deformAfterWeights: plant.deformAfterWeights === true,
581
+ deformCount,
557
582
  eventWrapOffByOne: plant.eventWrapOffByOne === true,
558
583
  restoreEventsFromStart: plant.restoreEventsFromStart === true,
559
584
  doc,
@@ -583,13 +608,13 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
583
608
  };
584
609
  }
585
610
 
586
- type AttachmentHead = { slot: number; name: string; skin: string; placeholder: string; path: string | null; bone: number; offset: number };
611
+ type AttachmentHead = { timeline: string | null; deformOffset: number; slot: number; name: string; skin: string; placeholder: string; path: string | null; bone: number; offset: number };
587
612
 
588
613
  const NO_INTS = new Int32Array(0);
589
614
  const NO_NUMBERS = new Float64Array(0);
590
615
 
591
616
  function vertexAttachment(at: AttachmentHead, draw: number, local: Float64Array, count: number): RuntimeAttachment {
592
- return { ...at, draw, local, count, bindingCounts: NO_INTS, bindingBones: NO_INTS, bindingX: NO_NUMBERS, bindingY: NO_NUMBERS, bindingWeights: NO_NUMBERS };
617
+ return { ...at, draw, local, count, deformCount: draw === DRAW_MESH ? count : 0, bindingCounts: NO_INTS, bindingBones: NO_INTS, bindingX: NO_NUMBERS, bindingY: NO_NUMBERS, bindingWeights: NO_NUMBERS };
593
618
  }
594
619
 
595
620
  /** A mesh's (or a linked mesh's source's) vertices: every stored number through `Math.fround` (src/core/vertices.ts, *A mesh's vertices*). */
@@ -615,7 +640,7 @@ function meshAttachment(at: AttachmentHead, vertices: ModelVertices, indexOf: (n
615
640
  ws.push(Math.fround(b.weight));
616
641
  }
617
642
  }
618
- return { ...at, draw: DRAW_WEIGHTED, local: NO_NUMBERS, count: counts.length * 2, bindingCounts: Int32Array.from(counts), bindingBones: Int32Array.from(ids), bindingX: Float64Array.from(xs), bindingY: Float64Array.from(ys), bindingWeights: Float64Array.from(ws) };
643
+ return { ...at, deformCount: ids.length * 2, draw: DRAW_WEIGHTED, local: NO_NUMBERS, count: counts.length * 2, bindingCounts: Int32Array.from(counts), bindingBones: Int32Array.from(ids), bindingX: Float64Array.from(xs), bindingY: Float64Array.from(ys), bindingWeights: Float64Array.from(ws) };
619
644
  }
620
645
 
621
646
  function compileIk(r: CoreIkRecord, index: number, applied: boolean, bones: Int32Array, indexOf: (name: string) => number): RuntimeIk {
@@ -675,6 +700,7 @@ function compileAnimation(
675
700
  slotIndex: ReadonlyMap<string, number>,
676
701
  slots: readonly RuntimeSlot[],
677
702
  constraints: readonly RuntimeConstraint[],
703
+ doc: CompiledDocument,
678
704
  resolve: (slot: number, placeholder: string | null) => number,
679
705
  ): RuntimeAnimation {
680
706
  const tl = anim.timelines;
@@ -682,7 +708,7 @@ function compileAnimation(
682
708
  let cursors = 0;
683
709
  const deform = tl.attachments.filter((a) => a.deform !== null);
684
710
  const sequence = tl.attachments.filter((a) => a.sequence !== null);
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);
711
+
686
712
  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);
687
713
  if (tl.drawOrder.length > 0) refusals.note('draw-order-timeline', `${tl.drawOrder.length} draw-order key(s)`, name);
688
714
 
@@ -732,5 +758,12 @@ function compileAnimation(
732
758
  constraintTimelines[c.index] = { constraint: c.index, curve: compileCurve(keyed.keys, TRANSFORM_PROPERTIES.length), cursor: cursors++, flags: null };
733
759
  }
734
760
  }
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 }))) };
761
+ const deforms: RuntimeDeformTimeline[] = deform.map(a => {
762
+ const g = doc.skins.find(s => s.name === a.skin)?.attachments[a.slot]?.[a.attachment]?.geometry;
763
+ if (g === undefined || g.kind === 'region' || g.kind === 'linkedmesh') throw new RuntimeInputError(`deform "${a.skin}/${a.slot}/${a.attachment}" has no vertex geometry (readModel refuses it first)`);
764
+ const keys = a.deform!;
765
+ const arrays = keys.map(k => heldArray(g.vertices, k));
766
+ return { identity: `${a.skin}/${a.slot}/${a.attachment}`, cursor: cursors++, curve: compileDeformCurve(keys), count: arrays[0].length, held: Float64Array.from(arrays.flat()) };
767
+ });
768
+ return { name, duration: tl.duration, deforms, 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 }))) };
736
769
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The runtime poser's per-instance half (issue #1591, stage 1): one
2
+ * The runtime poser's per-instance half (issue #1591, stage 2): 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.
@@ -38,7 +38,7 @@
38
38
  * hierarchy and never posed again by a constraint, nor the bones below it.
39
39
  */
40
40
  import { COLLAPSED_X_AXIS_SQ, RUNTIME_DEG, RUNTIME_PI } from '../core/world.ts';
41
- import { channelValue, keyAt } from './curves.ts';
41
+ import { channelValue, keyAt, deformFraction, CURVE_STEPPED } from './curves.ts';
42
42
  import {
43
43
  DRAW_MESH,
44
44
  DRAW_NONE,
@@ -111,6 +111,9 @@ export class RuntimePose {
111
111
  readonly shown: Int32Array;
112
112
  /** Every attachment's world vertices at its `offset`, written by `computeVertices` for the attachments shown. */
113
113
  readonly vertices: Float64Array;
114
+ /** Held/interpolated deform values, one pair per binding when weighted. */
115
+ readonly deform: Float64Array;
116
+ private readonly deformed: Uint8Array;
114
117
  /** The running sum of `step`'s steps, and the animation time the last pose was posed at. */
115
118
  trackTime = 0;
116
119
  time = 0;
@@ -146,6 +149,8 @@ export class RuntimePose {
146
149
  this.light = new Float64Array(ns * 4);
147
150
  this.shown = new Int32Array(ns);
148
151
  this.vertices = new Float64Array(model.vertexCount);
152
+ this.deform = new Float64Array(model.deformCount);
153
+ this.deformed = new Uint8Array(model.attachments.length);
149
154
  this.cursors = new Int32Array(Math.max(1, model.cursors)).fill(-1);
150
155
  this.ikMix = new Float64Array(nc);
151
156
  this.ikSoftness = new Float64Array(nc);
@@ -195,6 +200,7 @@ export class RuntimePose {
195
200
  this.constraintValues(null, 0);
196
201
  this.solveConstraints();
197
202
  this.slotValues(null, 0);
203
+ this.deformValues(null, 0);
198
204
  }
199
205
 
200
206
  /** The animation posed at its time `t`, from the setup pose (the header). */
@@ -209,6 +215,7 @@ export class RuntimePose {
209
215
  this.constraintValues(anim, t);
210
216
  this.solveConstraints();
211
217
  this.slotValues(anim, t);
218
+ this.deformValues(anim, t);
212
219
  }
213
220
 
214
221
  /** One step of a looping track: the track time moved by `dt`, the animation posed at it wrapped by the duration (`loopedTime` in src/core/raw.ts). */
@@ -940,6 +947,32 @@ export class RuntimePose {
940
947
  }
941
948
  }
942
949
 
950
+ /** Reset on every pose, then match source identity after switches; no old frame's offsets survive. */
951
+ private deformValues(anim: RuntimeAnimation | null, t: number): void {
952
+ this.deformed.fill(0);
953
+ this.deform.fill(0);
954
+ if (anim === null || this.model.omitDeform) return;
955
+ for (let k = 0; k < anim.deforms.length; k++) {
956
+ const tl = anim.deforms[k];
957
+ const i = keyAt(tl.curve, this.cursors, tl.cursor, t);
958
+ if (i < 0) continue;
959
+ const held = i + 1 === tl.curve.keys || tl.curve.kinds[i] === CURVE_STEPPED;
960
+ const p = held ? 0 : deformFraction(tl.curve, i, t);
961
+ const start = i * tl.count;
962
+ for (let s = 0; s < this.shown.length; s++) {
963
+ const index = this.shown[s];
964
+ if (index < 0 || !this.model.slots[s].live) continue;
965
+ const at = this.model.attachments[index];
966
+ if (at.timeline !== tl.identity || at.deformCount === 0) continue;
967
+ this.deformed[index] = 1;
968
+ for (let q = 0; q < tl.count; q++) {
969
+ const v = tl.held[start + q];
970
+ this.deform[at.deformOffset + q] = held ? v : v + (tl.held[start + tl.count + q] - v) * p;
971
+ }
972
+ }
973
+ }
974
+ }
975
+
943
976
  /** Every shown attachment's world vertices into `vertices` at its offset (`regionCorners`, `worldVertices`). */
944
977
  computeVertices(): void {
945
978
  const m = this.model;
@@ -961,7 +994,8 @@ export class RuntimePose {
961
994
  const d = W[b + W_D];
962
995
  const wx = W[b + W_X];
963
996
  const wy = W[b + W_Y];
964
- const local = at.local;
997
+ const local = this.deformed[index] && at.draw === DRAW_MESH ? this.deform : at.local;
998
+ const lo = this.deformed[index] && at.draw === DRAW_MESH ? at.deformOffset : 0;
965
999
  if (at.draw === DRAW_REGION) {
966
1000
  for (let q = 0; q < 8; q += 2) {
967
1001
  const lx = local[q];
@@ -971,8 +1005,8 @@ export class RuntimePose {
971
1005
  }
972
1006
  } else {
973
1007
  for (let q = 0; q < at.count; q += 2) {
974
- const x = local[q];
975
- const y = local[q + 1];
1008
+ const x = local[lo + q];
1009
+ const y = local[lo + q + 1];
976
1010
  out[o++] = x * a + y * bb + wx;
977
1011
  out[o++] = x * c + y * d + wy;
978
1012
  }
@@ -992,9 +1026,16 @@ export class RuntimePose {
992
1026
  const end = n + counts[v];
993
1027
  for (; n < end; n++) {
994
1028
  const b = bones[n] * WORLDS;
995
- const x = xs[n];
996
- const y = ys[n];
997
1029
  const weight = weights[n];
1030
+ // The pair belongs to this influence, before its transform and weight, never to the logical vertex.
1031
+ const deformed = this.deformed[index] !== 0;
1032
+ const x = deformed ? xs[n] + this.deform[at.deformOffset + n * 2] : xs[n];
1033
+ const y = deformed ? ys[n] + this.deform[at.deformOffset + n * 2 + 1] : ys[n];
1034
+ if (deformed && m.deformAfterWeights) {
1035
+ wx += (xs[n] * W[b + W_A] + ys[n] * W[b + W_B] + W[b + W_X]) * weight + this.deform[at.deformOffset + n * 2];
1036
+ wy += (xs[n] * W[b + W_C] + ys[n] * W[b + W_D] + W[b + W_Y]) * weight + this.deform[at.deformOffset + n * 2 + 1];
1037
+ continue;
1038
+ }
998
1039
  wx += (x * W[b + W_A] + y * W[b + W_B] + W[b + W_X]) * weight;
999
1040
  wy += (x * W[b + W_C] + y * W[b + W_D] + W[b + W_Y]) * weight;
1000
1041
  }
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