spine-rigc 2.55.0 → 2.57.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/motion.ts CHANGED
@@ -147,10 +147,11 @@ export const MOTION_KEYS = {
147
147
  MotionDrawOrderOffset: ['slot', 'offset'],
148
148
  MotionEventKey: ['t', 'name', 'int', 'float', 'string', 'volume', 'balance'],
149
149
  TrackDeriveTurn: ['kind', 'degrees', 'depth', 'carried', 'about'],
150
- DeformTurn: ['kind', 'radius', 'depth', 'degrees', 'about'],
151
- DeformAffine: ['kind', 'scale', 'about'],
152
- DeformWave: ['kind', 'amplitude', 'wavelength', 'phase', 'along', 'axis'],
153
- DeformBend: ['kind', 'amount', 'from', 'to', 'power', 'along', 'axis'],
150
+ DeformTurn: ['kind', 'radius', 'depth', 'degrees', 'about', 'envelope'],
151
+ DeformAffine: ['kind', 'scale', 'about', 'envelope'],
152
+ DeformWave: ['kind', 'amplitude', 'wavelength', 'phase', 'along', 'axis', 'envelope'],
153
+ DeformBend: ['kind', 'amount', 'from', 'to', 'power', 'along', 'axis', 'envelope'],
154
+ DeformEnvelope: ['axis', 'from', 'to', 'shape', 'mask'],
154
155
  } as const satisfies Record<string, readonly string[]>;
155
156
 
156
157
  /**
@@ -220,10 +221,16 @@ export const MOTION_TYPES = {
220
221
  t: 'number', name: 'string', int: 'number', float: 'number', string: 'string', volume: 'number', balance: 'number',
221
222
  },
222
223
  TrackDeriveTurn: { kind: 'enum', degrees: 'number', depth: 'mixed', carried: 'number', about: 'number' },
223
- DeformTurn: { kind: 'enum', radius: 'number', depth: 'boolean', degrees: 'number', about: 'number' },
224
- DeformAffine: { kind: 'enum', scale: 'number[]', about: 'number[]' },
225
- DeformWave: { kind: 'enum', amplitude: 'number', wavelength: 'number', phase: 'number', along: 'enum', axis: 'enum' },
226
- DeformBend: { kind: 'enum', amount: 'number', from: 'number', to: 'number', power: 'number', along: 'enum', axis: 'enum' },
224
+ DeformTurn: { kind: 'enum', radius: 'number', depth: 'boolean', degrees: 'number', about: 'number', envelope: 'object' },
225
+ DeformAffine: { kind: 'enum', scale: 'number[]', about: 'number[]', envelope: 'object' },
226
+ DeformWave: {
227
+ kind: 'enum', amplitude: 'number', wavelength: 'number', phase: 'number', along: 'enum', axis: 'enum', envelope: 'object',
228
+ },
229
+ DeformBend: {
230
+ kind: 'enum', amount: 'number', from: 'number', to: 'number', power: 'number', along: 'enum', axis: 'enum',
231
+ envelope: 'object',
232
+ },
233
+ DeformEnvelope: { axis: 'number[]', from: 'number', to: 'number', shape: 'enum', mask: 'string' },
227
234
  } as const satisfies MotionTypeTable;
228
235
 
229
236
  /**
@@ -242,6 +249,7 @@ export const MOTION_ENUMS = {
242
249
  DeformAffine: { kind: { owner: 'evaluateDeformTransform' } },
243
250
  DeformWave: { kind: { owner: 'evaluateDeformTransform' }, along: { owner: 'evaluateDeformTransform' }, axis: { owner: 'evaluateDeformTransform' } },
244
251
  DeformBend: { kind: { owner: 'evaluateDeformTransform' }, along: { owner: 'evaluateDeformTransform' }, axis: { owner: 'evaluateDeformTransform' } },
252
+ DeformEnvelope: { shape: { owner: 'evaluateDeformEnvelope' } },
245
253
  } as const satisfies SpecEnumTable<typeof MOTION_TYPES>;
246
254
 
247
255
  /** A deform key's `transform` kinds, and the shape each one's keys come from. */
@@ -587,6 +595,10 @@ function parseDeform(raw: unknown, where: string, at: string): void {
587
595
  if (isObj(parsed.transform)) {
588
596
  const shape = DEFORM_TRANSFORM_SHAPE[String(parsed.transform.kind)];
589
597
  if (shape !== undefined) known(parsed.transform, shape, where, `${at}.transform`);
598
+ // The envelope is one shape under all five kinds (issue #1413); which
599
+ // of its two forms it states, and whether the fields agree, is
600
+ // `evaluateDeformEnvelope`'s to say.
601
+ known(parsed.transform.envelope, 'DeformEnvelope', where, `${at}.transform.envelope`);
590
602
  }
591
603
  }
592
604
  }
@@ -115,6 +115,17 @@ export const SETUP_POSE_DIR = 'setup';
115
115
  * rate writes `<animation>/`, and any other rate writes `<animation>@<fps>fps/`.
116
116
  */
117
117
  export const PROTOCOL_FPS = 12;
118
+
119
+ /**
120
+ * The directory a frame set of `animation` sampled at `fps` is written to under
121
+ * the render root: the animation's own name at the protocol rate, and
122
+ * `<animation>@<fps>fps` at any other — so two rates of one animation sit side by
123
+ * side. `render` and `bench/render_reference.ts` both name their sets through
124
+ * it, and `render --help` and AUTHORING §0 say what it does (issue #1414).
125
+ */
126
+ export function frameSetDir(animation: string, fps: number): string {
127
+ return fps === PROTOCOL_FPS ? animation : `${animation}@${fps}fps`;
128
+ }
118
129
  /** The rate the framing box is measured at, whatever `--fps` writes frames at. */
119
130
  export const FRAMING_FPS = 60;
120
131
 
@@ -222,6 +233,18 @@ export interface FramesSidecar {
222
233
  * it as a reference by name.
223
234
  */
224
235
  environment?: EnvironmentStep[];
236
+ /**
237
+ * The box was given rather than fitted, when `render --viewport` or
238
+ * `render --viewport-from` gave it (issue #1414): which flag, and every slot
239
+ * that reaches past the box, by set and frame — what lies past it is not in
240
+ * the frames, since each frame is the box.
241
+ *
242
+ * ⭐ Absent on a fitted render, for `skin`'s reason: every frame set written
243
+ * before the field existed says the same, and a fitted box is measured over
244
+ * the rig's own poses (every animation at `FRAMING_FPS`), padded, rather than
245
+ * stated by anyone.
246
+ */
247
+ pinned?: PinnedViewport;
225
248
  /** The colour the frames were cleared to, straight RGBA 0..255. */
226
249
  background: RGBA;
227
250
  viewport: {
@@ -238,6 +261,13 @@ export interface FramesSidecar {
238
261
  sets: FrameSet[];
239
262
  }
240
263
 
264
+ /** How a given box was given, and what it leaves out — `FramesSidecar.pinned`. */
265
+ export interface PinnedViewport {
266
+ by: '--viewport' | '--viewport-from';
267
+ /** Every slot whose drawn geometry reaches past the box, per set, in the order the sets were drawn and the slots first reached past it. */
268
+ outside: Array<{ set: string; slot: string; frames: number[] }>;
269
+ }
270
+
241
271
  // ---------------------------------------------------------------------------
242
272
  // posing
243
273
  // ---------------------------------------------------------------------------
package/src/rig.ts CHANGED
@@ -32,6 +32,9 @@
32
32
  * - `invariants` — the structural facts skeleton JSON cannot state about itself,
33
33
  * which the validator's archetype assertions read. Nothing in the file says
34
34
  * "this bone carries the cut's axis" or "this parentage is forbidden".
35
+ * - `stops` — a swinging chain held off a leg, one declaration for the network
36
+ * of bones and constraints it stands for (`RigStop`), expanded into plain
37
+ * bones and constraints before anything else reads the rig.
35
38
  *
36
39
  * ## What a field's PRESENCE means
37
40
  *
@@ -61,6 +64,7 @@
61
64
  */
62
65
  import { ikShapeFault } from './assertions/bodies/a47.ts';
63
66
  import { CompileError, NotImplementedError } from './errors.ts';
67
+ import { expandStops } from './stop.ts';
64
68
  import { dottedPath, refuseNumbersTheFileCannotCarry, refuseUnknownKeys, refuseValuesOfTheWrongType, refuseValuesOutsideTheirSet } from './keys.ts';
65
69
  import type { ShapeVisit, SpecEnumTable, SpecValueType } from './keys.ts';
66
70
 
@@ -1887,6 +1891,69 @@ export interface RigDeformFoldExemption {
1887
1891
  why: string;
1888
1892
  }
1889
1893
 
1894
+ // ---------------------------------------------------------------------------
1895
+ // stops — a swinging chain held off a leg (issue #1413)
1896
+ // ---------------------------------------------------------------------------
1897
+
1898
+ /**
1899
+ * One chain held off a leg: the network [`src/stop.ts`](stop.ts) expands it to,
1900
+ * written once instead of by hand. Spine has no collider and no angle limit, so
1901
+ * a physics-driven skirt chain swings its hem through the thigh it hangs in
1902
+ * front of; this holds the chain's DRAWN links — the bones its mesh is
1903
+ * weighted to — so they may swing away from the leg and not toward it past
1904
+ * their rest angle.
1905
+ *
1906
+ * It expands, before anything else reads the rig, to exactly:
1907
+ *
1908
+ * - `<chain>_touch` — a point bone, child of `against`, at `at`;
1909
+ * - `<chain>_stop` — a frame hung where the first link hangs (its parent,
1910
+ * `x`, `y` and `length`), aimed at the point by the ik constraint
1911
+ * `<chain>_stop_aim`;
1912
+ * - `<link>_rel` — a child of the frame that the transform constraint
1913
+ * `<link>_rel_copy` gives the first link's world rotation;
1914
+ * - `<link>_d` — the first link's drawn link, a child of the frame, which the
1915
+ * transform constraint `<link>_clamp` gives `_rel`'s turn in the frame,
1916
+ * clamped to the range;
1917
+ * - for each link below the first, `<link>_d` — its drawn link, a sibling
1918
+ * with its stated transform — and `<link>_clamp`, which gives it the link's
1919
+ * own turn, clamped to `lower`.
1920
+ *
1921
+ * So a chain's next link hangs from the drawn link above it: author the chain
1922
+ * with each link's parent below `<previous link>_d`, and weight the mesh to the
1923
+ * `_d` bones. What the expansion derives, it derives from the setup pose this
1924
+ * file states (`src/stop.ts` lists it); nothing is measured off the art.
1925
+ */
1926
+ export interface RigStop {
1927
+ /** The name the point, the frame and the aim are called after. */
1928
+ chain: string;
1929
+ /**
1930
+ * The chain's swinging links, root first: each one written by a constraint
1931
+ * in `constraints` (its swing), each below the first hanging from the drawn
1932
+ * link of the one above. Resolved by name.
1933
+ */
1934
+ links: string[];
1935
+ /** The bone the point rides on — the leg. Resolved by name. */
1936
+ against: string;
1937
+ /** The point, `[x, y]` in `against`'s local space. Required: the compiler places no point. */
1938
+ at: [number, number];
1939
+ /**
1940
+ * The first drawn link's range, as its two ends: degrees from its rest angle
1941
+ * to the aim line, counted positive AWAY from the point. `inward: 0,
1942
+ * outward: 60` — never past rest toward the leg, up to 60° away from it.
1943
+ * Both required, `inward` not above `outward`.
1944
+ */
1945
+ inward: number;
1946
+ outward: number;
1947
+ /** Each lower drawn link's range off its own rest bend, the same way round. Required when `links` has more than one. */
1948
+ lower?: RigStopLower;
1949
+ }
1950
+
1951
+ /** A lower link's range — `RigStop.lower`. */
1952
+ export interface RigStopLower {
1953
+ inward: number;
1954
+ outward: number;
1955
+ }
1956
+
1890
1957
  // ---------------------------------------------------------------------------
1891
1958
  // the file
1892
1959
  // ---------------------------------------------------------------------------
@@ -1932,6 +1999,12 @@ export interface RigSpec {
1932
1999
  */
1933
2000
  events?: Record<string, RigEvent>;
1934
2001
  invariants?: RigInvariants;
2002
+ /**
2003
+ * Chains held off a leg — see `RigStop`. Expanded by `parseRigSpec` into
2004
+ * bones and constraints before anything else reads them; kept as written,
2005
+ * and emitted nowhere.
2006
+ */
2007
+ stops?: RigStop[];
1935
2008
  }
1936
2009
 
1937
2010
  // ---------------------------------------------------------------------------
@@ -1960,7 +2033,9 @@ function isObj(v: unknown): v is Record<string, unknown> {
1960
2033
  * fault.
1961
2034
  */
1962
2035
  export const RIG_KEYS = {
1963
- RigSpec: ['spec', 'name', 'note', 'skeleton', 'images', 'bones', 'slots', 'skins', 'constraints', 'events', 'invariants'],
2036
+ RigSpec: ['spec', 'name', 'note', 'skeleton', 'images', 'bones', 'slots', 'skins', 'constraints', 'events', 'invariants', 'stops'],
2037
+ RigStop: ['chain', 'links', 'against', 'at', 'inward', 'outward', 'lower'],
2038
+ RigStopLower: ['inward', 'outward'],
1964
2039
  RigSkeletonHeader: ['x', 'y', 'width', 'height', 'fps', 'referenceScale', 'images', 'audio', 'stageBox'],
1965
2040
  RigStageBox: ['slot', 'attachment'],
1966
2041
  RigBone: ['name', 'parent', 'length', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'shearX', 'shearY', 'inherit', 'skin', 'color', 'icon', 'from'],
@@ -2060,8 +2135,10 @@ type RigTypeTable = {
2060
2135
  export const RIG_TYPES = {
2061
2136
  RigSpec: {
2062
2137
  spec: 'enum', name: 'string', note: 'string', skeleton: 'object', images: 'string', bones: 'object[]', slots: 'object[]',
2063
- skins: 'map of object', constraints: 'object[]', events: 'map of object', invariants: 'object',
2138
+ skins: 'map of object', constraints: 'object[]', events: 'map of object', invariants: 'object', stops: 'object[]',
2064
2139
  },
2140
+ RigStop: { chain: 'string', links: 'string[]', against: 'string', at: 'number[]', inward: 'number', outward: 'number', lower: 'object' },
2141
+ RigStopLower: { inward: 'number', outward: 'number' },
2065
2142
  RigSkeletonHeader: {
2066
2143
  x: 'number', y: 'number', width: 'number | null', height: 'number | null', fps: 'number', referenceScale: 'number',
2067
2144
  images: 'string', audio: 'string | null', stageBox: 'object',
@@ -2405,6 +2482,12 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): Array<{
2405
2482
  at(event, 'RigEvent', `event "${name}"`, ['events', name]);
2406
2483
  }
2407
2484
 
2485
+ for (const [i, stop] of (Array.isArray(raw.stops) ? raw.stops : []).entries()) {
2486
+ const who = isObj(stop) && typeof stop.chain === 'string' ? `stop "${stop.chain}"` : `stops[${i}]`;
2487
+ at(stop, 'RigStop', who, ['stops', i]);
2488
+ if (isObj(stop)) at(stop.lower, 'RigStopLower', `${who}'s "lower"`, ['stops', i, 'lower']);
2489
+ }
2490
+
2408
2491
  if (isObj(raw.invariants)) {
2409
2492
  at(raw.invariants, 'RigInvariants', '"invariants"', ['invariants']);
2410
2493
  for (const [i, rule] of (Array.isArray(raw.invariants.detached) ? raw.invariants.detached : []).entries()) {
@@ -2528,6 +2611,11 @@ function rigPlace(raw: Record<string, unknown>): (path: ReadonlyArray<string | n
2528
2611
  if (head === 'bones') return `${named(raw.bones, second, 'bone', 'bones')}${rest(path.slice(2))}`;
2529
2612
  if (head === 'slots') return `${named(raw.slots, second, 'slot', 'slots')}${rest(path.slice(2))}`;
2530
2613
  if (head === 'constraints') return `${named(raw.constraints, second, 'constraint', 'constraints')}${rest(path.slice(2))}`;
2614
+ if (head === 'stops') {
2615
+ const node = Array.isArray(raw.stops) ? raw.stops[second] : undefined;
2616
+ const who = isObj(node) && typeof node.chain === 'string' ? `stop ${JSON.stringify(node.chain)}` : `stops[${second}]`;
2617
+ return `${who}${rest(path.slice(2))}`;
2618
+ }
2531
2619
  }
2532
2620
  if (head === 'events' && typeof second === 'string') return `event ${JSON.stringify(second)}${rest(path.slice(2))}`;
2533
2621
  if (head === 'skins' && typeof second === 'string') {
@@ -2609,7 +2697,11 @@ export function parseRigSpec(raw: unknown, where: string): RigSpec {
2609
2697
  throw new CompileError(`${where}: a rig spec needs a "slots" array (it may be empty; its ORDER is the draw order)`);
2610
2698
  }
2611
2699
 
2612
- const spec = raw as unknown as RigSpec;
2700
+ // `stops` first, ahead of every name this function resolves: a chain's next
2701
+ // link hangs from the drawn link a stop generates, so the bones below are
2702
+ // read as the expansion lays them out — and every reader after this one sees
2703
+ // ordinary bones and constraints (`src/stop.ts`).
2704
+ const spec = expandStops(raw as unknown as RigSpec, where);
2613
2705
 
2614
2706
  // The stage, stated or stated absent. Half a statement is refused here rather
2615
2707
  // than resolved in `compile`, because which half was meant is not derivable