@rig-c/core 2.69.0 → 2.71.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/src/deformgen.ts CHANGED
@@ -121,6 +121,16 @@ export interface DeformTurn {
121
121
  degrees: number;
122
122
  /** Where the axis crosses the driving coordinate. Default 0. */
123
123
  about?: number;
124
+ /**
125
+ * Whether the displacement includes the foreshortening of the vertex's setup
126
+ * position `(u − about)·(cos t − 1)`. Default `true`.
127
+ *
128
+ * When `false`, each vertex moves by `−z·sin t` alone — for a rig whose plate
129
+ * proportions are held at their front-view width across the turn and whose
130
+ * triangles therefore fold at `sin t = A₀ / A_axis` (`asin`) rather than
131
+ * `tan t = A₀ / A_axis` (`atan`) (issue #1584).
132
+ */
133
+ narrowing?: boolean;
124
134
  /** Scales each vertex's offset by a weight in [0, 1] — see `DeformEnvelope`. */
125
135
  envelope?: DeformEnvelope;
126
136
  }
@@ -457,6 +467,7 @@ export function evaluateDeformTransform(
457
467
  const rad = (degrees * Math.PI) / 180;
458
468
  const cosMinus1 = Math.cos(rad) - 1;
459
469
  const sin = Math.sin(rad);
470
+ const narrowing = t.narrowing !== false;
460
471
  // The driving coordinate is the one the axis is perpendicular to: x for a
461
472
  // yaw (a vertical axis), y for a pitch (a horizontal one). The other
462
473
  // component of every pair is 0, because a turn about an axis moves
@@ -481,11 +492,11 @@ export function evaluateDeformTransform(
481
492
  // mesh was built (`sampleContourDepth`) rather than here, where the
482
493
  // sheet is long out of reach.
483
494
  const z = fromDepth ? (depth as readonly number[])[v] : Math.sqrt(radius * radius - u * u);
484
- const d = u * cosMinus1 - z * sin;
495
+ const d = (narrowing ? u * cosMinus1 : 0) - z * sin;
485
496
  offsets[2 * v + along] = round(widen(d));
486
497
  offsets[2 * v + (1 - along)] = 0;
487
498
  }
488
- identity = round(cosMinus1) === 0 && round(sin) === 0;
499
+ identity = (narrowing ? round(cosMinus1) === 0 : true) && round(sin) === 0;
489
500
  identitySpelling = 'degrees 0';
490
501
  sampledTo = fromDepth
491
502
  ? `The turn is ${degrees}°, and a projection of it can only vanish where every vertex shares one ` +
@@ -494,15 +505,20 @@ export function evaluateDeformTransform(
494
505
  : `The turn is ${degrees}°, and a projection of it can only vanish where every vertex shares one ` +
495
506
  `${kind === 'yaw' ? 'x' : 'y'} — check that this attachment's setup geometry is the shape the radius says it is`;
496
507
  const c = kind === 'yaw' ? 'x' : 'y';
508
+ const narrowingNote = narrowing ? '' : ' narrowing=false';
497
509
  stated = fromDepth
498
- ? `depth=true degrees=${degrees}${t.about === undefined ? '' : ` about=${about}`}`
499
- : `radius=${radius} degrees=${degrees}${t.about === undefined ? '' : ` about=${about}`}`;
510
+ ? `depth=true degrees=${degrees}${t.about === undefined ? '' : ` about=${about}`}${narrowingNote}`
511
+ : `radius=${radius} degrees=${degrees}${t.about === undefined ? '' : ` about=${about}`}${narrowingNote}`;
500
512
  formula = fromDepth
501
- ? `d${c} = (${c}−about)·(cos t − 1) − z·sin t, z = the vertex's sampled depth`
502
- : `d${c} = (${c}−about)·(cos t − 1) − z·sin t, z = √(radius² − (${c}−about)²)`;
513
+ ? narrowing
514
+ ? `d${c} = (${c}−about)·(cos t − 1) − z·sin t, z = the vertex's sampled depth`
515
+ : `d${c} = −z·sin t, z = the vertex's sampled depth`
516
+ : narrowing
517
+ ? `d${c} = (${c}−about)·(cos t − 1) − z·sin t, z = √(radius² − (${c}−about)²)`
518
+ : `d${c} = −z·sin t, z = √(radius² − (${c}−about)²)`;
503
519
  derived = [
504
520
  `t = ${round(rad)} rad`,
505
- `cos t − 1 = ${round(cosMinus1)}`,
521
+ ...(narrowing ? [`cos t − 1 = ${round(cosMinus1)}`] : []),
506
522
  `sin t = ${round(sin)}`,
507
523
  ];
508
524
  if (fromDepth) {
package/src/depth.ts CHANGED
@@ -211,8 +211,8 @@ export function depthDigest(map: DepthMap): string {
211
211
  * be a different model. `u` is the point's offset from the axis; `z` is how far
212
212
  * in front of that axis it sits.
213
213
  */
214
- export function turnDisplacement(u: number, z: number, radians: number): number {
215
- return u * (Math.cos(radians) - 1) - z * Math.sin(radians);
214
+ export function turnDisplacement(u: number, z: number, radians: number, narrowing = true): number {
215
+ return (narrowing ? u * (Math.cos(radians) - 1) : 0) - z * Math.sin(radians);
216
216
  }
217
217
 
218
218
  /** What a field comparison measured. Pixels, in the part's own grid. */
@@ -676,10 +676,16 @@ export function turnCeiling(
676
676
  points: ReadonlyArray<readonly [number, number]>,
677
677
  z: readonly number[],
678
678
  triangles: ReadonlyArray<number>,
679
+ options?: { narrowing?: boolean | { yaw?: boolean; pitch?: boolean } },
679
680
  ): TurnCeiling {
680
681
  if (z.length !== points.length) {
681
682
  throw new DepthError(`the mesh has ${points.length} vertices and ${z.length} depths were supplied for it`);
682
683
  }
684
+ const narrowingOpt = options?.narrowing;
685
+ const narrowingByAxis = {
686
+ yaw: typeof narrowingOpt === 'object' ? narrowingOpt.yaw !== false : narrowingOpt !== false,
687
+ pitch: typeof narrowingOpt === 'object' ? narrowingOpt.pitch !== false : narrowingOpt !== false,
688
+ };
683
689
  const out: TurnCeiling = {
684
690
  yaw: { positive: null, negative: null },
685
691
  pitch: { positive: null, negative: null },
@@ -745,10 +751,17 @@ export function turnCeiling(
745
751
  ['pitch', dxb * dzc - dxc * dzb],
746
752
  ] as const) {
747
753
  // A zero here is a triangle the axis cannot fold at all: its area stays
748
- // `A₀·cos t`, which only reaches zero at a right angle.
754
+ // `A₀·cos t` (or `A₀` when `narrowing` is false), which only reaches zero
755
+ // at a right angle or never.
749
756
  if (aAxis === 0) continue;
750
757
  const ratio = a0 / aAxis;
751
- const degrees = (Math.atan(Math.abs(ratio)) * 180) / Math.PI;
758
+ const narrowing = narrowingByAxis[axis];
759
+ // With `narrowing: false`, `2A(t) = 2A₀ − 2A_axis·sin t`, which crosses
760
+ // zero at `sin t = A₀ / A_axis = ratio`. A ratio with `|ratio| >= 1` never
761
+ // crosses zero short of 90° (`|sin t| < 1` on `(0, 90°)`), so it is
762
+ // skipped for the same reason `aAxis === 0` is.
763
+ if (!narrowing && Math.abs(ratio) >= 1) continue;
764
+ const degrees = ((narrowing ? Math.atan(Math.abs(ratio)) : Math.asin(Math.abs(ratio))) * 180) / Math.PI;
752
765
  const side = ratio > 0 ? 'positive' : 'negative';
753
766
  angles[axis][side].push(degrees);
754
767
  const held = out[axis][side];
package/src/motion.ts CHANGED
@@ -146,8 +146,8 @@ export const MOTION_KEYS = {
146
146
  MotionDrawOrderKey: ['t', 'offsets'],
147
147
  MotionDrawOrderOffset: ['slot', 'offset'],
148
148
  MotionEventKey: ['t', 'name', 'int', 'float', 'string', 'volume', 'balance'],
149
- TrackDeriveTurn: ['kind', 'degrees', 'depth', 'carried', 'about'],
150
- DeformTurn: ['kind', 'radius', 'depth', 'degrees', 'about', 'envelope'],
149
+ TrackDeriveTurn: ['kind', 'degrees', 'depth', 'carried', 'about', 'narrowing'],
150
+ DeformTurn: ['kind', 'radius', 'depth', 'degrees', 'about', 'narrowing', 'envelope'],
151
151
  DeformAffine: ['kind', 'scale', 'about', 'envelope'],
152
152
  DeformWave: ['kind', 'amplitude', 'wavelength', 'phase', 'along', 'axis', 'envelope'],
153
153
  DeformBend: ['kind', 'amount', 'from', 'to', 'power', 'along', 'axis', 'envelope'],
@@ -220,8 +220,8 @@ export const MOTION_TYPES = {
220
220
  MotionEventKey: {
221
221
  t: 'number', name: 'string', int: 'number', float: 'number', string: 'string', volume: 'number', balance: 'number',
222
222
  },
223
- TrackDeriveTurn: { kind: 'enum', degrees: 'number', depth: 'mixed', carried: 'number', about: 'number' },
224
- DeformTurn: { kind: 'enum', radius: 'number', depth: 'boolean', degrees: 'number', about: 'number', envelope: 'object' },
223
+ TrackDeriveTurn: { kind: 'enum', degrees: 'number', depth: 'mixed', carried: 'number', about: 'number', narrowing: 'boolean' },
224
+ DeformTurn: { kind: 'enum', radius: 'number', depth: 'boolean', degrees: 'number', about: 'number', narrowing: 'boolean', envelope: 'object' },
225
225
  DeformAffine: { kind: 'enum', scale: 'number[]', about: 'number[]', envelope: 'object' },
226
226
  DeformWave: {
227
227
  kind: 'enum', amplitude: 'number', wavelength: 'number', phase: 'number', along: 'enum', axis: 'enum', envelope: 'object',
@@ -97,6 +97,8 @@ export { choosePosers, pairRefusal, regionKey };
97
97
 
98
98
  /** Opaque, and light: both of rung 3's parts are dark slate, so is every ground. */
99
99
  export const BACKGROUND: RGBA = [232, 232, 232, 255];
100
+ /** Transparent canvas for `render --sheet`, leaving diagnostic `f####.png` and `contact.png` on `BACKGROUND`. */
101
+ export const TRANSPARENT_BACKGROUND: RGBA = [0, 0, 0, 0];
100
102
  /** Padding around the union bounding box, as a fraction of its long side. */
101
103
  export const PAD = 0.04;
102
104
  /**
@@ -221,6 +223,23 @@ export interface FrameSet {
221
223
  plates: number;
222
224
  moving: Array<{ under: string; over: string; frames: number; most: { index: number; px: number } }>;
223
225
  };
226
+ /**
227
+ * The spritesheet export, when `render --sheet` asked for it (issue #1560):
228
+ * the set's manifest file, the table format, the page-edge policy, how many
229
+ * pages were written, how many frames were packed, and how many of them were
230
+ * empty (`1x1` transparent sentinel).
231
+ *
232
+ * ⭐ Absent unless asked for, for `skin`'s reason in `FramesSidecar`: a render
233
+ * without the flag stays the bytes it always was.
234
+ */
235
+ sheet?: {
236
+ manifest: string;
237
+ format: 'json-hash';
238
+ pageEdges: 'pot' | 'free';
239
+ pages: number;
240
+ frames: number;
241
+ emptyFrames: number;
242
+ };
224
243
  }
225
244
 
226
245
  export interface FramesSidecar {
@@ -0,0 +1,106 @@
1
+ /**
2
+ * A keyed timeline compiled once for the runtime poser (issue #1591): its key
3
+ * times and channel values as the oracle stores them (float32, `CoreKey.time`
4
+ * and `.values`), each segment's curve kind, and every Bézier segment's
5
+ * polyline evaluated at load by the oracle's own `bezierPolyline`, so that a
6
+ * pose evaluates the segment the oracle evaluates and nothing else.
7
+ *
8
+ * The two reads a pose makes are the oracle's (`keyIndexAt` and `channelAt`
9
+ * in `src/core/animation.ts`), operation for operation:
10
+ *
11
+ * - **The key at `t`** is the last key whose time is at or before `t`, −1
12
+ * before the first. The oracle scans from key 0 every time; the runtime
13
+ * walks a per-pose cursor from where the previous pose left it, backwards
14
+ * while the key's time is after `t` and forwards while the next one's is
15
+ * not. Over key times that never decrease (the reader refuses a key not
16
+ * after the one before, and float32 can only make two equal), both find the
17
+ * same key for every finite `t`. A `t` that is not finite is refused before
18
+ * any search (`RuntimeInputError`): the scan reads −1 for NaN and the
19
+ * cursor would read wherever it stood.
20
+ * - **A channel's value** on the segment from key `i`: the key's own value on
21
+ * the last key and after a hold; `v0 + ((t − t0) / (t1 − t0)) · (v1 − v0)`
22
+ * on a linear segment; and on a Bézier segment the linear piece of the
23
+ * polyline whose end is the first point at or after `t`.
24
+ *
25
+ * Nothing here allocates once compiled.
26
+ */
27
+ import { bezierPolyline, BEZIER_PIECES, type CoreKey } from '../core/animation.ts';
28
+
29
+ /** A segment's curve, per key. */
30
+ export const CURVE_LINEAR = 0;
31
+ export const CURVE_STEPPED = 1;
32
+ export const CURVE_BEZIER = 2;
33
+
34
+ /** Numbers in one channel's polyline: the segment's two ends and the nine interior points, each a time and a value. */
35
+ export const POLYLINE = 2 * (BEZIER_PIECES + 1);
36
+
37
+ /** One compiled timeline. `values` and `polylines` are by key, then by channel. */
38
+ export interface RuntimeCurve {
39
+ readonly keys: number;
40
+ readonly channels: number;
41
+ readonly times: Float64Array;
42
+ readonly values: Float64Array;
43
+ readonly kinds: Uint8Array;
44
+ /** `POLYLINE` numbers per key and channel; filled only where the key starts a Bézier segment. */
45
+ readonly polylines: Float64Array;
46
+ }
47
+
48
+ /** A timeline's keys compiled; `channels` is how many values each key carries (0 for an attachment timeline). */
49
+ export function compileCurve(keys: readonly CoreKey[], channels: number): RuntimeCurve {
50
+ const n = keys.length;
51
+ const times = new Float64Array(n);
52
+ const values = new Float64Array(n * channels);
53
+ const kinds = new Uint8Array(n);
54
+ const polylines = new Float64Array(n * channels * POLYLINE);
55
+ for (let i = 0; i < n; i++) {
56
+ const a = keys[i];
57
+ times[i] = a.time;
58
+ for (let c = 0; c < channels; c++) values[i * channels + c] = a.values[c];
59
+ const curve = a.curve;
60
+ kinds[i] = curve === 'linear' ? CURVE_LINEAR : curve === 'stepped' ? CURVE_STEPPED : CURVE_BEZIER;
61
+ if (typeof curve === 'string' || i + 1 >= n) continue;
62
+ const b = keys[i + 1];
63
+ for (let c = 0; c < channels; c++) {
64
+ // The oracle's polyline (`channelAt`): the segment's float32 ends around the recurrence over the numbers as the document states them.
65
+ const inner = bezierPolyline(a.stated.time, a.stated.values[c], curve[c * 4], curve[c * 4 + 1], curve[c * 4 + 2], curve[c * 4 + 3], b.stated.time, b.stated.values[c]);
66
+ const o = (i * channels + c) * POLYLINE;
67
+ polylines[o] = a.time;
68
+ polylines[o + 1] = a.values[c];
69
+ for (let q = 0; q < inner.length; q++) polylines[o + 2 + q] = inner[q];
70
+ polylines[o + POLYLINE - 2] = b.time;
71
+ polylines[o + POLYLINE - 1] = b.values[c];
72
+ }
73
+ }
74
+ return { keys: n, channels, times, values, kinds, polylines };
75
+ }
76
+
77
+ /** The last key at or before `t` (−1 before the first), found from `cursors[slot]` and left there — the header's first read. */
78
+ export function keyAt(curve: RuntimeCurve, cursors: Int32Array, slot: number, t: number): number {
79
+ const times = curve.times;
80
+ let i = cursors[slot];
81
+ while (i >= 0 && times[i] > t) i--;
82
+ while (i + 1 < curve.keys && times[i + 1] <= t) i++;
83
+ cursors[slot] = i;
84
+ return i;
85
+ }
86
+
87
+ /** Channel `c` of the segment starting at key `i` (at or after 0), at `t` — the header's second read. */
88
+ export function channelValue(curve: RuntimeCurve, i: number, c: number, t: number): number {
89
+ const ch = curve.channels;
90
+ const values = curve.values;
91
+ const v0 = values[i * ch + c];
92
+ if (i + 1 >= curve.keys) return v0;
93
+ const kind = curve.kinds[i];
94
+ if (kind === CURVE_STEPPED) return v0;
95
+ if (kind === CURVE_LINEAR) {
96
+ const t0 = curve.times[i];
97
+ return v0 + ((t - t0) / (curve.times[i + 1] - t0)) * (values[(i + 1) * ch + c] - v0);
98
+ }
99
+ const p = curve.polylines;
100
+ const o = (i * ch + c) * POLYLINE;
101
+ let q = 2;
102
+ while (q < POLYLINE - 2 && p[o + q] < t) q += 2;
103
+ const x0 = p[o + q - 2];
104
+ const y0 = p[o + q - 1];
105
+ return y0 + ((t - x0) / (p[o + q] - x0)) * (p[o + q + 1] - y0);
106
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * rigc's runtime poser (issue #1591): the model document posed in place, for
3
+ * a player drawing many skeletons a frame — separate from the verification
4
+ * oracle (`src/core/`) and held equal to it at tolerance 0.
5
+ *
6
+ * ## Why there are two posers
7
+ *
8
+ * The oracle is the reference. Each of its poses is a pure function of the
9
+ * document, the skin and the time, returned as a fresh, name-keyed, detached
10
+ * record — the shape a comparison needs, and the shape the core gate holds to
11
+ * spine-core 4.3.13 to the last bit. A player that consumes that record every
12
+ * frame pays for the record: the audit behind issue #1591 measured 175–251
13
+ * KiB allocated per skeleton per frame, against under half a KiB for the
14
+ * official runtime. The runtime poser keeps one pose per skeleton, resolves
15
+ * every name to an index once at load (`compileRuntime`) and overwrites typed
16
+ * arrays in place (`RuntimePose`), so a steady-state frame allocates nothing.
17
+ *
18
+ * It does not re-derive the oracle's rules. Every formula is the oracle's, in
19
+ * the oracle's order of operations (`./pose.ts`'s header), and the gate is the
20
+ * oracle itself: `tools/core_gate.ts`'s third dumper poses every model of the
21
+ * corpus through both at the oracle's samples and compares full doubles at
22
+ * tolerance 0 (*The runtime poser* there). The oracle is not changed for it.
23
+ *
24
+ * ## Stage 0: what it poses, and what it refuses
25
+ *
26
+ * Bones and their five inherit modes; the rotate, translate, scale and shear
27
+ * timelines; a slot's attachment and colour (`rgba`, `rgb`, `alpha`)
28
+ * timelines; region, mesh, weighted mesh and linked mesh vertices; one- and
29
+ * two-bone ik and world-space, absolute transform constraints with their
30
+ * timelines; skin-required bones and constraints under the view's skin. One
31
+ * animation at full weight from the setup pose, at a time or stepped as a
32
+ * looping track. Everything else is refused at load by name with the list of
33
+ * what is missing (`./model.ts`'s header and `RUNTIME_FEATURES`); a consumer
34
+ * falls back to the oracle for a refused model or animation, so nothing breaks
35
+ * while coverage grows.
36
+ *
37
+ * ```ts
38
+ * const model = compileRuntime(underNoSkin(readModel(text)), { ignore: ['events'] });
39
+ * const pose = new RuntimePose(model);
40
+ * pose.setAnimation('walk');
41
+ * pose.step(1 / 60); // or pose.apply(t)
42
+ * pose.computeVertices(); // pose.world, pose.light, pose.shown, pose.vertices
43
+ * ```
44
+ *
45
+ * This module is the `rig-c/runtime` entry (`exports` in package.json), which
46
+ * is **experimental**: not promised until the poser stabilises (RELEASING.md,
47
+ * *The import surface*). Pure, as all of `src/`, linking nothing from the Spine
48
+ * runtime package (`RP10`), and safe to bundle for a browser: its value closure
49
+ * is this directory and `src/core/` (`CUR136`).
50
+ */
51
+ export {
52
+ compileRuntime,
53
+ missingText,
54
+ RuntimeInputError,
55
+ RuntimeRefusal,
56
+ RUNTIME_FEATURES,
57
+ RUNTIME_STAGE,
58
+ DRAW_MESH,
59
+ DRAW_NONE,
60
+ DRAW_REGION,
61
+ DRAW_WEIGHTED,
62
+ LOCAL_FIELD_NAMES,
63
+ LOCALS,
64
+ WORLDS,
65
+ W_A,
66
+ W_B,
67
+ W_C,
68
+ W_D,
69
+ W_X,
70
+ W_Y,
71
+ L_X,
72
+ L_Y,
73
+ L_ROTATION,
74
+ L_SCALE_X,
75
+ L_SCALE_Y,
76
+ L_SHEAR_X,
77
+ L_SHEAR_Y,
78
+ } from './model.ts';
79
+ export type { RuntimeAnimation, RuntimeAttachment, RuntimeFeature, RuntimeMissing, RuntimeModel, RuntimeOptions, RuntimePlant, RuntimeSlot } from './model.ts';
80
+ export { RuntimePose } from './pose.ts';