@rig-c/core 2.73.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.
package/docs/AUTHORING.md CHANGED
@@ -12610,7 +12610,7 @@ verdict on those figures.
12610
12610
  | <a id="CE_COMPILE_ALPHA_CLIP_CONFLICT"></a>`CE_COMPILE_ALPHA_CLIP_CONFLICT` | Names authored fields or convex/inverse flags conflicting with generated inside-clipping geometry. Use one authored/generated form.<br>Exact fixed diagnostic chunks (named values between chunks vary): ⟦: alphaClip conflicts with [⟧<br>⟦vertexCount⟧<br>⟦vertices⟧<br>⟦weights⟧<br>⟦boneIndexing⟧<br>⟦, ⟧<br>⟦], convex=⟧<br>⟦, inverse=⟧<br>⟦; required ordinary inside clipping with generated geometry only. Remove authored geometry/flags.⟧ |
12611
12611
  | <a id="CE_COMPILE_ALPHA_CLIP_DEFORM"></a>`CE_COMPILE_ALPHA_CLIP_DEFORM` | Names animation and source/clip key/frame counts that cannot share the existing projection contract. Give matching explicit supported transform keys in compatible actual frames, with constant world shares where needed, or author the corresponding clip geometry and motion; weighted binding does not copy source deform.<br>Exact fixed diagnostic chunks (named values between chunks vary): ⟦ alphaClip from "⟧<br>⟦"⟧<br>⟦: animation "⟧<br>⟦" has ⟧<br>⟦ source and ⟧<br>⟦ clip keys; source frame=⟧<br>⟦no vertex geometry⟧<br>⟦clip frame=⟧<br>⟦, constant world influence shares=⟧<br>⟦Required matching explicit affine/depth-yaw/pitch transforms in compatible DeformGeometry frames, ⟧<br>⟦with constant shares for world-space transfer. Fix the named frame/keys or author corresponding clip motion.⟧ |
12612
12612
 
12613
- ## 15. Posing in place for a player — the runtime poser (stage 1)
12613
+ ## 15. Posing in place for a player — the runtime poser (stage 2)
12614
12614
 
12615
12615
  rigc poses a model document twice over, on purpose (issue #1591). The **oracle**
12616
12616
  (`src/core/`) is the reference: each pose is a pure function of the document,
@@ -12645,15 +12645,28 @@ exact resolved table identity `(skin, model.slots[attachment.slot].name, placeho
12645
12645
  The display `name` may be shared by different placeholders; it is never a table
12646
12646
  key to guess the source from. These load-time strings do not change the pose path.
12647
12647
 
12648
- | | what stage 1 poses |
12648
+ | | what stage 2 poses |
12649
12649
  | --- | --- |
12650
12650
  | bones | local and world transforms, all five inherit modes, skin-required bones under the view's skin (an inactive bone is zeros, as the oracle poses it) |
12651
12651
  | timelines | `rotate`, `translate(x/y)`, `scale(x/y)`, `shear(x/y)`; a slot's `attachment`, `rgba`, `rgb` and `alpha`; ik and transform timelines; linear, stepped and Bézier keys, each read from a per-pose key cursor |
12652
12652
  | constraints | one- and two-bone ik; world-space, absolute transform constraints; the document's order, each followed by the bones below what it moved posed again |
12653
12653
  | vertices | regions, meshes, weighted meshes and linked meshes, written into the pose's buffer |
12654
+ | deform | weighted and unweighted mesh timelines, linear/stepped/Bézier, per-pose key cursors and in-place buffers; linked meshes inherit their source's timelines only with `timelines: true` |
12654
12655
  | skins and events | named/no-skin views with their skin-required bones and constraints; event delivery in key order over `(last, time]`, tail then head across a wrap |
12655
12656
  | time | one animation at full weight from the setup pose, at a time (`apply`) or stepped as a looping track (`step`, the track time wrapped by the duration as `openTrack` wraps it) |
12656
12657
 
12658
+ Deform keys are expanded once at load. Unweighted keys hold float32 positions,
12659
+ `fround(fround(setup) + fround(offset))`; weighted keys hold float32 offsets,
12660
+ **one x/y pair per influence**, in vertex then influence order. At a pose,
12661
+ interpolation stays double. Each weighted offset is added to its float32 binding
12662
+ coordinate before the bone transform, translation and weight, summed in binding
12663
+ order. Bézier uses the oracle's deform recurrence end `0.99999999` and its
12664
+ product-before-division first and last pieces, rather than ordinary channel easing.
12665
+ Attachment switches resolve before deform matching by source skin/slot/placeholder;
12666
+ display names do not identify timelines. Every pose clears deform state, including
12667
+ before the first key, at setup, on backwards seeks and across loop wraps. Buffers
12668
+ and cursors belong to the pose; compiling another skin resolves another identity.
12669
+
12657
12670
  Skin switching is a load-time view change: compile `underSkin(document, name)`
12658
12671
  (or `underNoSkin(document)`), create the replacement `RuntimePose`, then call
12659
12672
  `replacement.restore(animationName, oldPose.trackTime, oldPose.time)`. Restore
@@ -12681,7 +12694,7 @@ the list; normal compilation never does.
12681
12694
 
12682
12695
  **Everything else is refused at load, by name.** `RuntimeRefusal.missing` lists
12683
12696
  every construct met, each `{ feature, detail, animation }`, and the message
12684
- reads `not posed by the runtime poser (stage 1), N construct(s) missing:
12697
+ reads `not posed by the runtime poser (stage 2), N construct(s) missing:
12685
12698
  <feature>: <detail>; …`. A model-level feature refuses the whole model; an
12686
12699
  animation-level one refuses that animation alone (`model.refused`;
12687
12700
  `model.animation(name)` and `setAnimation(name)` throw its refusal) and every
@@ -12697,7 +12710,7 @@ refused: nothing is posed approximately.
12697
12710
  | `clipping`, `null-atlas-region` | the model when shown at setup, else the animation that keys it | a clipping attachment; a region whose atlas rectangle the model states as `null` |
12698
12711
  | `skin-conflict` | as above | a placeholder the skins fill differently |
12699
12712
  | `oracle-absent` | the model | a pose the oracle itself leaves out (`constraintsAbsentWhy`) |
12700
- | `inherit-timeline`, `deform-timeline`, `sequence-timeline`, `draw-order-timeline`, `dark-colour-timeline` | the animation | a timeline of that kind (`dark-colour` is `rgba2` / `rgb2`) |
12713
+ | `inherit-timeline`, `sequence-timeline`, `draw-order-timeline`, `dark-colour-timeline` | the animation | a timeline of that kind (`dark-colour` is `rgba2` / `rgb2`) |
12701
12714
 
12702
12715
  A call it cannot answer throws `RuntimeInputError`, naming the call:
12703
12716
  `apply: no animation is set (setAnimation first)`, `animation "x" is not one of
@@ -12717,7 +12730,7 @@ the poses and values compared. A `DIFF` or `REFUSED` turns the run RED; a SKIP
12717
12730
  never counts as a pass. The page UVs are the atlas's and are compared by neither
12718
12731
  side here. The selftest's `runtime-poser` suite holds the gate to its plants (a
12719
12732
  1e-6° bone perturbation, a weighted binding bound to the wrong bone index, a
12720
- refused feature smuggled through — each caught by name), a seeded population of
12733
+ omitted deform — each caught by name), a seeded population of
12721
12734
  every construct above, the looping step against `openTrack`, and a steady-state
12722
12735
  frame measured at 0 bytes with the collector off.
12723
12736
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/core",
3
- "version": "2.73.0",
3
+ "version": "2.74.0",
4
4
  "description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,6 +25,7 @@
25
25
  * Nothing here allocates once compiled.
26
26
  */
27
27
  import { bezierPolyline, BEZIER_PIECES, type CoreKey } from '../core/animation.ts';
28
+ import { DEFORM_CURVE_END, type CoreDeformKey } from '../core/deform.ts';
28
29
 
29
30
  /** A segment's curve, per key. */
30
31
  export const CURVE_LINEAR = 0;
@@ -104,3 +105,37 @@ export function channelValue(curve: RuntimeCurve, i: number, c: number, t: numbe
104
105
  const y0 = p[o + q - 1];
105
106
  return y0 + ((t - x0) / (p[o + q] - x0)) * (p[o + q + 1] - y0);
106
107
  }
108
+
109
+ /** Deform easing has one fraction channel, a distinct recurrence end and end-piece arithmetic (core/deform.ts). */
110
+ export function compileDeformCurve(keys: readonly CoreDeformKey[]): RuntimeCurve {
111
+ const curve = compileCurve(keys.map(k => ({ time: k.time, values: [0], curve: 'linear' as const, stated: { time: k.stated, values: [0] } })), 1);
112
+ for (let i = 0; i < keys.length; i++) {
113
+ const a = keys[i];
114
+ curve.kinds[i] = a.curve === 'linear' ? CURVE_LINEAR : a.curve === 'stepped' ? CURVE_STEPPED : CURVE_BEZIER;
115
+ if (typeof a.curve === 'string' || i + 1 === keys.length) continue;
116
+ const b = keys[i + 1];
117
+ const c = a.curve;
118
+ const inner = bezierPolyline(a.stated, 0, c[0], c[1], c[2], c[3], b.stated, DEFORM_CURVE_END);
119
+ const o = i * POLYLINE;
120
+ curve.polylines[o] = a.time;
121
+ curve.polylines[o + 1] = 0;
122
+ curve.polylines.set(inner, o + 2);
123
+ curve.polylines[o + POLYLINE - 2] = b.time;
124
+ curve.polylines[o + POLYLINE - 1] = 1;
125
+ }
126
+ return curve;
127
+ }
128
+
129
+ /** The oracle's deformPercent, with the polyline compiled once and product-before-division at both ends. */
130
+ export function deformFraction(curve: RuntimeCurve, i: number, t: number): number {
131
+ if (curve.kinds[i] === CURVE_STEPPED) return 0;
132
+ if (curve.kinds[i] === CURVE_LINEAR) return (t - curve.times[i]) / (curve.times[i + 1] - curve.times[i]);
133
+ const p = curve.polylines;
134
+ const o = i * POLYLINE;
135
+ let q = 2;
136
+ while (q < POLYLINE - 2 && p[o + q] < t) q += 2;
137
+ const x0 = p[o + q - 2], y0 = p[o + q - 1], x1 = p[o + q], y1 = p[o + q + 1];
138
+ if (q === 2) return (y1 * (t - x0)) / (x1 - x0);
139
+ if (q === POLYLINE - 2) return y0 + ((1 - y0) * (t - x0)) / (x1 - x0);
140
+ return y0 + ((t - x0) / (x1 - x0)) * (y1 - y0);
141
+ }
@@ -21,11 +21,11 @@
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 1: what it poses, and what it refuses
24
+ * ## Stage 2: 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`)
28
- * timelines; region, mesh, weighted mesh and linked mesh vertices; one- and
28
+ * timelines; weighted/unweighted deform timelines; region, mesh, weighted mesh and linked mesh vertices; one- and
29
29
  * two-bone ik and world-space, absolute transform constraints with their
30
30
  * timelines; skin-required bones and constraints under the view's skin. One
31
31
  * animation at full weight from the setup pose, at a time or stepped as a
@@ -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
  }