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,126 @@
1
+ /**
2
+ * A47, the body (issue #1025, step 4c of #380): an ik constraint muted for
3
+ * good β€” resting at mix 0 with no animation switching it on β€” is refused,
4
+ * unless the rig declares that the consumer drives its mix.
5
+ *
6
+ * The question A23, A36 and A37 ask of their own kinds, asked of the kind
7
+ * editor exports use most (issue #765): a constraint that rests muted and
8
+ * that no animation switches on parses, sits in the update cache and moves
9
+ * nothing. πŸ”‘ Live is the runtime's own test, `!== 0`: `IkConstraint.update`
10
+ * returns on `mix === 0`, so a negative mix runs. Channel 0 of an ik frame is
11
+ * `mix`; the other four are softness, bend direction, compress and stretch.
12
+ * The argument with its measurements sits above the `check` call in
13
+ * `validate()`.
14
+ *
15
+ * Moved out of `src/validate.ts` whole: the bones, the target, the setup mix
16
+ * and the keyed ones are the rig's, the declaration the rig spec's.
17
+ *
18
+ * πŸ”© A second clause reads the constraint's `bones` as a shape, whatever its
19
+ * mix (issue #1205): more than two bones, or a pair whose second bone's
20
+ * parent is not the first, is a constraint the solver cannot apply as drawn
21
+ * β€” `ikShapeFault` below, whose sentence the rig-spec parser prints too. A
22
+ * clause rather than an assertion of its own because it asks this body's
23
+ * question of the same object β€” does this ik do what it says β€” and a new
24
+ * registry entry would move every count the documents state.
25
+ */
26
+ import type { Verdicts } from '../harness.ts';
27
+ import type { ConstraintFacts } from '../facts/constraints.ts';
28
+ import { consumerDriven, consumerSkip, declaredButLive, declareIt, ikLive, noneKeysItsMixAbove0, REST_OR_KEY_ITS_MIX, switchedOn } from '../constraint_words.ts';
29
+ import type { RigInfo } from '../../types.ts';
30
+
31
+ /**
32
+ * What is wrong with an ik constraint's `bones` as a shape, in the sentence
33
+ * both the rig-spec parser and A47 print β€” or `null` when the shape is one
34
+ * the solver applies as drawn (issue #1205).
35
+ *
36
+ * - **A count other than one or two moves nothing.** The runtime's update
37
+ * switches on the count with a case for one bone and one for two, so a
38
+ * constraint over three or more is in the update cache and applies nothing
39
+ * [measured: a chain under an ik over three bones and over four posed every
40
+ * bone exactly where the same rig with no constraint did, 49 samples,
41
+ * tolerance 0]. A list of none is refused before this (the compiler's
42
+ * "needs a non-empty bones array"; an export's own loader).
43
+ * - **A pair whose second bone's parent is not the first solves another
44
+ * triangle.** The two-bone solve places the second bone through the first's
45
+ * matrix from the second's own local offset, so whatever stands between
46
+ * them is left out of the chain it solves [measured on the same chain: a
47
+ * bone between them at the child's origin left the tip 0.27–2.60 off the
48
+ * target, a pair skipping a bone 18.95–22.25 off, and a zero-offset bone
49
+ * between them at the parent's origin landed only while it held its
50
+ * identity β€” turned 15Β° in setup, 3.50–4.11 off in every frame]. The rule
51
+ * reads the parent and not the pose, because a pose that happens to agree
52
+ * is not a rig that does. rigc's own reader of the model document refuses
53
+ * both shapes too (`A00_MODEL_READ` on the core entry); this is the same
54
+ * refusal on the side that reads a Spine file.
55
+ *
56
+ * `secondAncestors` is the second bone's ancestors, parent first, as the
57
+ * facts carry them.
58
+ */
59
+ export function ikShapeFault(name: string, bones: readonly string[], secondAncestors: readonly string[]): string | null {
60
+ const quoted = (list: readonly string[]): string => list.map((b) => `"${b}"`).join(', ');
61
+ if (bones.length > 2) {
62
+ return (
63
+ `ik constraint "${name}" names ${bones.length} bones (${quoted(bones)}); the solver applies one or two, so a constraint ` +
64
+ `over ${bones.length} moves nothing β€” name one bone, or two where the second is the first's child`
65
+ );
66
+ }
67
+ if (bones.length !== 2 || secondAncestors[0] === bones[0]) return null;
68
+ const [first, second] = bones;
69
+ const at = secondAncestors.indexOf(first);
70
+ const why =
71
+ at > 0
72
+ ? `${quoted(secondAncestors.slice(0, at))} ${at === 1 ? 'stands' : 'stand'} between`
73
+ : secondAncestors.length === 0
74
+ ? `"${second}" has no parent`
75
+ : `"${first}" is not above it; its parent is "${secondAncestors[0]}"`;
76
+ return (
77
+ `ik constraint "${name}": "${second}" is not a child of "${first}" (${why}), so the two-bone solve is not of the ` +
78
+ `chain drawn β€” name "${second}"'s own parent as the first bone, or make "${second}" a child of "${first}"`
79
+ );
80
+ }
81
+
82
+ export function a47IkConstraintNotMutedThroughout({ fail, skip, stats }: Verdicts, facts: ConstraintFacts, rig: RigInfo | undefined): void {
83
+ const NAME = 'A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT';
84
+ const constraints = facts.constraints.flatMap((c, index) => (c.ik === undefined ? [] : [{ name: c.name, ik: c.ik, index }]));
85
+ if (!constraints.length) return skip(NAME, 'the skeleton declares no ik constraint');
86
+ // The shape first, whatever the mix: a constraint the solver cannot apply as drawn is not exempted by a declaration of who sets its mix.
87
+ let shapeFaults = 0;
88
+ for (const { name, ik } of constraints) {
89
+ const fault = ikShapeFault(name, ik.bones, ik.secondAncestors);
90
+ if (fault === null) continue;
91
+ shapeFaults++;
92
+ fail(NAME, fault);
93
+ }
94
+ // Channel 0 is `mix`; the other four are softness, bend direction, compress and stretch.
95
+ const ikSwitchedOn = switchedOn(facts, (timeline) => timeline.kind === 'ik', 1, (_timeline, value, channel) => channel === 0 && ikLive(value));
96
+ const declared = consumerDriven(rig, 'ik');
97
+ const exempt: Array<[string, string]> = [];
98
+ for (const { name, ik: constraint, index } of constraints) {
99
+ const mix = constraint.mix;
100
+ const live = ikLive(mix) || ikSwitchedOn.has(index);
101
+ const why = declared.get(name);
102
+ if (why !== undefined) {
103
+ if (!live) exempt.push([name, why]);
104
+ else {
105
+ fail(
106
+ NAME,
107
+ declaredButLive(
108
+ `ik constraint "${name}"`,
109
+ ikLive(mix) ? `it rests at mix ${mix}` : 'an animation keys its mix above 0',
110
+ ),
111
+ );
112
+ }
113
+ continue;
114
+ }
115
+ if (live) continue;
116
+ fail(
117
+ NAME,
118
+ `ik constraint "${name}" has mix ${mix} at setup and ${noneKeysItsMixAbove0(facts.animations)}; ` +
119
+ `update() returns on mix 0, so ${constraint.bones.map((bone) => `"${bone}"`).join(' and ')} never ` +
120
+ `reach${constraint.bones.length === 1 ? 'es' : ''} for "${constraint.target}" β€” ${REST_OR_KEY_ITS_MIX}, ` +
121
+ declareIt('ik', name),
122
+ );
123
+ }
124
+ if (exempt.length) stats.ikConsumerDriven = exempt.map(([name]) => name).join(',');
125
+ if (exempt.length === constraints.length && shapeFaults === 0) return skip(NAME, consumerSkip('ik', exempt, facts.animations));
126
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * A48, the body (issue #1025, step 4c of #380): a transform constraint muted
3
+ * for good on every mix it reads is refused, unless the rig declares that the
4
+ * consumer drives its mix.
5
+ *
6
+ * πŸ”‘ **A transform mix is read only for a property the constraint drives.**
7
+ * The early return in `TransformConstraint.update` is over all six mixes, but
8
+ * it is not what decides whether anything moves: each `to` entry reads its own
9
+ * mix (`ToRotate.mix` is `mixRotate`, …). At setup the parser only reads a mix
10
+ * whose property is declared, so the two tests agree there β€” but a timeline
11
+ * key that omits a mix is read as 1 (`SkeletonJson.js`, every `getValue(…, 1)`),
12
+ * so a key of `mixRotate: 0` alone on a rotate-only constraint passes the
13
+ * six-mix test on five mixes nothing reads. [measured] that key poses every
14
+ * bone exactly where no constraint does, and so does one keying `mixX` 1 on
15
+ * the same constraint. So this reads the mixes of the declared `to` kinds,
16
+ * at setup and on every value a key poses β€” the fact's `mixes`, `null` where
17
+ * the constraint declares no `to` of that property. Live is `!== 0`
18
+ * (`ikLive`'s note).
19
+ *
20
+ * Moved out of `src/validate.ts` whole: the declared `to` kinds, the setup
21
+ * mixes and the keyed ones are the rig's, the declaration the rig spec's.
22
+ */
23
+ import type { Verdicts } from '../harness.ts';
24
+ import type { ConstraintFacts } from '../facts/constraints.ts';
25
+ import { consumerDriven, consumerSkip, declaredButLive, declareIt, noneKeysItsMixAbove0, switchedOn } from '../constraint_words.ts';
26
+ import type { RigInfo } from '../../types.ts';
27
+
28
+ export function a48TransformConstraintNotMutedThroughout({ fail, skip, stats }: Verdicts, facts: ConstraintFacts, rig: RigInfo | undefined): void {
29
+ const NAME = 'A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT';
30
+ const constraints = facts.constraints.flatMap((c, index) => (c.transform === undefined ? [] : [{ name: c.name, transform: c.transform, index }]));
31
+ if (!constraints.length) return skip(NAME, 'the skeleton declares no transform constraint');
32
+ /** Which of the six channels `constraint` reads at all: the ones whose `to` kind it declares. */
33
+ const transformReads = (index: number): boolean[] => (facts.constraints[index]?.transform?.mixes ?? []).map((mix) => mix !== null);
34
+ const transformSwitchedOn = switchedOn(facts, (timeline) => timeline.kind === 'transform', 6, (timeline, value, channel) => {
35
+ const constraint = facts.constraints[timeline.constraint];
36
+ return constraint?.transform !== undefined && transformReads(timeline.constraint)[channel] && value !== 0;
37
+ });
38
+ const declared = consumerDriven(rig, 'transform');
39
+ const exempt: Array<[string, string]> = [];
40
+ for (const { name, transform: constraint, index } of constraints) {
41
+ const where = `transform constraint "${name}"`;
42
+ const pose: Record<string, number> = {};
43
+ for (const mix of constraint.mixes) if (mix !== null) pose[mix.field] = mix.setup;
44
+ const read = constraint.mixes.flatMap((mix) => (mix === null ? [] : [mix.field]));
45
+ if (read.length === 0) {
46
+ // No `to` at all: no mix is ever read, so neither remedy below applies.
47
+ fail(
48
+ NAME,
49
+ `${where} drives no property β€” its \`properties\` name no \`to\` β€” so no mix it carries is ever read and it ` +
50
+ 'moves nothing; declare the property it should drive',
51
+ );
52
+ continue;
53
+ }
54
+ const resting = read.filter((field) => pose[field] !== 0);
55
+ const live = resting.length > 0 || transformSwitchedOn.has(index);
56
+ const why = declared.get(name);
57
+ if (why !== undefined) {
58
+ if (!live) exempt.push([name, why]);
59
+ else {
60
+ fail(
61
+ NAME,
62
+ declaredButLive(
63
+ where,
64
+ resting.length ? `it rests at ${resting.map((field) => `${field} ${pose[field]}`).join(', ')}` : 'an animation keys its mix above 0',
65
+ ),
66
+ );
67
+ }
68
+ continue;
69
+ }
70
+ if (live) continue;
71
+ fail(
72
+ NAME,
73
+ `${where} drives ${read.map((field) => field.slice(3).replace(/^./, (c) => c.toLowerCase())).join(', ')} and has ` +
74
+ `${read.map((field) => `${field} ${pose[field]}`).join(', ')} at setup, and ` +
75
+ `${noneKeysItsMixAbove0(facts.animations)}; a mix is read only for a property the constraint drives, and ` +
76
+ `update() skips each one at 0, so nothing ever moves ${constraint.bones.map((bone) => `"${bone}"`).join(', ')} β€” ` +
77
+ `rest ${read.length === 1 ? read[0] : `one of ${read.join(', ')}`} above 0, or key its mix above 0 in an animation, ` +
78
+ declareIt('transform', name),
79
+ );
80
+ }
81
+ if (exempt.length) stats.transformConsumerDriven = exempt.map(([name]) => name).join(',');
82
+ if (exempt.length === constraints.length) return skip(NAME, consumerSkip('transform', exempt, facts.animations));
83
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * A49, the body (issue #1099): no two regions on one page draw over each
3
+ * other β€” refused only where their rectangles overlap **and** what they draw
4
+ * overlaps, so a mesh's region may sit over a neighbour's rectangle wherever
5
+ * its hull is not. The footprint, and why the test is exactly A06's old tiling
6
+ * clause for two rectangles, is `../footprints.ts`.
7
+ *
8
+ * ⭐ **The clause it replaces.** A06 held, under `spine-html`, that no two
9
+ * region rectangles on one page overlap. A rectangle's footprint is the
10
+ * rectangle, so on every pair A06 refused where neither region is a readable
11
+ * mesh hull β€” every editor alias, every sequence frame, every rotated region,
12
+ * every plant of two rectangles over the same texels β€” this refuses the same
13
+ * pair, printing the same two rectangles first. What it accepts that A06 did
14
+ * not is a pair whose rectangles overlap where a hull leaves the rectangle
15
+ * transparent: what `--pack-shape polygon` writes.
16
+ *
17
+ * πŸ”Έ **Nothing about the padding.** A06 never held a gap between two
18
+ * rectangles, and the padding a pack was made with is on no file the gate
19
+ * reads β€” not the atlas, not the model document; only the `pack:` line on
20
+ * stdout states it. The `2 Β· padding` the packer keeps between footprints is
21
+ * the packer's promise, held by `PK81`.
22
+ *
23
+ * πŸ“ **Renderer kind, the profile the clause ran under.** Two regions over the
24
+ * same texels is what an editor's packer writes for an alias or a sequence:
25
+ * measured over the ten atlases in `examples/`, 49 pairs on four pages, and a
26
+ * correct editor export. A rule that refused them under `spine` would be one
27
+ * consumer's convention refusing everybody else's data.
28
+ *
29
+ * The order of the lines is the atlas's: pages as they come, and within a page
30
+ * each pair in region order β€” the order A06's clause printed them in.
31
+ */
32
+ import type { Verdicts } from '../harness.ts';
33
+ import type { AtlasRegionFacts } from '../facts/atlas_regions.ts';
34
+ import type { MeshFacts } from '../facts/mesh_attachments.ts';
35
+ import type { RegionJoinFacts } from '../facts/region_joins.ts';
36
+ import { footprintOverlap, rectanglesOverlap, regionFootprints, type RegionFootprint } from '../footprints.ts';
37
+ import { SKIP_NO_ATLAS, SKIP_NO_ATLAS_PAGE, SKIP_NO_ATLAS_REGION, SKIP_NO_ATLAS_REGION_PAIR } from '../reasons.ts';
38
+
39
+ const NAME = 'A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP';
40
+
41
+ /** A number for the sentence: to four places, trailing zeros dropped β€” `347`, `81.5`, `1916.5512`. */
42
+ const texels = (n: number): string => n.toFixed(4).replace(/\.?0+$/, '');
43
+
44
+ const rectSaid = (f: RegionFootprint): string => `"${f.region.name}" (${f.rect.x},${f.rect.y} ${f.rect.width}x${f.rect.height})`;
45
+
46
+ export function a49PackedFootprintsDoNotOverlap({ fail, skip }: Verdicts, facts: AtlasRegionFacts, joins: RegionJoinFacts, meshes: MeshFacts | null): void {
47
+ const { atlas } = facts;
48
+ if (!atlas) return skip(NAME, SKIP_NO_ATLAS);
49
+ if (atlas.pages.length === 0) return skip(NAME, SKIP_NO_ATLAS_PAGE);
50
+ if (atlas.regions.length === 0) return skip(NAME, SKIP_NO_ATLAS_REGION);
51
+ const feet = regionFootprints(facts, joins, meshes);
52
+ const perPage = new Map<string, RegionFootprint[]>();
53
+ for (const foot of feet) {
54
+ const on = perPage.get(foot.region.page.name);
55
+ if (on) on.push(foot);
56
+ else perPage.set(foot.region.page.name, [foot]);
57
+ }
58
+ if ([...perPage.values()].every((on) => on.length < 2)) return skip(NAME, SKIP_NO_ATLAS_REGION_PAIR);
59
+ for (const [pageName, on] of perPage) {
60
+ for (let i = 0; i < on.length; i++) {
61
+ for (let j = i + 1; j < on.length; j++) {
62
+ const a = on[i];
63
+ const b = on[j];
64
+ if (!rectanglesOverlap(a, b)) continue;
65
+ const shared = footprintOverlap(a, b);
66
+ if (shared === null) continue;
67
+ const where =
68
+ a.whole && b.whole
69
+ ? `the two rectangles share ${texels(shared.box.x0)},${texels(shared.box.y0)} ${texels(shared.box.x1 - shared.box.x0)}x${texels(shared.box.y1 - shared.box.y0)}`
70
+ : `what they draw overlaps by ${texels(shared.area)} texel(s) of area inside ${texels(shared.box.x0)},${texels(shared.box.y0)} ` +
71
+ `${texels(shared.box.x1 - shared.box.x0)}x${texels(shared.box.y1 - shared.box.y0)}`;
72
+ fail(
73
+ NAME,
74
+ `regions ${rectSaid(a)} and ${rectSaid(b)} overlap on page "${pageName}", and so do their footprints β€” ` +
75
+ `"${a.region.name}" draws ${a.drawn}, "${b.region.name}" draws ${b.drawn}; ${where}. One drawing samples ` +
76
+ "texels that are the other's: move one region clear of the other's footprint, or pack the page again",
77
+ );
78
+ }
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * A50, the body (issue #1168): the stage box a rig asked for is the stage β€”
3
+ * in the Spine files a consumer ships, read back the way that consumer reads
4
+ * it.
5
+ *
6
+ * Since issue #907 the Spine header carries the setup-pose bounding box and the
7
+ * stage is stated only in `skeleton.model.json`, which a consumer does not ship.
8
+ * A rig that asks (`skeleton.stageBox`) carries the stage as a bounding-box
9
+ * attachment, whose numbers `compile` writes from the stage. This rule holds
10
+ * that box to the stage the document states, on both readings a consumer has:
11
+ *
12
+ * 1. **The data.** The slot carries, in the `default` skin under the asked
13
+ * name, an unweighted bounding box of four vertices, and its stored
14
+ * vertices are the stage's corners β€” `(x, y)`, `(x + w, y)`,
15
+ * `(x + w, y + h)`, `(x, y + h)` β€” each exactly, as the float32 the
16
+ * runtime loads. That is what a reader of the attachment gets without
17
+ * posing anything.
18
+ * 2. **The setup pose.** Its world vertices at the setup pose β€” constraints
19
+ * applied, no physics, no skin set β€” are those corners as the runtime
20
+ * poses them on an unrotated root: through the frame the core computes
21
+ * for a root that states no transform (`worldTransforms` in
22
+ * `../../core/world.ts`, whose unrotated `b` is cos 90Β° at the runtime's
23
+ * pi, βˆ’2.3e-8, not 0), compared on the header's grid
24
+ * (`headerBoxNumber` in `../../compile.ts`) β€” the tolerance the header's
25
+ * own box is held at. A constraint that moves the box's bone at setup
26
+ * is caught here and nowhere else: `compile` poses no constraint.
27
+ *
28
+ * **SKIP** where nothing asks: an export or a directory with no `/3` document,
29
+ * and a document that states a stage and no box β€” a rig that does not ask is
30
+ * unmeasured, never certified.
31
+ *
32
+ * Links nothing from the runtime.
33
+ */
34
+ import type { Verdicts } from '../harness.ts';
35
+ import type { StageBoxFacts } from '../facts/stage_box.ts';
36
+ import { worldTransforms } from '../../core/world.ts';
37
+ import { headerBoxNumber } from '../../compile.ts';
38
+
39
+ const CODE = 'A50_STAGE_BOX_IS_THE_STAGE';
40
+
41
+ /** The stage's four corners in Spine world, bottom-left first and counter-clockwise β€” the order `compile` writes. */
42
+ export function stageCorners(stage: { x: number; y: number; width: number; height: number }): number[] {
43
+ const right = stage.x + stage.width;
44
+ const top = stage.y + stage.height;
45
+ return [stage.x, stage.y, right, stage.y, right, top, stage.x, top];
46
+ }
47
+
48
+ export function a50StageBoxIsTheStage({ fail, skip }: Verdicts, facts: StageBoxFacts): void {
49
+ const { asked } = facts;
50
+ if (asked === null) return skip(CODE, facts.why);
51
+ const where = `the stage box (slot "${asked.slot}", attachment "${asked.attachment}")`;
52
+ const { x, y, width, height } = asked.stage;
53
+ const required = `the stage ${x},${y} ${width}x${height}`;
54
+ if (!facts.slot) return fail(CODE, `${where}: the skeleton has no slot "${asked.slot}", so a consumer finds no stage β€” ${required} β€” in it`);
55
+ const held = facts.held;
56
+ if (held === null) {
57
+ return fail(CODE, `${where}: the default skin holds no attachment "${asked.attachment}" on slot "${asked.slot}", so a consumer finds no stage β€” ${required} β€” in it`);
58
+ }
59
+ if (held.type !== 'boundingbox') {
60
+ return fail(CODE, `${where}: the attachment is a ${held.type}, not a bounding box β€” ${required} travels as a boundingbox attachment, which every runtime returns by slot and name`);
61
+ }
62
+ if (held.weighted) {
63
+ return fail(CODE, `${where}: the box's vertices are bound to bones, and the stage is four unweighted corners in the root's space β€” ${required}`);
64
+ }
65
+ const corners = stageCorners(asked.stage);
66
+ if (held.stored.length !== corners.length) {
67
+ return fail(CODE, `${where}: the box holds ${held.stored.length / 2} vertices, and the stage is 4 corners β€” ${required}`);
68
+ }
69
+ let faults = 0;
70
+ for (let i = 0; i < corners.length; i += 2) {
71
+ const want = [Math.fround(corners[i]), Math.fround(corners[i + 1])];
72
+ const found = [held.stored[i], held.stored[i + 1]];
73
+ if (found[0] !== want[0] || found[1] !== want[1]) {
74
+ faults++;
75
+ fail(CODE, `${where}: vertex ${i / 2} is stored as (${found[0]}, ${found[1]}) and the stage's corner is (${want[0]}, ${want[1]}) β€” ${required}, bottom-left first and counter-clockwise`);
76
+ }
77
+ }
78
+ if (faults > 0) return;
79
+ if (typeof held.world === 'string') return fail(CODE, `${where}: its setup pose cannot be read β€” ${held.world}`);
80
+ // The runtime's frame of a root that states no transform, as the core computes it.
81
+ const frame = worldTransforms([{ name: 'root' }]).get('root');
82
+ if (frame === undefined) throw new Error('internal: the core posed no frame for an unrotated root');
83
+ for (let i = 0; i < corners.length; i += 2) {
84
+ const cx = Math.fround(corners[i]);
85
+ const cy = Math.fround(corners[i + 1]);
86
+ const want = [headerBoxNumber(cx * frame.a + cy * frame.b + frame.worldX), headerBoxNumber(cx * frame.c + cy * frame.d + frame.worldY)];
87
+ const found = [headerBoxNumber(held.world[i]), headerBoxNumber(held.world[i + 1])];
88
+ if (found[0] !== want[0] || found[1] !== want[1]) {
89
+ fail(
90
+ CODE,
91
+ `${where}: vertex ${i / 2} poses at (${found[0]}, ${found[1]}) at the setup pose and the stage's corner poses at ` +
92
+ `(${want[0]}, ${want[1]}) on an unrotated root, on the header's grid β€” something moves the box's bone at setup ` +
93
+ '(a constraint, or a pose the root does not state), so the box a consumer reads is not the stage',
94
+ );
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * What the constraint bodies share (issue #1025, step 4c of #380): the one
3
+ * reading of "does an animation switch this on", the one predicate a path or
4
+ * slider mix is judged by, the physics components, and the sentences A23,
5
+ * A36, A37, A47 and A48 say alike. Moved here from `src/validate.ts`, each
6
+ * unchanged but for what `switchedOn` reads β€” the facts
7
+ * (`./facts/constraints.ts`) rather than the loaded timelines β€” because the
8
+ * bodies that read them moved, and a second copy is how two rules come to
9
+ * disagree. `src/validate.ts` imports what A40 still reads.
10
+ *
11
+ * Links nothing from the runtime.
12
+ */
13
+ import type { ConstraintFacts, ConstraintTimeline } from './facts/constraints.ts';
14
+ import type { RigInfo } from '../types.ts';
15
+
16
+ /**
17
+ * Every component a physics constraint can drive (`PhysicsConstraintData`), the
18
+ * vocabulary A23 reads when it asks whether a constraint drives anything.
19
+ *
20
+ * πŸ“ It stood beside a second set, the components "the Spine editor models",
21
+ * read by A41 (the editor round-trip rule) and measured in issue #540 as
22
+ * `x` and `y`. Issue #1196 measured what that was really seeing, on Spine 4.3.23
23
+ * and 4.3.26: the editor imports a physics constraint's `rotate`, `scaleX` and
24
+ * `shearX` into its project and its JSON and binary exports omit them **only on
25
+ * a bone with no length** β€” on any bone with a length (0.01, 1, 40 tried) all
26
+ * three come back, and the editor's own example export keeps 18 of 18 `rotate`
27
+ * constraints through the same trip. A23 refuses those components on a
28
+ * zero-length bone (issue #1195), so a rig that passes it has nothing the editor drops, and
29
+ * A41, its declaration and its set were retired rather than reworded.
30
+ */
31
+ export const PHYSICS_COMPONENTS = ['x', 'y', 'rotate', 'scaleX', 'shearX'] as const;
32
+
33
+ /**
34
+ * The half of a muted-at-rest refusal that says what was searched, and how
35
+ * widely β€” one text for `A23`, `A36` and `A37`, which ask one question of three
36
+ * constraint kinds (issues #743, #752). A rig with no animation at all says
37
+ * "none of the 0 animations" rather than implying somebody keyed something.
38
+ */
39
+ export function noneKeysItsMixAbove0(animations: number): string {
40
+ return `none of the ${animations} animation${animations === 1 ? '' : 's'} keys its mix above 0`;
41
+ }
42
+
43
+ /** The two repairs a muted-at-rest refusal names, since either one is a rig the runtime plays. */
44
+ export const REST_OR_KEY_ITS_MIX = 'rest it above 0, or key its mix above 0 in an animation';
45
+
46
+ /**
47
+ * The one predicate a path or slider mix is judged by, at setup and on every
48
+ * value a key poses: above 0, where `update()` does anything at all.
49
+ */
50
+ export const mixLive = (value: number): boolean => value > 0;
51
+
52
+ /**
53
+ * πŸ”‘ **An ik or transform mix is live by the runtime's own test, `!== 0`, and
54
+ * not `mixLive`'s `> 0`.** `IkConstraint.update` returns on `mix === 0` and a
55
+ * transform's inner loop applies a property only when `to.mix(pose) !== 0`, so
56
+ * a negative mix runs. That is not a corner: [measured] five transform
57
+ * constraints across four of the editor's own example exports rest at mixX =
58
+ * mixY = βˆ’1, nothing keys them, and each moves its bones at setup against the
59
+ * same constraint with every mix 0. A `> 0` reading refuses all five.
60
+ * (`A36`/`A37` still read `> 0` β€” a path or slider resting negative is a
61
+ * question for their own card.)
62
+ */
63
+ export const ikLive = (value: number): boolean => value !== 0;
64
+
65
+ /**
66
+ * The constraints some animation keys to a value `live` accepts, on any of the
67
+ * first `channels` channels of a timeline `owns` claims β€” the one answer to
68
+ * "does anything switch this on" for `A23`, `A36`, `A37`, `A40`, `A47` and
69
+ * `A48` (issues #743, #752), as positions in `facts.constraints`.
70
+ *
71
+ * ⭐ It replaced `keyedBy`, which read the raw JSON and took a non-empty key
72
+ * array as the answer, and it did so rather than teaching that one to read
73
+ * values, because the raw file is the wrong place to read a value: a path
74
+ * `mix` key that omits `mixRotate` means 1 (`SkeletonJson.js:1011-1013`), a
75
+ * `mixY` it omits means that key's `mixX`, and a Bezier between two keys
76
+ * poses values neither key states. So this reads every value a channel poses
77
+ * β€” each key's own and every sample of each BΓ©zier segment the parser built
78
+ * between two keys (`ConstraintTimeline.channelValues`): spine-core's loaded
79
+ * timelines on one side, the core's reading of the document on the other.
80
+ * [measured] the reading `keyedBy` gave was wrong in the accepting
81
+ * direction: a path constraint and a slider, both muted at rest and keyed to
82
+ * 0 only, pose every bone exactly where the same rig with no timeline does
83
+ * (max |Ξ”| 0.000000 over 60 steps at 60 fps) and both passed.
84
+ *
85
+ * A timeline naming no constraint is the physics family's global form, and
86
+ * its `reach` says who it writes; every other constraint timeline names its
87
+ * one constraint.
88
+ *
89
+ * `live` is handed the channel as well as the value, because not every
90
+ * channel of every constraint timeline is a mix (issue #765): an ik frame is
91
+ * mix, softness, bend direction, compress and stretch, so a bend direction of
92
+ * +1 is not a key that switches anything on, and a transform frame carries
93
+ * six mixes of which only the ones for a property the constraint drives are
94
+ * ever read. `channels` is how many leading channels the caller's `live` can
95
+ * accept a value on.
96
+ */
97
+ export function switchedOn(
98
+ facts: ConstraintFacts,
99
+ owns: (timeline: ConstraintTimeline) => boolean,
100
+ channels: number,
101
+ live: (timeline: ConstraintTimeline, value: number, channel: number) => boolean,
102
+ ): Set<number> {
103
+ const reached = new Set<number>();
104
+ for (const timeline of facts.timelines) {
105
+ if (!owns(timeline)) continue;
106
+ let keysLive = false;
107
+ for (let channel = 0; channel < channels && !keysLive; channel++) {
108
+ keysLive = timeline.channelValues(channel).some((value) => live(timeline, value, channel));
109
+ }
110
+ if (!keysLive) continue;
111
+ for (const one of timeline.reach) reached.add(one);
112
+ }
113
+ return reached;
114
+ }
115
+
116
+ // πŸ”‘ **The third door: the consumer drives the mix (issue #784).** A muted
117
+ // constraint nothing in the file switches on is either a leftover or a dial
118
+ // a game turns from code, and the two export as the same bytes β€” so the rig
119
+ // spec says which, in `invariants.consumerDrivenMix`, and a declared
120
+ // constraint is not measured by `A47` / `A48`. What that buys is never a pass
121
+ // on it:
122
+ //
123
+ // * every constraint of the kind declared β€” nothing is left to measure, so
124
+ // the rule SKIPs, naming each constraint, the declaration and its `why`;
125
+ // * some declared and some not β€” the rest are measured, and the declared
126
+ // ones go on the stats line, which is `A39`'s shape for `deformMayFold`.
127
+ // A SKIP there would put "nothing measured" over a rule that measured,
128
+ // and the summary would count a measured rule as skipped (`reportLines`'
129
+ // four buckets partition the registry, one row per rule).
130
+ //
131
+ // β›” A declared constraint the file ALSO switches on β€” resting live, or keyed
132
+ // above 0 β€” is refused: the declaration exempts nothing there, which is the
133
+ // shape `deformMayFold` on a slot with no mesh is refused for. [measured]
134
+ // (`scratchpad/consumerdriven_runtime.ts`) an ik keyed at mix 0.5 with code
135
+ // writing 1: code before `state.apply` is overwritten (applied mix 0.5),
136
+ // code after it wins (1.0) β€” so which author holds a frame is the order of
137
+ // the consumer's own loop, a fact about the scene rather than the object.
138
+
139
+ /** The rig's consumer-driven declarations of one kind: constraint name -> why. */
140
+ export function consumerDriven(rig: RigInfo | undefined, type: 'ik' | 'transform'): Map<string, string> {
141
+ return new Map((rig?.consumerDrivenMix ?? []).filter((e) => e.type === type).map((e) => [e.constraint, e.why]));
142
+ }
143
+
144
+ /** The third repair a muted-throughout refusal names. */
145
+ export function declareIt(type: 'ik' | 'transform', name: string): string {
146
+ return (
147
+ `or declare that the consumer drives its mix, in the rig spec as invariants.consumerDrivenMix: ` +
148
+ `[{ "constraint": "${name}", "type": "${type}", "why": … }]`
149
+ );
150
+ }
151
+
152
+ /** The refusal of a declaration that exempts nothing. */
153
+ export function declaredButLive(where: string, how: string): string {
154
+ return (
155
+ `${where} is declared in the rig spec as invariants.consumerDrivenMix, and the file already switches it on β€” ` +
156
+ `${how} β€” so the declaration exempts nothing; drop the entry. Where code also sets that mix, which of the two ` +
157
+ 'holds on a frame is the order of the consumer\'s own loop: `state.apply` overwrites a mix written before it, ' +
158
+ 'and a mix written after it replaces the key'
159
+ );
160
+ }
161
+
162
+ /** The SKIP when every constraint of the kind is declared consumer-driven. */
163
+ export function consumerSkip(kind: string, exempt: Array<[string, string]>, animations: number): string {
164
+ return (
165
+ `every ${kind} constraint here is declared in the rig spec as invariants.consumerDrivenMix, so its mix is the ` +
166
+ `consumer's to set and nothing in this file shows it moving β€” ${exempt.map(([name, why]) => `"${name}" (why: ${why})`).join('; ')}: ` +
167
+ `${exempt.length === 1 ? 'it rests' : 'each rests'} muted and ${noneKeysItsMixAbove0(animations)}`
168
+ );
169
+ }