spine-rigc 0.25.2 → 0.25.3

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
@@ -2006,13 +2006,27 @@ it, on every frame, with the gate green — the second reason to write
2006
2006
  later one. `PS130` in `selftest.ts` poses both models rather than quoting the
2007
2007
  runtime, and [`docs/FACE.md`](FACE.md) §8 is the same rule on a face's two axes.
2008
2008
 
2009
- ⚠️ **And `"additive": true` is not always available.** Only some timelines support
2010
- additive application at all: bone, deform, transform-constraint, path `position`,
2011
- physics `wind`/`gravity`, and a slider's own `mix`. A **slot colour, an attachment
2012
- swap, a draw order or a sequence ignores the flag entirely**, so two sliders
2013
- sharing one of those overwrite each other whatever you write. A40 names that case
2014
- separately, because the fix is different: key such a property from one slider
2015
- only, or move both edits into the single animation one slider applies.
2009
+ ⚠️ **And `"additive": true` is not always available.** The list that supports
2010
+ additive application at all is the closed one: bone, deform, transform-constraint,
2011
+ path `position`, physics `wind`/`gravity`, and a slider's own `mix`. **Everything
2012
+ else ignores the flag** — a slot colour, an attachment swap, a draw order and a
2013
+ sequence, and also an **ik constraint's mix**, a path's `spacing`, and every
2014
+ physics timeline except those two — so two sliders sharing one of those overwrite
2015
+ each other whatever you write. ⚠️ The four spelled out here used to read as the
2016
+ whole of the complement and they are examples of it; `A40` was never reading a
2017
+ list, it reads the runtime's own `Timeline.additive`, which is why it refuses the
2018
+ ik case this sentence did not name.
2019
+
2020
+ ⇒ **And "overwrite each other" has a direction, measured**: the slider **later in
2021
+ the `constraints` array** puts its own animation's value there and the earlier one
2022
+ contributes nothing at all, at every reading of either dial, with both flags set
2023
+ to `true`. Swap the two array entries and the answer swaps with them — it is the
2024
+ array that decides, not the flags and not which animation the file names first
2025
+ (`PS135` in `selftest.ts` poses four such targets both ways; `PS136` poses the
2026
+ three that do compose, and they are the same sum §3.5.2 states, over each target's
2027
+ own setup value). A40 names this case separately, because the fix is different:
2028
+ key such a property from one slider only, or move both edits into the single
2029
+ animation one slider applies.
2016
2030
 
2017
2031
  #### 3.5.2.1 What each `property` can actually be read AS
2018
2032
 
@@ -2424,6 +2438,33 @@ a deform). Folding them in would make `v` mean four different things depending o
2424
2438
  Translate values are **relative to the bone's setup position**; scale values are
2425
2439
  multipliers where `1` is setup; rotation is in degrees.
2426
2440
 
2441
+ ⚠️ **A `slot` track's `property` is one of the two above, and anything else is a
2442
+ compile error** — `animation "A" slot "X" has no timeline "P" (it has:
2443
+ attachment, rgba)`, §5.1's row. Until
2444
+ [#650](https://github.com/firejune/rigc/issues/650) it was not: the emitter had a
2445
+ branch for `attachment` and wrote **everything else** as an rgba timeline under
2446
+ the name you gave it, so a track spelled `sequence` compiled, emitted
2447
+ `slots.X.sequence` with rgba keys, and was refused one stage later by the gate
2448
+ (`A00_ROUNDTRIP_PARSE: threw: Invalid timeline type for a slot`) — while the
2449
+ one-channel spelling of the same mistake was refused at compile as `rgba value
2450
+ needs 4 channels, got 1`, a message about a key you had not written.
2451
+
2452
+ - The pair is the emitter's own dispatch table (`SLOT_TRACKS` in
2453
+ `src/compile.ts`): `compileTrack` reads it to pick its branch, and the refusal
2454
+ prints `Object.keys` of the same object, so what you are told a slot accepts
2455
+ is what it accepts.
2456
+ - **Nothing derives this page's copy of that pair from the table**, and the list
2457
+ is two names long: no `DQ*`/`RD*`/`CUR*` control reads §4.4 (the only gated
2458
+ table on this page is §3.5.2.1's, held by `RD01`–`RD06`). What keeps the two
2459
+ in step is the control that quotes the message — `RF23` in `selftest.ts` —
2460
+ which goes red if the accepted pair ever widens without this page moving with
2461
+ it.
2462
+ - The format has four more slot timelines (`rgb`, `alpha`, `rgba2`, `rgb2`) and
2463
+ rigc emits none of them, so their names are refused here too; `A12_NO_DARK_COLOR`
2464
+ refuses the last two in a file rigc did not write (SPEC_COVERAGE §2.1).
2465
+ `sequence` is a timeline on an **attachment**, not on a slot, and rigc does
2466
+ not emit that either.
2467
+
2427
2468
  **A physics constraint's six tuning timelines override §4.6's table for the
2428
2469
  length of an animation.** `{ "physics": "hair", "property": "wind", "keys": […] }`
2429
2470
  is a wind that rises and falls; `damping` is how fast the jiggle settles,
@@ -4004,6 +4045,8 @@ or the key's position in its own track. These are the frequent ones, verbatim:
4004
4045
  | `skin "S": uses the long form … and also has a key "X"` | §3.4.1 — move the slot inside `attachments` |
4005
4046
  | `animation "A" keys "X" as a path constraint, but the rig declares it as a "slider"` | §4.12 — a timeline group resolves by name AND type; use the field named after the constraint's own type |
4006
4047
  | `animation "A": "position" is a path constraint timeline, and this track names no constraint` | §4.12 — put the name in `"path"` |
4048
+ | `rgba value needs 4 channels, got 3` | §4.4 — an `rgba` key is `[r, g, b, a]`. It names no animation, slot or key time, and the only input that reaches it is a slot `rgba` key: the setup pose's `color` is refused earlier, by its own row, with the slot named |
4049
+ | `animation "A" slot "X" has no timeline "P" (it has: attachment, rgba)` | §4.4 — a slot has exactly two timelines and `P` is neither. Fix the spelling; a bone or constraint property written on a slot track is refused by its own row instead. Before [#650](https://github.com/firejune/rigc/issues/650) every other name compiled as an **rgba** timeline called `P`, and what you saw was `A00_ROUNDTRIP_PARSE` on the emitted file — or, for the one-channel spelling, `rgba value needs 4 channels, got 1` |
4007
4050
  | `N pair(s) of animation names have no one order: … "turn" / "Turn" (case) — they are one name in two cases, and which of them the editor puts first is not measured; rename one of them so they differ by more than letter case` | **R10** — rename until no pair is left. The kind in brackets says which of the editor comparator's four UNMEASURED choices decides the pair: `case` (a pure case tie), `number` (one number written two ways, or a run of digits against a word) or `separator` (make the first character that differs a letter or a digit). rigc keys `animations` in the editor's own comparator — natural and case-insensitive ([#539](https://github.com/firejune/rigc/issues/539), [#543](https://github.com/firejune/rigc/issues/543)) — so a pair that comparator settles is emitted rather than refused, and only the four choices nobody has measured are a compile error; on those, the editor's re-key repoints every slider whose animation moves index ([#535](https://github.com/firejune/rigc/issues/535)) |
4008
4051
  | `N pair(s) of skin names have no one order: … "Zulu" / "mike" (case) — folded to one case "Zulu" and "mike" order the other way round, so whether the editor folds SKIN names decides this pair` | **R11** — rename until no pair is left. The same shape as the row above with a **wider** family: #539 measured the editor's comparator for animation names and thereby ruled codepoint out, and nothing has ruled anything out for skin names, so a pair the candidates could disagree about is refused even where the animation rule would emit it. `Zulu`/`mike` and `mike10`/`mike2` build as animation names and are refused as skin names ([#541](https://github.com/firejune/rigc/issues/541)) |
4009
4052
  | `slot "patch": placeholder "patch" is filled by the "default" skin AND by skins "zulu", "mike", and the Spine editor has no way to hold that … Move the default skin's entry for this slot into a named skin — call it "base"` | **R12** — do what it says: move that entry out of `default` into a named skin. The editor has no representation for a placeholder the default skin shares with a named one, in either spelling, and §3.4.2 has both measurements. Renaming the placeholder does not help; the shape is what is refused |
package/docs/FACE.md CHANGED
@@ -1205,6 +1205,43 @@ green. ⇒ write `"additive": true` on **every** slider that shares a target and
1205
1205
  not only on the later one, which is what the paragraph above already asks for and
1206
1206
  this is the second reason for.
1207
1207
 
1208
+ 🚨 **A second dial on a slot colour, an attachment swap or a draw order is not a
1209
+ second dial at all.** Those timelines ignore `additive`, so the flag is not the
1210
+ repair and writing it on both changes nothing: the slider **later in the
1211
+ `constraints` array** owns that property outright and the earlier one contributes
1212
+ nothing, at every position of its dial. An **ik constraint's mix** behaves the
1213
+ same way. What does compose is the rest of what a face keys — a bone transform, a
1214
+ mesh deform, a transform constraint's mix, a physics `wind` — and those are the
1215
+ same sum as the two axes above, over each target's own setup value. ⇒ if a blink
1216
+ fades a slot and the yaw dial also fades it, one of the two has to stop: key that
1217
+ property from **one** slider, or move both edits into the animation a single
1218
+ slider applies. `A40` refuses the rest by name, and AUTHORING §3.5.2 is the
1219
+ mechanism.
1220
+
1221
+ ⭐ **Three dials are the same sum as two — as long as every one of them is
1222
+ additive.** The case worth knowing is a non-additive dial in the *middle* of
1223
+ three, because it is neither of the two failures you would expect: it erases every
1224
+ dial **before** it and is then added to by every dial **after** it, so the face is
1225
+ neither the sum nor the last dial alone, and no reading of a two-dial rig has that
1226
+ shape.
1227
+
1228
+ ⚠️ **`"loop": true` puts the animation's FIRST frame at the top of the dial.** A
1229
+ looping slider wraps its time as a positive modulo rather than holding the last
1230
+ frame, so the axis is a sawtooth: the two ends of the range are the same pose and
1231
+ every position past the top repeats the range from its bottom. That is right for a
1232
+ parameter that genuinely cycles — a wheel, a breath — and wrong for a yaw, where
1233
+ the range's top has to *stay* at the extreme of the turn. Leave `loop` off for a
1234
+ face axis; the default is the one you want.
1235
+
1236
+ 🔸 **And a `local: false` dial never reads back the number you set.** The world
1237
+ reader goes through the bone's matrix, where the reference runtime's float32 π
1238
+ leaves a fraction of a degree behind, and it has a period: a bone one whole turn
1239
+ from its position reads *identically*, so the dial cannot tell the two apart. The
1240
+ composition is unchanged — two world dials add exactly as two local ones do — but
1241
+ `local: true` is what makes the number on the dial the number the rig reads, which
1242
+ is the same repair §3.5.2's circle already asks for. Every one of the six
1243
+ `property` readings composes by that one arithmetic under `local: true`.
1244
+
1208
1245
  ✅ **The editor half, measured.** This paragraph said *unknown* until the round
1209
1246
  trip was taken with `tools/editor_roundtrip.ts` on a licensed editor (data
1210
1247
  version 4.3.26) against a 4.3.13 build of
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "0.25.2",
3
+ "version": "0.25.3",
4
4
  "description": "Rig compiler for Spine — declarative rig specs in, Spine 4.3 skeleton data out, verified by a spine-core round-trip. Built so AI agents can author rigs and check their own work; the output imports into the Spine editor.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/compile.ts CHANGED
@@ -853,6 +853,38 @@ const SLIDER_TRACKS: Record<string, ValueTrackShape> = {
853
853
  mix: { fields: ['value'], identity: [1] },
854
854
  };
855
855
 
856
+ /**
857
+ * Slot timelines (`animations.<a>.slots.<slot>.<timeline>`): the two
858
+ * `compileTrack` writes, and which key shape each one is written with.
859
+ *
860
+ * ⭐ **The table IS the dispatch.** `compileTrack` reads the shape out of here
861
+ * to pick its branch, and the refusal for a property that is not in it prints
862
+ * `Object.keys` of the same object — so the list an author is given cannot
863
+ * disagree with the list the emitter has, because there is only one.
864
+ *
865
+ * 🚨 It exists because the dispatch used to be one `if` on `attachment` and a
866
+ * fall-through to rgba, which made **every** other property name a legal
867
+ * spelling of an rgba timeline: `{"slot": "x", "property": "sequence", "v": [0,
868
+ * 0, 0, 0]}` compiled and wrote `slots.x.sequence` with rgba-shaped keys. The
869
+ * gate caught the file (`A00_ROUNDTRIP_PARSE: threw: Invalid timeline type for
870
+ * a slot`, and `A05_CURVE_ARRAY_LENGTH`) and the one-channel spelling of the
871
+ * same mistake was refused at compile as *"rgba value needs 4 channels, got
872
+ * 1"* — a message about a key nobody wrote. It is the shape `A21`'s
873
+ * `meshKinds[slot] || 'ring'` had (issue #44): a default that turns "nothing to
874
+ * emit" into an emission of the wrong thing (issue #650).
875
+ *
876
+ * ⚠️ The KEY is the timeline name as the file carries it, and the VALUE names
877
+ * the branch below that writes its keys — so an entry added here without a
878
+ * branch to write it is an entry emitted in some other timeline's shape, which
879
+ * is the defect this table closed rather than a new affordance. The format has
880
+ * four more (`rgb`, `alpha`, `rgba2`, `rgb2`); rigc emits none of them, and
881
+ * `A12_NO_DARK_COLOR` refuses the last two outright.
882
+ */
883
+ export const SLOT_TRACKS: Record<string, 'attachment' | 'rgba'> = {
884
+ attachment: 'attachment',
885
+ rgba: 'rgba',
886
+ };
887
+
856
888
  /**
857
889
  * The three constraint families a `MotionTrack` can target, and the table of
858
890
  * timelines each one accepts.
@@ -6601,6 +6633,16 @@ function compileTrack(
6601
6633
  skinAttachments: Record<string, Record<string, SpineAttachment>>,
6602
6634
  ): SpineTimelineKey[] {
6603
6635
  const where = `animation "${animName}" slot "${target}" ${track.property}`;
6636
+ // Before any key is shaped, and before the empty-track refusal: a property
6637
+ // the emitter has no branch for is the fault, and a track that names one has
6638
+ // no shape to be missing keys from (issue #650).
6639
+ const shape = SLOT_TRACKS[track.property];
6640
+ if (shape === undefined) {
6641
+ throw new CompileError(
6642
+ `animation "${animName}" slot "${target}" has no timeline "${track.property}" ` +
6643
+ `(it has: ${Object.keys(SLOT_TRACKS).join(', ')})`,
6644
+ );
6645
+ }
6604
6646
  if (!track.keys.length) throw new CompileError(`${where}: no keys`);
6605
6647
 
6606
6648
  const out: SpineTimelineKey[] = [];
@@ -6613,7 +6655,7 @@ function compileTrack(
6613
6655
  }
6614
6656
  checkKeyTime(where, time, key.t, duration);
6615
6657
 
6616
- if (track.property === 'attachment') {
6658
+ if (shape === 'attachment') {
6617
6659
  if (key.v !== null && typeof key.v !== 'string') {
6618
6660
  throw new CompileError(`${where}: attachment key value must be a string or null`);
6619
6661
  }
@@ -6626,7 +6668,8 @@ function compileTrack(
6626
6668
  continue;
6627
6669
  }
6628
6670
 
6629
- // rgba
6671
+ // rgba — the other shape `SLOT_TRACKS` names, and now the only way to reach
6672
+ // this branch: a property the table does not carry was refused above.
6630
6673
  if (!Array.isArray(key.v)) throw new CompileError(`${where}: rgba key value must be [r,g,b,a]`);
6631
6674
  const entry: SpineTimelineKey = { time, color: rgbaHex(key.v) };
6632
6675
  if (key.ease !== undefined && key.curve !== undefined) {
package/src/ingest.ts CHANGED
@@ -44,7 +44,7 @@
44
44
  * break `A18_DETERMINISTIC_EMIT` the first time anybody rebuilt from an ingested
45
45
  * spec.
46
46
  */
47
- import { SPINE_VERSION } from './compile.ts';
47
+ import { SLOT_TRACKS as EMITTED_SLOT_TRACKS, SPINE_VERSION } from './compile.ts';
48
48
  import { MOTION_SPEC_VERSION, parseMotionSpec } from './motion.ts';
49
49
  import { parseRigSpec, RIG_KEYS, RIG_SPEC_VERSION, type RigSpec } from './rig.ts';
50
50
  import type { MotionSpec } from './types.ts';
@@ -347,8 +347,18 @@ const HEADER_REDERIVED = ['spine'];
347
347
  /** The attachment types this module inverts. Everything else is refused by name. */
348
348
  const ATTACHMENT_TYPES = ['region', 'mesh', 'boundingbox', 'clipping', 'path'];
349
349
 
350
- /** The two slot timelines the motion spec carries (`compileTrack`'s two branches). */
351
- const SLOT_TRACKS = ['rgba', 'attachment'];
350
+ /**
351
+ * The slot timelines the motion spec carries — `compileTrack`'s own table,
352
+ * rather than a second list of the same two names.
353
+ *
354
+ * ⚠️ It was that second list until issue #650, spelled `['rgba', 'attachment']`
355
+ * with a comment saying where it had been copied from. The copy was true, which
356
+ * is the point: the emitter had no list at all — one `if` and a fall-through —
357
+ * so this module's blocker was the only place in `src/` that said what a slot
358
+ * track may be, and it said it about a compiler that accepted anything. Now
359
+ * there is one list and both sides read it.
360
+ */
361
+ const SLOT_TRACKS = Object.keys(EMITTED_SLOT_TRACKS);
352
362
 
353
363
  /**
354
364
  * Everything this module has a branch for, as the branches themselves state it.