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,458 @@
1
+ /**
2
+ * What one timeline does when it is applied additively — the question A40
3
+ * asks of the timeline a later slider shares with an earlier one (issue
4
+ * #1025, cut 4c-5 of step 4c of #380). The validator asks the runtime by
5
+ * posing (`timelineAddBehaviour` in `src/validate.ts`): the timeline applied
6
+ * with `add` set, at alpha 1, `MixFrom.current`, no events, TWICE, at every
7
+ * time its own keys name, the midpoints between them and one past the last,
8
+ * from two start states — the setup pose, and the setup pose with every number
9
+ * a pose holds moved by `0.375`, every slot showing nothing and the draw order
10
+ * turned by one — under no skin and under every skin. A second application
11
+ * that moves the pose again is `accumulates`; one that moved it the first time
12
+ * and not the second is `overwrites`; one that never moves it is `inert`; the
13
+ * strongest any cell reads wins. This module answers the same question from
14
+ * the model document, with the core's own readings.
15
+ *
16
+ * ## The class is not a function of the timeline's kind alone
17
+ *
18
+ * Measured first, by posing the runtime's probe over a seeded population that
19
+ * reaches every timeline the catalogue lists (`src/timelines.ts`), each in
20
+ * three value variants — keys away from the setup, keys stating the setup
21
+ * values, keys stating the identity (0, scale 1, white, no draw-order move) —
22
+ * under no skin, the default skin and a second one, with targets active under
23
+ * one skin only (234 timelines, the core suite's `CO27`): of the 36
24
+ * spellings, 18 read one class on every timeline and 18 did not — every
25
+ * adding kind read `accumulates` on some timelines and `inert` on others, and
26
+ * a deform read all three. The kind decides what an application CAN do; three
27
+ * facts of the rig decide what it does:
28
+ *
29
+ * - **Whether its target is active under some skin.** A timeline whose bone,
30
+ * slot bone or constraint is inactive under every view writes nothing:
31
+ * `inert`, whatever its kind. The rules are the core's own — `activeBones`,
32
+ * `slotTimelinesApply`, `constraintInactiveWhy` (an ik is gated on its
33
+ * target, a transform on its source, a path on its slot's bone, a physics
34
+ * constraint on its bone, a slider on its dial) — measured equal here on an
35
+ * ik over a skin-required bone (applied) and onto one (not applied).
36
+ * - **Whether a deform or sequence timeline's attachment is shown.** Both
37
+ * are gated on the slot showing the record they are keyed on, or a linked
38
+ * mesh that plays its timelines (`timelineIdentity`); from the displaced
39
+ * state the slot shows nothing, so only the setup state can be written. A
40
+ * deform keyed on a record no view shows at setup is `inert`; a deform
41
+ * whose every offset is 0 fills the empty deform array once and adds 0 the
42
+ * second time — `overwrites`, not `accumulates`.
43
+ * - **Whether the value it adds is 0.** The ten bone value timelines, the
44
+ * transform mixes, a path's position and mix, physics wind and gravity and
45
+ * a slider's time and mix ADD: `current + v` (a scale `current + (v·setup −
46
+ * setup)`, the setup's and not the current's — a displaced `scaleX` 1.875
47
+ * over setup 1.5 keyed 3 read 4.875). Keyed to 0 (a scale to 1) they write
48
+ * nothing at all: `inert`.
49
+ *
50
+ * So the class is a computation over the document, and the table below
51
+ * carries only what the kind contributes — what its application does when it
52
+ * writes. The rest is computed per cell, as the probe reads it. (What the
53
+ * runtime calls the timeline is the validator's table, `RUNTIME_TIMELINE`,
54
+ * since issue #1054.)
55
+ *
56
+ * ## The cells, per kind
57
+ *
58
+ * - `adds`: each field `once = current + d`, `twice = once + d`, `d` the
59
+ * keyed value at the time (`channelAt`, the runtime's curve as the core
60
+ * reproduces it), a scale's `v·setup − setup`; `current` the setup value,
61
+ * or the setup value plus `0.375`. A deform: the setup state holds no
62
+ * deform, `once` is the keyed array (`deformAt`) and `twice` the core's
63
+ * additive blend of it over itself at alpha 1 (`blendDeform`).
64
+ * - `writes`: the keyed value replaces the current one — a number through the
65
+ * setup blend at alpha 1, `current + (v − current)`, a mass as its inverse; a
66
+ * colour channel the keyed value clamped to [0, 1]; an ik's flags as keyed;
67
+ * an inherit mode, an attachment by the name it resolves to under the view
68
+ * (`shownAttachment`), a draw order (`drawOrderAt`) against the setup order
69
+ * or the order turned by one; a sequence writes its frame wherever its
70
+ * record is shown (the setup pose holds no frame index).
71
+ * - `writes nothing`: an event timeline (a slider fires none) and a physics
72
+ * `reset` (no time passes between the two applications).
73
+ *
74
+ * A physics timeline naming no constraint (`*`) writes every active record
75
+ * whose `…Global` flag for its value is on (`unnamedPhysicsTargets`).
76
+ *
77
+ * ## Held
78
+ *
79
+ * The core suite's `CO27` poses the runtime's probe over the seeded
80
+ * population and holds every cell's letter, and every added field's three
81
+ * values at tolerance 0, to this module's; one row of the table flipped per
82
+ * class turns exactly the timelines using it red.
83
+ *
84
+ * ## Purity
85
+ *
86
+ * As the rest of the core: nothing from the Spine runtime package, nothing
87
+ * from `src/transform.ts`, no clock, no randomness, no I/O.
88
+ */
89
+ import type { ModelBone, ModelVertices } from '../model.ts';
90
+ import { channelAt, keyIndexAt, SLOT_COLOUR_TIMELINES, type BoneTimelineKind, type CoreKey, type SlotTimelineKind } from './animation.ts';
91
+ import { constraintInactiveWhy, TRANSFORM_MIXES, TRANSFORM_PROPERTIES, type CoreConstraintRecord, type CoreIkKey } from './constraints.ts';
92
+ import { EVERY_GLOBAL_PHYSICS, physicsActive, unnamedPhysicsTargets, type CorePhysicsRecord, type PhysicsTimelineKind } from './constraints_physics.ts';
93
+ import { blendDeform, deformAt, timelineIdentity, type CoreDeformKey, type CoreSequenceKey } from './deform.ts';
94
+ import { drawOrderAt, type CoreDrawOrderKey } from './draw_order.ts';
95
+ import { activeBones, foldInheritMode, readColour, shownAttachment, shownRow, type CompiledDocument, type CoreAttachment } from './index.ts';
96
+ import { slotTimelinesApply } from './skins.ts';
97
+
98
+ /** What a kind's application does when it writes (the header's *The cells, per kind*). */
99
+ export type AdditiveMode = 'adds' | 'writes' | 'writes nothing';
100
+
101
+ /**
102
+ * What each timeline spelling's application does when it writes, keyed by the
103
+ * document's own words (`additiveSpelling`) — measured off the probe's cells
104
+ * over the seeded population (the header). What the runtime calls each
105
+ * spelling — the class it loads as and the properties it registers, which A40's
106
+ * sentence prints — is the validator's to know and stands beside it
107
+ * (`RUNTIME_TIMELINE`, `src/assertions/model/runtime_timelines.ts`, issue
108
+ * #1054): this module speaks the document's words only.
109
+ */
110
+ export const ADDITIVE_MODE: Readonly<Record<string, AdditiveMode>> = {
111
+ 'bone rotate': 'adds',
112
+ 'bone translate': 'adds',
113
+ 'bone translatex': 'adds',
114
+ 'bone translatey': 'adds',
115
+ 'bone scale': 'adds',
116
+ 'bone scalex': 'adds',
117
+ 'bone scaley': 'adds',
118
+ 'bone shear': 'adds',
119
+ 'bone shearx': 'adds',
120
+ 'bone sheary': 'adds',
121
+ 'bone inherit': 'writes',
122
+ 'slot attachment': 'writes',
123
+ 'slot rgba': 'writes',
124
+ 'slot rgb': 'writes',
125
+ 'slot alpha': 'writes',
126
+ 'slot rgba2': 'writes',
127
+ 'slot rgb2': 'writes',
128
+ 'attachment deform': 'adds',
129
+ 'attachment sequence': 'writes',
130
+ ik: 'writes',
131
+ transform: 'adds',
132
+ 'path position': 'adds',
133
+ 'path spacing': 'writes',
134
+ 'path mix': 'adds',
135
+ 'physics inertia': 'writes',
136
+ 'physics strength': 'writes',
137
+ 'physics damping': 'writes',
138
+ 'physics mass': 'writes',
139
+ 'physics wind': 'adds',
140
+ 'physics gravity': 'adds',
141
+ 'physics mix': 'writes',
142
+ 'physics reset': 'writes nothing',
143
+ 'slider time': 'adds',
144
+ 'slider mix': 'adds',
145
+ drawOrder: 'writes',
146
+ events: 'writes nothing',
147
+ };
148
+
149
+ /** One timeline of a document's animation, as the core reads it — enough to pose the probe. */
150
+ export type AdditiveTimeline =
151
+ | { group: 'bone'; word: BoneTimelineKind; bone: string; keys: readonly CoreKey[] }
152
+ | { group: 'slot'; word: SlotTimelineKind; slot: string; keys: readonly CoreKey[] }
153
+ | { group: 'ik'; word: 'ik'; constraint: string; keys: readonly CoreIkKey[] }
154
+ | { group: 'transform'; word: 'transform'; constraint: string; keys: readonly CoreKey[] }
155
+ | { group: 'path'; word: 'position' | 'spacing' | 'mix'; constraint: string; keys: readonly CoreKey[] }
156
+ | { group: 'physics'; word: PhysicsTimelineKind; constraint: string; keys: readonly CoreKey[] }
157
+ | { group: 'slider'; word: 'time' | 'mix'; constraint: string; keys: readonly CoreKey[] }
158
+ | { group: 'attachment'; word: 'deform'; skin: string; slot: string; attachment: string; keys: readonly CoreDeformKey[] }
159
+ | { group: 'attachment'; word: 'sequence'; skin: string; slot: string; attachment: string; keys: readonly CoreSequenceKey[] }
160
+ | { group: 'drawOrder'; word: 'drawOrder'; keys: readonly CoreDrawOrderKey[] }
161
+ | { group: 'events'; word: 'events'; times: readonly number[] };
162
+
163
+ /** The spelling a timeline is a row of. */
164
+ export function additiveSpelling(t: AdditiveTimeline): string {
165
+ if (t.group === 'ik' || t.group === 'transform' || t.group === 'drawOrder' || t.group === 'events') return t.group;
166
+ return `${t.group} ${t.word}`;
167
+ }
168
+
169
+ /** One view the probe poses under: the name the runtime's skin carries (`(none)` for no skin set) and the document under it. */
170
+ export interface AdditiveView {
171
+ name: string;
172
+ doc: CompiledDocument;
173
+ }
174
+
175
+ /** What the probe moved one field to: before either application, after one, after two — each as the pose prints it. */
176
+ export interface AdditiveValue {
177
+ /** `bone:<name>.<field>`, `constraint:<name>.<field>` or `slot:<name>.deform`, the pose field's own name. */
178
+ field: string;
179
+ t: number;
180
+ before: string;
181
+ once: string;
182
+ twice: string;
183
+ }
184
+
185
+ /** One view under one start state: the letter the probe reads (`A` a second application moved it, `W` only the first did, `-` nothing), and the values an adding kind moved. */
186
+ export interface AdditiveCell {
187
+ view: string;
188
+ state: 'setup' | 'displaced';
189
+ letter: 'A' | 'W' | '-';
190
+ values: AdditiveValue[];
191
+ }
192
+
193
+ /** The class the probe reads: the strongest any cell reads. */
194
+ export type AdditiveBehaviour = 'accumulates' | 'overwrites' | 'inert';
195
+
196
+ /** The probe's displacement of every number a pose holds — the validator's `PROBE_DISPLACEMENT`, exact in binary. */
197
+ export const ADDITIVE_DISPLACEMENT = 0.375;
198
+
199
+ /** A plant: rows of the table read otherwise (the core suite's `CO27`). */
200
+ export interface AdditivePlant {
201
+ modes?: Readonly<Record<string, AdditiveMode>>;
202
+ }
203
+
204
+ /** Every time the probe applies a timeline at: its keys' own times, the midpoints between them, and one past the last. */
205
+ export function additiveTimes(times: readonly number[]): number[] {
206
+ const between: number[] = [];
207
+ for (let i = 1; i < times.length; i++) between.push((times[i - 1] + times[i]) / 2);
208
+ return [...times, ...between, (times.length ? times[times.length - 1] : 0) + 1];
209
+ }
210
+
211
+ const spell = (v: number): string => String(v);
212
+ const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);
213
+
214
+ /** One field moved by an adding kind: its two sums from `current`. */
215
+ function added(values: AdditiveValue[], field: string, t: number, current: number, d: number): { wrote: boolean; again: boolean } {
216
+ const once = current + d;
217
+ const twice = once + d;
218
+ values.push({ field, t, before: spell(current), once: spell(once), twice: spell(twice) });
219
+ return { wrote: spell(once) !== spell(current), again: spell(twice) !== spell(once) };
220
+ }
221
+
222
+ /** The keyed channels at `t`, the probe's times all at or after the first key. */
223
+ function channelsAt(keys: readonly CoreKey[], t: number): number[] {
224
+ const i = keyIndexAt(keys, t);
225
+ if (i < 0) return [];
226
+ return keys[i].values.map((_v, c) => channelAt(keys, i, c, t));
227
+ }
228
+
229
+ /** A number written through the setup blend at alpha 1: whether it moves `current`. */
230
+ const blendMoves = (current: number, v: number): boolean => spell(current + (v - current)) !== spell(current);
231
+
232
+ /** The setup value of a bone field. */
233
+ function boneSetup(b: ModelBone, field: 'x' | 'y' | 'rotation' | 'scaleX' | 'scaleY' | 'shearX' | 'shearY'): number {
234
+ return b[field] ?? (field === 'scaleX' || field === 'scaleY' ? 1 : 0);
235
+ }
236
+
237
+ const BONE_FIELDS: Readonly<Record<Exclude<BoneTimelineKind, 'inherit'>, ReadonlyArray<'x' | 'y' | 'rotation' | 'scaleX' | 'scaleY' | 'shearX' | 'shearY'>>> = {
238
+ rotate: ['rotation'],
239
+ translate: ['x', 'y'],
240
+ translatex: ['x'],
241
+ translatey: ['y'],
242
+ scale: ['scaleX', 'scaleY'],
243
+ scalex: ['scaleX'],
244
+ scaley: ['scaleY'],
245
+ shear: ['shearX', 'shearY'],
246
+ shearx: ['shearX'],
247
+ sheary: ['shearY'],
248
+ };
249
+
250
+ /** A constraint's record under a view, by kind and name. */
251
+ function recordOf(view: CompiledDocument, kind: CoreConstraintRecord['kind'], name: string): CoreConstraintRecord | undefined {
252
+ return view.constraints.find((c) => c.kind === kind && c.name === name)?.record;
253
+ }
254
+
255
+ /**
256
+ * Whether a constraint's timelines write into its pose under a view whose
257
+ * active bones are `active` — the solver's rule (`constraintInactiveWhy`,
258
+ * `physicsActive`) except for a slider, whose timelines write whatever its dial
259
+ * bone is: measured, a slider dialled by a skin-required bone keyed `mix`
260
+ * accumulated under no skin and the default skin, where the bone is inactive,
261
+ * as under the skin that lists the bone. The solver leaves such a slider
262
+ * unapplied; whether the runtime applies it was not measured here (the
263
+ * header's ⚠️).
264
+ */
265
+ function applies(r: CoreConstraintRecord, active: ReadonlySet<string>): boolean {
266
+ if (r.kind === 'physics') return physicsActive(r, active);
267
+ const why = constraintInactiveWhy(r, active);
268
+ return why === null || (r.kind === 'slider' && why !== 'skin');
269
+ }
270
+
271
+ /** The fields an adding constraint timeline moves, with their setup values. */
272
+ function constraintFields(t: AdditiveTimeline, r: CoreConstraintRecord): Array<{ field: string; setup: number; channel: number }> {
273
+ if (t.group === 'transform' && r.kind === 'transform') return TRANSFORM_PROPERTIES.map((p, channel) => ({ field: TRANSFORM_MIXES[p], setup: r.mixes[p], channel }));
274
+ if (t.group === 'path' && r.kind === 'path') {
275
+ if (t.word === 'position') return [{ field: 'position', setup: r.position, channel: 0 }];
276
+ if (t.word === 'mix') return [{ field: 'mixRotate', setup: r.mixRotate, channel: 0 }, { field: 'mixX', setup: r.mixX, channel: 1 }, { field: 'mixY', setup: r.mixY, channel: 2 }];
277
+ return [{ field: 'spacing', setup: r.spacing, channel: 0 }];
278
+ }
279
+ if (t.group === 'physics' && r.kind === 'physics' && t.word !== 'reset') {
280
+ if (t.word === 'mass') return [{ field: 'massInverse', setup: r.massInverse, channel: 0 }];
281
+ return [{ field: t.word, setup: r[t.word], channel: 0 }];
282
+ }
283
+ if (t.group === 'slider' && r.kind === 'slider') return [{ field: t.word, setup: r[t.word], channel: 0 }];
284
+ if (t.group === 'ik' && r.kind === 'ik') return [{ field: 'mix', setup: r.mix, channel: 0 }, { field: 'softness', setup: r.softness, channel: 1 }];
285
+ return [];
286
+ }
287
+
288
+ /** The vertices a shown record's deform array is computed on: its own, or a linked mesh's source's. */
289
+ function deformVertices(view: CompiledDocument, record: CoreAttachment): ModelVertices | null {
290
+ const g = record.geometry;
291
+ if (g === undefined) return null;
292
+ const source = g.kind === 'linkedmesh' ? view.skins.find((k) => k.name === g.skin)?.attachments[g.slot]?.[g.source]?.geometry : g;
293
+ return source !== undefined && source.kind !== 'region' && source.kind !== 'linkedmesh' ? source.vertices : null;
294
+ }
295
+
296
+ /** One view, one start state: the letter and the values (the header's cells). */
297
+ function cellOf(view: AdditiveView, state: 'setup' | 'displaced', t: AdditiveTimeline, mode: AdditiveMode): AdditiveCell {
298
+ const doc = view.doc;
299
+ const active = activeBones(doc);
300
+ const shift = state === 'setup' ? 0 : ADDITIVE_DISPLACEMENT;
301
+ const values: AdditiveValue[] = [];
302
+ let wrote = false;
303
+ let again = false;
304
+ const note = (r: { wrote: boolean; again: boolean }): void => {
305
+ wrote ||= r.wrote;
306
+ again ||= r.again;
307
+ };
308
+ const times = additiveTimes(t.group === 'events' ? t.times : t.keys.map((k) => k.time));
309
+ const done = (): AdditiveCell => ({ view: view.name, state, letter: again ? 'A' : wrote ? 'W' : '-', values });
310
+ if (mode === 'writes nothing') return done();
311
+ switch (t.group) {
312
+ case 'bone': {
313
+ const b = doc.bones.find((x) => x.name === t.bone);
314
+ if (b === undefined || !active.has(b.name)) return done();
315
+ if (t.word === 'inherit') {
316
+ // The displaced pose holds the mode's number plus 0.375, which no mode is; the setup pose holds the setup mode.
317
+ const setup = foldInheritMode(b.inheritMode ?? 'normal') ?? 'normal';
318
+ for (const time of times) {
319
+ const i = keyIndexAt(t.keys, time);
320
+ if (i < 0) continue;
321
+ if (state === 'displaced' || t.keys[i].mode !== setup) wrote = true;
322
+ }
323
+ return done();
324
+ }
325
+ const fields = BONE_FIELDS[t.word];
326
+ for (const time of times) {
327
+ const v = channelsAt(t.keys, time);
328
+ fields.forEach((field, c) => {
329
+ const setup = boneSetup(b, field);
330
+ const d = mode === 'adds' ? (field === 'scaleX' || field === 'scaleY' ? v[c] * setup - setup : v[c]) : Number.NaN;
331
+ if (mode === 'adds') note(added(values, `bone:${b.name}.${field}`, time, setup + shift, d));
332
+ else note({ wrote: blendMoves(setup + shift, v[c]), again: false });
333
+ });
334
+ }
335
+ return done();
336
+ }
337
+ case 'slot': {
338
+ const slot = doc.slots.find((s) => s.name === t.slot);
339
+ if (slot === undefined || !slotTimelinesApply(doc, slot, active)) return done();
340
+ if (t.word === 'attachment') {
341
+ const nameShown = (placeholder: string | null): string | null => {
342
+ if (placeholder === null) return null;
343
+ const s = shownAttachment(doc, { ...slot, setup: placeholder });
344
+ return s === null || 'conflict' in s ? null : shownRow(s).name;
345
+ };
346
+ // The setup pose shows the setup attachment; the displaced pose shows nothing.
347
+ const current = state === 'setup' ? nameShown(slot.setup) : null;
348
+ for (const time of times) {
349
+ const i = keyIndexAt(t.keys, time);
350
+ if (i >= 0 && nameShown(t.keys[i].name ?? null) !== current) wrote = true;
351
+ }
352
+ return done();
353
+ }
354
+ const shape = SLOT_COLOUR_TIMELINES[t.word];
355
+ const light = readColour(slot.color ?? 'ffffffff');
356
+ const dark = slot.dark === undefined ? null : readColour(slot.dark);
357
+ for (const time of times) {
358
+ const v = channelsAt(t.keys, time);
359
+ shape.channels.forEach((channel: string, c: number) => {
360
+ const second = channel.endsWith('2');
361
+ const at = ['r', 'g', 'b', 'a'].indexOf(second ? channel.slice(0, 1) : channel);
362
+ const setup = second ? (dark === null ? Number.NaN : dark[at]) : light[at];
363
+ if (spell(clamp01(v[c])) !== spell(setup + shift)) wrote = true;
364
+ });
365
+ }
366
+ return done();
367
+ }
368
+ case 'attachment': {
369
+ // The displaced pose shows nothing on any slot, and both timelines are gated on what a slot shows.
370
+ if (state === 'displaced') return done();
371
+ const identity = `${t.skin}/${t.slot}/${t.attachment}`;
372
+ for (const slot of doc.slots) {
373
+ if (!slotTimelinesApply(doc, slot, active)) continue;
374
+ const shown = shownAttachment(doc, slot);
375
+ if (shown === null || 'conflict' in shown || timelineIdentity(slot.name, shown) !== identity) continue;
376
+ if (t.word === 'sequence') {
377
+ if (shown.record.sequenceCount !== undefined && t.keys.length > 0) wrote = true;
378
+ continue;
379
+ }
380
+ const vertices = deformVertices(doc, shown.record);
381
+ if (vertices === null) continue;
382
+ for (const time of times) {
383
+ const target = deformAt(vertices, t.keys, time);
384
+ if (target === null) continue;
385
+ // From no deform, the additive blend at alpha 1 is the target (`blendDeform`); a second one adds `target − setup` to it.
386
+ const once = blendDeform(vertices, null, target, 1, true);
387
+ const twice = blendDeform(vertices, once, target, 1, true);
388
+ values.push({ field: `slot:${slot.name}.deform`, t: time, before: '[]', once: `[${once.join(',')}]`, twice: `[${twice.join(',')}]` });
389
+ wrote = true;
390
+ if (once.join(',') !== twice.join(',')) again = true;
391
+ }
392
+ }
393
+ return done();
394
+ }
395
+ case 'drawOrder': {
396
+ const count = doc.slots.length;
397
+ const setup = Array.from({ length: count }, (_v, i) => i);
398
+ const current = state === 'setup' ? setup : [...setup.slice(1), ...setup.slice(0, 1)];
399
+ for (const time of times) {
400
+ const order = drawOrderAt(count, t.keys, time);
401
+ if (order !== null && order.join(',') !== current.join(',')) wrote = true;
402
+ }
403
+ return done();
404
+ }
405
+ case 'events':
406
+ // A kind that writes moves the pose; an event timeline under a slider fires nothing (the table's mode), so only a plant reaches this.
407
+ if (mode === 'writes') wrote = true;
408
+ return done();
409
+ default: {
410
+ const records: CoreConstraintRecord[] =
411
+ t.group === 'physics' && t.constraint === EVERY_GLOBAL_PHYSICS
412
+ ? unnamedPhysicsTargets(
413
+ doc.constraints.flatMap((c): CorePhysicsRecord[] => (c.record?.kind === 'physics' ? [c.record] : [])),
414
+ t.word,
415
+ (r) => physicsActive(r, active),
416
+ )
417
+ : [recordOf(doc, t.group, t.constraint)].filter((r): r is CoreConstraintRecord => r !== undefined && applies(r, active));
418
+ for (const r of records) {
419
+ // A kind that writes with no field of its own to compare — only a plant makes a `reset` one — moves the pose wherever it applies.
420
+ if (t.group === 'physics' && t.word === 'reset' && mode === 'writes') wrote = true;
421
+ const fields = constraintFields(t, r);
422
+ for (const time of times) {
423
+ const v = channelsAt(t.keys, time);
424
+ for (const { field, setup, channel } of fields) {
425
+ const current = setup + shift;
426
+ if (mode === 'adds') note(added(values, `constraint:${r.name}.${field}`, time, current, v[channel]));
427
+ // A mass is keyed as a mass and posed as its inverse.
428
+ else if (field === 'massInverse') note({ wrote: spell(1 / (1 / current + (v[channel] - 1 / current))) !== spell(current), again: false });
429
+ else note({ wrote: blendMoves(current, v[channel]), again: false });
430
+ }
431
+ // An ik key's flags: the displaced pose moved the bend direction (a number); the setup pose holds the setup flags.
432
+ if (t.group === 'ik' && r.kind === 'ik') {
433
+ const i = keyIndexAt(t.keys, time);
434
+ const flags = i < 0 ? undefined : t.keys[i].flags;
435
+ if (flags !== undefined && (state === 'displaced' || flags.bendPositive !== r.bendPositive || flags.compress !== r.compress || flags.stretch !== r.stretch)) wrote = true;
436
+ }
437
+ }
438
+ }
439
+ return done();
440
+ }
441
+ }
442
+ }
443
+
444
+ /** Every cell of the probe over `views` — each view under the setup state, then the displaced one. */
445
+ export function additiveCells(views: readonly AdditiveView[], t: AdditiveTimeline, plant: AdditivePlant = {}): AdditiveCell[] {
446
+ const modes = plant.modes ?? ADDITIVE_MODE;
447
+ const spelled = additiveSpelling(t);
448
+ const mode = modes[spelled];
449
+ if (mode === undefined) throw new Error(`internal: "${spelled}" is no row of the additive table`);
450
+ return views.flatMap((view) => [cellOf(view, 'setup', t, mode), cellOf(view, 'displaced', t, mode)]);
451
+ }
452
+
453
+ /** The class the probe reads over its cells: `accumulates` where any cell does, `overwrites` where any cell only wrote, `inert` otherwise. */
454
+ export function additiveBehaviour(cells: readonly AdditiveCell[]): AdditiveBehaviour {
455
+ if (cells.some((c) => c.letter === 'A')) return 'accumulates';
456
+ return cells.some((c) => c.letter === 'W') ? 'overwrites' : 'inert';
457
+ }
458
+