rig-c 0.0.0-stage → 2.20.4

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.
Files changed (213) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/LICENSE +30 -0
  4. package/NOTICE.md +145 -0
  5. package/README.md +817 -3
  6. package/bin/rigc.cjs +83 -0
  7. package/cli.ts +61 -0
  8. package/cli_core.ts +46 -0
  9. package/docs/AUTHORING.md +9923 -0
  10. package/docs/FACE.md +1948 -0
  11. package/docs/INGEST.md +1488 -0
  12. package/docs/MOTION.md +1241 -0
  13. package/docs/PROMPTING.md +109 -0
  14. package/docs/RIGGING.md +1441 -0
  15. package/docs/SPEC_COVERAGE.md +357 -0
  16. package/package.json +108 -4
  17. package/skills/rigc/SKILL.md +133 -0
  18. package/skills/rigc-face/SKILL.md +60 -0
  19. package/skills/rigc-ingest/SKILL.md +78 -0
  20. package/skills/rigc-motion/SKILL.md +51 -0
  21. package/skills/rigc-rigging/SKILL.md +49 -0
  22. package/src/areaband.ts +159 -0
  23. package/src/assertions/bodies/a01.ts +23 -0
  24. package/src/assertions/bodies/a02.ts +21 -0
  25. package/src/assertions/bodies/a03.ts +27 -0
  26. package/src/assertions/bodies/a04.ts +40 -0
  27. package/src/assertions/bodies/a05.ts +56 -0
  28. package/src/assertions/bodies/a06.ts +245 -0
  29. package/src/assertions/bodies/a07.ts +68 -0
  30. package/src/assertions/bodies/a08.ts +76 -0
  31. package/src/assertions/bodies/a09.ts +82 -0
  32. package/src/assertions/bodies/a10.ts +116 -0
  33. package/src/assertions/bodies/a11.ts +15 -0
  34. package/src/assertions/bodies/a12.ts +30 -0
  35. package/src/assertions/bodies/a13.ts +51 -0
  36. package/src/assertions/bodies/a14.ts +35 -0
  37. package/src/assertions/bodies/a15.ts +97 -0
  38. package/src/assertions/bodies/a16.ts +24 -0
  39. package/src/assertions/bodies/a17.ts +26 -0
  40. package/src/assertions/bodies/a18.ts +62 -0
  41. package/src/assertions/bodies/a19.ts +404 -0
  42. package/src/assertions/bodies/a20.ts +122 -0
  43. package/src/assertions/bodies/a21.ts +190 -0
  44. package/src/assertions/bodies/a22.ts +39 -0
  45. package/src/assertions/bodies/a23.ts +305 -0
  46. package/src/assertions/bodies/a24.ts +68 -0
  47. package/src/assertions/bodies/a25.ts +39 -0
  48. package/src/assertions/bodies/a26.ts +61 -0
  49. package/src/assertions/bodies/a27.ts +33 -0
  50. package/src/assertions/bodies/a28.ts +70 -0
  51. package/src/assertions/bodies/a29.ts +34 -0
  52. package/src/assertions/bodies/a30.ts +50 -0
  53. package/src/assertions/bodies/a31.ts +61 -0
  54. package/src/assertions/bodies/a32.ts +44 -0
  55. package/src/assertions/bodies/a33.ts +110 -0
  56. package/src/assertions/bodies/a34.ts +133 -0
  57. package/src/assertions/bodies/a35.ts +160 -0
  58. package/src/assertions/bodies/a36.ts +81 -0
  59. package/src/assertions/bodies/a37.ts +77 -0
  60. package/src/assertions/bodies/a38.ts +73 -0
  61. package/src/assertions/bodies/a39.ts +303 -0
  62. package/src/assertions/bodies/a40.ts +128 -0
  63. package/src/assertions/bodies/a42.ts +97 -0
  64. package/src/assertions/bodies/a43.ts +181 -0
  65. package/src/assertions/bodies/a44.ts +23 -0
  66. package/src/assertions/bodies/a45.ts +172 -0
  67. package/src/assertions/bodies/a46.ts +224 -0
  68. package/src/assertions/bodies/a47.ts +126 -0
  69. package/src/assertions/bodies/a48.ts +83 -0
  70. package/src/assertions/bodies/a49.ts +81 -0
  71. package/src/assertions/bodies/a50.ts +97 -0
  72. package/src/assertions/constraint_words.ts +169 -0
  73. package/src/assertions/emitted/index.ts +148 -0
  74. package/src/assertions/facts/animated_bones.ts +30 -0
  75. package/src/assertions/facts/animation_durations.ts +37 -0
  76. package/src/assertions/facts/atlas_pages.ts +19 -0
  77. package/src/assertions/facts/atlas_regions.ts +52 -0
  78. package/src/assertions/facts/bone_timelines.ts +37 -0
  79. package/src/assertions/facts/constraint_targets.ts +56 -0
  80. package/src/assertions/facts/constraints.ts +155 -0
  81. package/src/assertions/facts/deform_survey.ts +27 -0
  82. package/src/assertions/facts/event_keys.ts +55 -0
  83. package/src/assertions/facts/linked_meshes.ts +38 -0
  84. package/src/assertions/facts/mesh_attachments.ts +100 -0
  85. package/src/assertions/facts/region_joins.ts +34 -0
  86. package/src/assertions/facts/sequences.ts +85 -0
  87. package/src/assertions/facts/skeleton_roster.ts +45 -0
  88. package/src/assertions/facts/skin_entries.ts +37 -0
  89. package/src/assertions/facts/skin_members.ts +53 -0
  90. package/src/assertions/facts/slider_composition.ts +78 -0
  91. package/src/assertions/facts/slot_colour.ts +43 -0
  92. package/src/assertions/facts/stage.ts +27 -0
  93. package/src/assertions/facts/stage_box.ts +65 -0
  94. package/src/assertions/facts/stepped_poses.ts +74 -0
  95. package/src/assertions/facts/two_colour.ts +52 -0
  96. package/src/assertions/facts/vertex_polygons.ts +53 -0
  97. package/src/assertions/footprints.ts +367 -0
  98. package/src/assertions/harness.ts +109 -0
  99. package/src/assertions/inward_advance.ts +58 -0
  100. package/src/assertions/kinds.ts +105 -0
  101. package/src/assertions/mesh_kinds.ts +56 -0
  102. package/src/assertions/model/animated_bones.ts +38 -0
  103. package/src/assertions/model/animation_durations.ts +57 -0
  104. package/src/assertions/model/atlas_pages.ts +15 -0
  105. package/src/assertions/model/atlas_regions.ts +76 -0
  106. package/src/assertions/model/bone_timelines.ts +58 -0
  107. package/src/assertions/model/constraint_targets.ts +82 -0
  108. package/src/assertions/model/constraints.ts +233 -0
  109. package/src/assertions/model/declared.ts +125 -0
  110. package/src/assertions/model/deform_survey.ts +24 -0
  111. package/src/assertions/model/event_keys.ts +45 -0
  112. package/src/assertions/model/given.ts +45 -0
  113. package/src/assertions/model/index.ts +398 -0
  114. package/src/assertions/model/linked_meshes.ts +24 -0
  115. package/src/assertions/model/mesh_attachments.ts +119 -0
  116. package/src/assertions/model/parse.ts +146 -0
  117. package/src/assertions/model/region_joins.ts +67 -0
  118. package/src/assertions/model/runtime_timelines.ts +78 -0
  119. package/src/assertions/model/sequences.ts +157 -0
  120. package/src/assertions/model/skeleton_roster.ts +23 -0
  121. package/src/assertions/model/skin_entries.ts +69 -0
  122. package/src/assertions/model/skin_members.ts +64 -0
  123. package/src/assertions/model/slider_composition.ts +193 -0
  124. package/src/assertions/model/slot_colour.ts +81 -0
  125. package/src/assertions/model/stage.ts +28 -0
  126. package/src/assertions/model/stage_box.ts +51 -0
  127. package/src/assertions/model/stepped_poses.ts +105 -0
  128. package/src/assertions/model/two_colour.ts +61 -0
  129. package/src/assertions/model/vertex_polygons.ts +72 -0
  130. package/src/assertions/reasons.ts +129 -0
  131. package/src/assertions/region_lookups.ts +61 -0
  132. package/src/assertions/report.ts +189 -0
  133. package/src/assertions/values.ts +39 -0
  134. package/src/atlas.ts +2870 -0
  135. package/src/ballot.ts +866 -0
  136. package/src/bonedist.ts +643 -0
  137. package/src/chainfit.ts +2752 -0
  138. package/src/chains.ts +170 -0
  139. package/src/check.ts +4303 -0
  140. package/src/checkpics.ts +295 -0
  141. package/src/cli/core_commands.ts +1627 -0
  142. package/src/cli/repack.ts +414 -0
  143. package/src/cli/shared.ts +2776 -0
  144. package/src/cli/spine_commands.ts +820 -0
  145. package/src/compile.ts +9414 -0
  146. package/src/core/additive.ts +458 -0
  147. package/src/core/animation.ts +1050 -0
  148. package/src/core/clipping.ts +696 -0
  149. package/src/core/constraints.ts +1876 -0
  150. package/src/core/constraints_path.ts +964 -0
  151. package/src/core/constraints_physics.ts +881 -0
  152. package/src/core/constraints_slider.ts +635 -0
  153. package/src/core/deform.ts +613 -0
  154. package/src/core/draw_order.ts +125 -0
  155. package/src/core/events.ts +135 -0
  156. package/src/core/hooks.ts +249 -0
  157. package/src/core/index.ts +1400 -0
  158. package/src/core/raw.ts +739 -0
  159. package/src/core/skins.ts +129 -0
  160. package/src/core/uvs.ts +469 -0
  161. package/src/core/vertices.ts +490 -0
  162. package/src/core/walk.ts +197 -0
  163. package/src/core/world.ts +289 -0
  164. package/src/correspondence.ts +15 -0
  165. package/src/deformbuild.ts +60 -0
  166. package/src/deformgen.ts +630 -0
  167. package/src/deformmeasure.ts +732 -0
  168. package/src/deformreport.ts +373 -0
  169. package/src/deformstructure.ts +386 -0
  170. package/src/deformsurvey.ts +2162 -0
  171. package/src/depth.ts +784 -0
  172. package/src/diff.ts +2252 -0
  173. package/src/emit.ts +134 -0
  174. package/src/emit_spine.ts +854 -0
  175. package/src/errors.ts +53 -0
  176. package/src/framing.ts +819 -0
  177. package/src/generation.ts +139 -0
  178. package/src/ingest.ts +2293 -0
  179. package/src/json-position.ts +253 -0
  180. package/src/keyorder.ts +587 -0
  181. package/src/keys.ts +486 -0
  182. package/src/ladder.ts +121 -0
  183. package/src/mesh.ts +2382 -0
  184. package/src/meshcompare.ts +1188 -0
  185. package/src/meshquality.ts +2042 -0
  186. package/src/meshrasters.ts +944 -0
  187. package/src/meshreduce.ts +1425 -0
  188. package/src/model.ts +1245 -0
  189. package/src/motion.ts +809 -0
  190. package/src/nonfinite.ts +54 -0
  191. package/src/package_meta.ts +48 -0
  192. package/src/png.ts +297 -0
  193. package/src/pose.ts +2324 -0
  194. package/src/preview.ts +434 -0
  195. package/src/region_joins.ts +54 -0
  196. package/src/render.ts +1013 -0
  197. package/src/render_core.ts +871 -0
  198. package/src/render_shared.ts +2958 -0
  199. package/src/repack.ts +495 -0
  200. package/src/rig.ts +2941 -0
  201. package/src/slots.ts +892 -0
  202. package/src/spine_side.ts +138 -0
  203. package/src/timelines.ts +837 -0
  204. package/src/trackgen.ts +364 -0
  205. package/src/transform.ts +310 -0
  206. package/src/types.ts +1797 -0
  207. package/src/validate.ts +3875 -0
  208. package/tools/contact.ts +126 -0
  209. package/tools/editor_roundtrip.ts +1641 -0
  210. package/tools/font5x7.ts +101 -0
  211. package/tools/measure_contact_depth.ts +105 -0
  212. package/tools/plate.ts +508 -0
  213. package/tools/png_probe.mjs +72 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * A35, restated over the emitted text (issue #1060): the clause
3
+ * `validate()` runs in `src/validate.ts`, word for word, over the skeleton JSON — the
4
+ * text rigc wrote, read by rigc's own reader rather than by spine-core's. The
5
+ * round trip's copy is unchanged and runs in `cli.ts build`; this one runs in
6
+ * the gate of the entry that links none of the runtime
7
+ * (`../emitted/index.ts`), and the selftest's `RC28` holds the two to the
8
+ * same lines on every recipe and on this assertion's mutants.
9
+ */
10
+ import type { Verdicts } from '../harness.ts';
11
+ import { isObj, type Json } from '../values.ts';
12
+
13
+ /**
14
+ * How long the array a deform key edits is, read off one raw attachment — or
15
+ * `null` when the file does not say.
16
+ *
17
+ * The rule is `readVertices`' own: the attachment's `vertices` is coordinates
18
+ * when its length equals `worldVerticesLength`, and a weight run otherwise. A
19
+ * deform array is therefore one `x, y` pair per **vertex** in the first case and
20
+ * one per **bone influence** (`vertices.length / 3`) in the second — the same
21
+ * count with two different meanings, which is exactly why this measures rather
22
+ * than assumes.
23
+ *
24
+ * `null` for the two shapes that cannot be measured from this object alone: a
25
+ * type with no vertices at all (nothing to deform, and the parser throws on it),
26
+ * and a `linkedmesh`, whose geometry belongs to another attachment.
27
+ */
28
+ function deformArrayLength(att: Json): number | null {
29
+ const type = typeof att.type === 'string' ? att.type : 'region';
30
+ let worldVerticesLength: number;
31
+ if (type === 'mesh') {
32
+ if (!Array.isArray(att.uvs)) return null;
33
+ worldVerticesLength = att.uvs.length;
34
+ } else if (type === 'boundingbox' || type === 'clipping' || type === 'path') {
35
+ if (typeof att.vertexCount !== 'number') return null;
36
+ worldVerticesLength = att.vertexCount * 2;
37
+ } else {
38
+ return null;
39
+ }
40
+ if (!Array.isArray(att.vertices)) return null;
41
+ const vertices = att.vertices as unknown[];
42
+ if (vertices.length === worldVerticesLength) return worldVerticesLength;
43
+ // ⚠️ Weighted, and the length is the INFLUENCE COUNT — which is the sum of the
44
+ // per-vertex bone counts, not a division of this array's length. The file
45
+ // holds `boneCount` followed by `boneIndex, x, y, weight` per influence, so a
46
+ // one-bone vertex is FIVE numbers; `readVertices` unpacks that into three
47
+ // numbers per influence, which is where the parser's own `/3*2` comes from and
48
+ // exactly why it cannot be applied to the raw form. Applied here it measured
49
+ // `gallery/flex`'s 77-vertex leaf at 256.667 against its true 154 and put
50
+ // A35's bar two thirds too wide on every weighted mesh — the silence A35
51
+ // exists to break, arriving inside A35.
52
+ //
53
+ // Derived here rather than shared with `src/compile.ts`'s own walk on purpose:
54
+ // the gate re-derives from the emitted file so that it is not checking the
55
+ // compiler's assumptions with the compiler's code. Both had this wrong, which
56
+ // is an argument for a control on each and not for one implementation.
57
+ let influences = 0;
58
+ for (let i = 0; i < vertices.length; ) {
59
+ const n = vertices[i++];
60
+ if (typeof n !== 'number' || !Number.isInteger(n) || n < 1) return null;
61
+ influences += n;
62
+ i += n * 4;
63
+ if (i > vertices.length) return null;
64
+ }
65
+ return influences * 2;
66
+ }
67
+
68
+ export function a35DeformKeysFitTheAttachment({ fail, skip }: Verdicts, raw: Json | null): void {
69
+ if (!raw) return skip('A35_DEFORM_KEYS_FIT_THE_ATTACHMENT', 'the skeleton JSON did not parse (A00 owns that failure)');
70
+ if (!isObj(raw.animations)) return skip('A35_DEFORM_KEYS_FIT_THE_ATTACHMENT', 'the skeleton declares no animations');
71
+ /** skin name -> slot -> attachment, straight off the raw JSON. */
72
+ const skins = new Map<string, Json>();
73
+ for (const skin of Array.isArray(raw.skins) ? (raw.skins as unknown[]) : []) {
74
+ if (isObj(skin) && typeof skin.name === 'string' && isObj(skin.attachments)) {
75
+ skins.set(skin.name, skin.attachments as Json);
76
+ }
77
+ }
78
+ let sawATimeline = false;
79
+ let measured = 0;
80
+ for (const [animName, anim] of Object.entries(raw.animations as Json)) {
81
+ if (!isObj(anim) || !isObj(anim.attachments)) continue;
82
+ for (const [skinName, slotMap] of Object.entries(anim.attachments as Json)) {
83
+ if (!isObj(slotMap)) continue;
84
+ for (const [slotName, attMap] of Object.entries(slotMap)) {
85
+ if (!isObj(attMap)) continue;
86
+ for (const [attName, timelines] of Object.entries(attMap)) {
87
+ if (!isObj(timelines) || !Array.isArray(timelines.deform)) continue;
88
+ sawATimeline = true;
89
+ const at = `animation "${animName}" deform ${skinName}/${slotName}/${attName}`;
90
+ const attachment = isObj(skins.get(skinName)?.[slotName])
91
+ ? ((skins.get(skinName)![slotName] as Json)[attName] as unknown)
92
+ : undefined;
93
+ if (!isObj(attachment)) {
94
+ fail(
95
+ 'A35_DEFORM_KEYS_FIT_THE_ATTACHMENT',
96
+ `${at}: skin "${skinName}" has no attachment "${attName}" on slot "${slotName}"; the parser throws ` +
97
+ '`Timeline attachment not found`',
98
+ );
99
+ continue;
100
+ }
101
+ const length = deformArrayLength(attachment);
102
+ if (length === null) {
103
+ // A linked mesh takes its geometry from another attachment, so the
104
+ // raw file cannot state this one's length. Saying nothing beats
105
+ // inventing a bound: an assertion with a default is how "nothing to
106
+ // measure" becomes a measurement of the wrong thing.
107
+ continue;
108
+ }
109
+ measured++;
110
+ const keys = timelines.deform as unknown[];
111
+ if (keys.length === 0) {
112
+ fail('A35_DEFORM_KEYS_FIT_THE_ATTACHMENT', `${at}: the key array is empty; the parser skips the timeline in silence`);
113
+ continue;
114
+ }
115
+ keys.forEach((key, k) => {
116
+ if (!isObj(key)) {
117
+ fail('A35_DEFORM_KEYS_FIT_THE_ATTACHMENT', `${at} key ${k}: not an object`);
118
+ return;
119
+ }
120
+ const vertices = key.vertices;
121
+ if (vertices === undefined || vertices === null) return; // "back to setup" — nothing to fit
122
+ if (!Array.isArray(vertices)) {
123
+ fail('A35_DEFORM_KEYS_FIT_THE_ATTACHMENT', `${at} key ${k}: vertices is ${JSON.stringify(vertices)}, not an array`);
124
+ return;
125
+ }
126
+ const offset = key.offset === undefined ? 0 : key.offset;
127
+ if (typeof offset !== 'number' || !Number.isInteger(offset) || offset < 0) {
128
+ fail('A35_DEFORM_KEYS_FIT_THE_ATTACHMENT', `${at} key ${k}: offset is ${JSON.stringify(key.offset)}`);
129
+ return;
130
+ }
131
+ // ⛔ No parity clause here, and the header says why: an odd `offset`
132
+ // and an odd-length run are both what a trimmed editor export looks
133
+ // like, and the parser has no pair arithmetic to be misaligned
134
+ // against (#262).
135
+ if (offset + vertices.length > length) {
136
+ fail(
137
+ 'A35_DEFORM_KEYS_FIT_THE_ATTACHMENT',
138
+ `${at} key ${k}: the run covers ${offset}..${offset + vertices.length} of a ${length}-long deform ` +
139
+ 'array; everything past the end is copied into a Float32Array and dropped without a word',
140
+ );
141
+ }
142
+ for (const n of vertices as unknown[]) {
143
+ if (typeof n !== 'number' || !Number.isFinite(n)) {
144
+ fail('A35_DEFORM_KEYS_FIT_THE_ATTACHMENT', `${at} key ${k}: the run holds a non-finite value ${JSON.stringify(n)}`);
145
+ break;
146
+ }
147
+ }
148
+ });
149
+ }
150
+ }
151
+ }
152
+ }
153
+ if (!sawATimeline) return skip('A35_DEFORM_KEYS_FIT_THE_ATTACHMENT', 'no animation carries a deform timeline');
154
+ if (measured === 0) {
155
+ return skip(
156
+ 'A35_DEFORM_KEYS_FIT_THE_ATTACHMENT',
157
+ 'every deform timeline here keys an attachment whose vertex count the raw file does not state (a linked mesh)',
158
+ );
159
+ }
160
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * A36, the body (issue #1025, step 4c of #380): a path constraint that
3
+ * follows nothing, quietly.
4
+ *
5
+ * 🚨 The first failure here is the quietest in the whole constraint half of
6
+ * the format. `PathConstraint.update` opens with
7
+ *
8
+ * const attachment = this.slot.appliedPose.attachment;
9
+ * if (!(attachment instanceof PathAttachment)) return;
10
+ *
11
+ * so a path constraint aimed at a slot that never shows a path loads
12
+ * perfectly, reports every mix it was given, appears in the update cache —
13
+ * and moves nothing, forever. Nothing in the file is wrong on its face: the
14
+ * slot exists, the constraint resolves, the mixes are 1.
15
+ *
16
+ * The rest are the same shape as A23's: a constraint that parses and does
17
+ * nothing. All three mixes at 0 is only a finding when no animation keys one
18
+ * of them above 0 (`switchedOn` over the `mix` timelines, judged by
19
+ * `mixLive`), and a chain with no bones on it is one whether or not anything
20
+ * is keyed.
21
+ *
22
+ * Moved out of `src/validate.ts` whole: the bones, the slot and the skins'
23
+ * paths, the setup mixes and the keyed ones are all the rig's, and a
24
+ * document's records state each.
25
+ */
26
+ import type { Verdicts } from '../harness.ts';
27
+ import type { ConstraintFacts } from '../facts/constraints.ts';
28
+ import { mixLive, noneKeysItsMixAbove0, switchedOn } from '../constraint_words.ts';
29
+
30
+ export function a36PathConstraintEffective({ fail, skip, stats }: Verdicts, facts: ConstraintFacts): void {
31
+ const constraints = facts.constraints.flatMap((c, index) => (c.path === undefined ? [] : [{ name: c.name, path: c.path, index }]));
32
+ if (!constraints.length) return skip('A36_PATH_CONSTRAINT_EFFECTIVE', 'the skeleton declares no path constraint');
33
+ /**
34
+ * Which path constraints an animation switches ON — `switchedOn` over their
35
+ * `mix` timelines, judged by `mixLive`, the predicate their setup pose is
36
+ * judged by below.
37
+ *
38
+ * ⭐ The reason this is needed: a constraint whose mixes are all 0 at setup
39
+ * is **the idiom**, not a defect — spineboy's aim rig is exactly that, and
40
+ * issue #88 landed the timelines that turn one on. So "muted" is only a
41
+ * finding when nothing turns it on, and that question lives in the
42
+ * animations rather than in the constraint.
43
+ *
44
+ * 🔑 "Turns it on" is a VALUE question, and until issue #752 this asked a
45
+ * weaker one than `A23` — whether a `mix` key array was non-empty — so a
46
+ * timeline keying 0 only was a rescue for a constraint it leaves exactly as
47
+ * muted as no timeline does. Now a path constraint is switched on by a key
48
+ * posing any of its three mixes above 0, which is the runtime's own
49
+ * condition: `PathConstraint.update` returns when `mixRotate`, `mixX` and
50
+ * `mixY` are all 0 (`PathConstraint.js:73-75`).
51
+ */
52
+ const pathSwitchedOn = switchedOn(facts, (timeline) => timeline.kind === 'path' && timeline.word === 'mix', 3, (_timeline, value) => mixLive(value));
53
+ /** The slots some skin gives a path attachment. */
54
+ const pathsBySlot = new Set(facts.pathSlots);
55
+ for (const { name, path: constraint, index } of constraints) {
56
+ const where = `path constraint "${name}"`;
57
+ if (!constraint.bones.length) {
58
+ fail('A36_PATH_CONSTRAINT_EFFECTIVE', `${where} constrains no bone; it parses and does nothing`);
59
+ }
60
+ const slot = constraint.slot;
61
+ if (!pathsBySlot.has(slot)) {
62
+ fail(
63
+ 'A36_PATH_CONSTRAINT_EFFECTIVE',
64
+ `${where} follows slot "${slot}", and no skin gives that slot a path attachment — ` +
65
+ "PathConstraint.update returns immediately unless the slot's attachment is a path, so this " +
66
+ 'constraint reports its mixes and moves nothing',
67
+ );
68
+ }
69
+ const pose = constraint.setup;
70
+ const muted = ![pose.mixRotate, pose.mixX, pose.mixY].some(mixLive);
71
+ if (muted && !pathSwitchedOn.has(index)) {
72
+ fail(
73
+ 'A36_PATH_CONSTRAINT_EFFECTIVE',
74
+ `${where} has mixRotate ${pose.mixRotate}, mixX ${pose.mixX} and mixY ${pose.mixY} at setup and ` +
75
+ `${noneKeysItsMixAbove0(facts.animations)}; update() returns on all-zero mixes, so nothing ever ` +
76
+ 'puts a bone on the path — rest one of the three above 0, or key its mix above 0 in an animation',
77
+ );
78
+ }
79
+ }
80
+ stats.pathConstraints = constraints.length;
81
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * A37, the body (issue #1025, step 4c of #380): a slider that applies
3
+ * nothing, quietly.
4
+ *
5
+ * A slider is the only constraint that applies an ANIMATION, so its failure
6
+ * modes are about that animation rather than about a transform:
7
+ *
8
+ * 1. **An animation with no timelines.** `animation.apply` walks an empty
9
+ * array. The slider is in the update cache, its time moves, and the
10
+ * skeleton never changes.
11
+ * 2. **`loop` on a zero-length animation.** `Slider.update` computes
12
+ * `animation.duration + (p.time % animation.duration)` when looping, so
13
+ * a duration of 0 makes the time **NaN** — and it then applies the
14
+ * animation at NaN, which is a pose nobody can predict and no error.
15
+ * Only reachable with a bone, because that is the branch the loop
16
+ * arithmetic lives in.
17
+ * 3. **`scale` 0 with a bone.** `time = offset + (value - offset) * 0`, so
18
+ * the dial turns and the slider holds one frame.
19
+ * 4. **`mix` 0** with nothing keying it — the same rule as A36's.
20
+ *
21
+ * Moved out of `src/validate.ts` whole: the animation it applies (its
22
+ * timeline count and duration), its dial, `loop`, `scale` and mix, and the
23
+ * keys that raise its mix are the rig's, and a document's records state each.
24
+ * ⚠️ `scale` is read behind `bone` only, and that is where the one known
25
+ * disagreement between the suppliers lives: on a bone-less slider spine-core
26
+ * leaves `scale` at 0 and the document states 1 (#1027). Clause 3 never reads
27
+ * a bone-less slider's scale, so no line can differ on it.
28
+ */
29
+ import type { Verdicts } from '../harness.ts';
30
+ import type { ConstraintFacts } from '../facts/constraints.ts';
31
+ import { mixLive, noneKeysItsMixAbove0, REST_OR_KEY_ITS_MIX, switchedOn } from '../constraint_words.ts';
32
+
33
+ export function a37SliderConstraintEffective({ fail, skip, stats }: Verdicts, facts: ConstraintFacts): void {
34
+ const sliders = facts.constraints.flatMap((c, index) => (c.slider === undefined ? [] : [{ name: c.name, slider: c.slider, index }]));
35
+ if (!sliders.length) return skip('A37_SLIDER_CONSTRAINT_EFFECTIVE', 'the skeleton declares no slider constraint');
36
+ /** Which sliders an animation switches ON — `switchedOn` over their `mix` timelines, judged by `mixLive` (A36's ⭐ and 🔑; `Slider.update` returns when `mix` is 0, `Slider.js:53-54`). */
37
+ const sliderSwitchedOn = switchedOn(facts, (timeline) => timeline.kind === 'slider' && timeline.word === 'mix', 1, (_timeline, value) => mixLive(value));
38
+ for (const { name, slider, index } of sliders) {
39
+ const where = `slider "${name}"`;
40
+ const animation = slider.animation;
41
+ if (!animation) {
42
+ // The parser's second pass throws on a miss, so this is only reachable
43
+ // on an artifact that never went through it.
44
+ fail('A37_SLIDER_CONSTRAINT_EFFECTIVE', `${where} applies no animation`);
45
+ continue;
46
+ }
47
+ if (animation.timelines === 0) {
48
+ fail(
49
+ 'A37_SLIDER_CONSTRAINT_EFFECTIVE',
50
+ `${where} applies animation "${animation.name}", which carries no timeline at all; the slider runs and ` +
51
+ 'the skeleton never changes',
52
+ );
53
+ }
54
+ if (slider.bone && slider.loop && !(animation.duration > 0)) {
55
+ fail(
56
+ 'A37_SLIDER_CONSTRAINT_EFFECTIVE',
57
+ `${where} loops animation "${animation.name}", whose duration is ${animation.duration} — ` +
58
+ 'Slider.update computes `duration + (time % duration)` when looping, so the applied time is NaN',
59
+ );
60
+ }
61
+ if (slider.bone && slider.scale === 0) {
62
+ fail(
63
+ 'A37_SLIDER_CONSTRAINT_EFFECTIVE',
64
+ `${where} drives off bone "${slider.bone}" with scale 0, so the property cannot move the slider's time`,
65
+ );
66
+ }
67
+ const mix = slider.mix;
68
+ if (!mixLive(mix) && !sliderSwitchedOn.has(index)) {
69
+ fail(
70
+ 'A37_SLIDER_CONSTRAINT_EFFECTIVE',
71
+ `${where} has mix ${mix} at setup and ${noneKeysItsMixAbove0(facts.animations)}; update() returns ` +
72
+ `on mix 0 — ${REST_OR_KEY_ITS_MIX}`,
73
+ );
74
+ }
75
+ }
76
+ stats.sliderConstraints = sliders.length;
77
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * A38, the body (issue #1025, step 4c of #380): a skin's member list and its
3
+ * members' `skin: true` flag agree.
4
+ *
5
+ * Moved out of `src/validate.ts` unchanged but for what it reads: the bones,
6
+ * the constraints and the skins' lists are a fact (`../facts/skin_members.ts`)
7
+ * — which spine-core's loaded `SkeletonData` satisfies as it stands. Why both
8
+ * halves of the switch are dead data alone stays above the `check` call in
9
+ * `validate()`.
10
+ */
11
+ import type { Verdicts } from '../harness.ts';
12
+ import type { MemberBone, SkinMemberFacts } from '../facts/skin_members.ts';
13
+
14
+ export function a38SkinMembersAreSkinRequired({ fail, skip, stats }: Verdicts, data: SkinMemberFacts): void {
15
+ /** Every bone a skin can switch on, including the ancestors it drags in. */
16
+ const activatable = new Set<string>();
17
+ // The constraint OBJECTS a skin lists, never their names: a name is not a
18
+ // constraint in this format, and two constraints of one name under two
19
+ // kinds are two objects a skin may list separately (issue #692). Keyed by
20
+ // name, a `transform` `leg` that no skin lists read as listed because an
21
+ // `ik` `leg` was — a skinRequired constraint that never runs, reported
22
+ // green by the one assertion that looks for exactly that.
23
+ const listedConstraints = new Set(data.skins.flatMap((skin) => skin.constraints));
24
+ let listed = 0;
25
+ for (const skin of data.skins) {
26
+ for (const bone of skin.bones) {
27
+ listed++;
28
+ for (let cursor: MemberBone | null = bone; cursor; cursor = cursor.parent) activatable.add(cursor.name);
29
+ if (!bone.skinRequired) {
30
+ fail(
31
+ 'A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED',
32
+ `skin "${skin.name}" activates bone "${bone.name}", which is not skinRequired — updateCache starts it ` +
33
+ 'active anyway, so it poses under every skin and this list changes nothing',
34
+ );
35
+ }
36
+ }
37
+ for (const constraint of skin.constraints) {
38
+ listed++;
39
+ if (!constraint.skinRequired) {
40
+ fail(
41
+ 'A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED',
42
+ `skin "${skin.name}" activates constraint "${constraint.name}", which is not skinRequired — it runs ` +
43
+ 'under every skin, so this list changes nothing',
44
+ );
45
+ }
46
+ }
47
+ }
48
+ const required = data.bones.filter((b) => b.skinRequired).length + data.constraints.filter((c) => c.skinRequired).length;
49
+ if (listed === 0 && required === 0) {
50
+ return skip(
51
+ 'A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED',
52
+ 'no skin activates a bone or a constraint, and nothing declares itself skinRequired',
53
+ );
54
+ }
55
+ for (const bone of data.bones) {
56
+ if (bone.skinRequired && !activatable.has(bone.name)) {
57
+ fail(
58
+ 'A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED',
59
+ `bone "${bone.name}" is skinRequired and no skin's bones list reaches it, so it is never active — ` +
60
+ 'it and its subtree hold the setup pose under every skin',
61
+ );
62
+ }
63
+ }
64
+ for (const constraint of data.constraints) {
65
+ if (constraint.skinRequired && !listedConstraints.has(constraint)) {
66
+ fail(
67
+ 'A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED',
68
+ `constraint "${constraint.name}" is skinRequired and no skin lists it, so it never runs`,
69
+ );
70
+ }
71
+ }
72
+ stats.skinMembers = listed;
73
+ }