spine-rigc 1.9.2 → 1.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/cli.ts +28 -4397
  2. package/docs/AUTHORING.md +1 -1
  3. package/package.json +1 -1
  4. package/src/assertions/bodies/a13.ts +3 -3
  5. package/src/assertions/bodies/a14.ts +3 -3
  6. package/src/assertions/bodies/a15.ts +5 -5
  7. package/src/assertions/bodies/a22.ts +3 -3
  8. package/src/assertions/facts/animated_bones.ts +4 -2
  9. package/src/assertions/facts/mesh_attachments.ts +33 -4
  10. package/src/assertions/model/animated_bones.ts +13 -5
  11. package/src/assertions/model/bone_timelines.ts +5 -3
  12. package/src/assertions/model/declared.ts +127 -0
  13. package/src/assertions/model/index.ts +63 -40
  14. package/src/assertions/model/mesh_attachments.ts +31 -2
  15. package/src/assertions/model/parse.ts +9 -1
  16. package/src/assertions/model/runtime_timelines.ts +78 -0
  17. package/src/assertions/model/slider_composition.ts +10 -8
  18. package/src/assertions/model/stepped_poses.ts +3 -2
  19. package/src/bonedist.ts +4 -5
  20. package/src/check.ts +3 -2
  21. package/src/checkpics.ts +1 -1
  22. package/src/cli/core_commands.ts +1532 -0
  23. package/src/cli/shared.ts +1935 -0
  24. package/src/cli/spine_commands.ts +1141 -0
  25. package/src/core/additive.ts +52 -62
  26. package/src/core/constraints.ts +26 -3
  27. package/src/core/constraints_physics.ts +11 -1
  28. package/src/core/constraints_slider.ts +136 -6
  29. package/src/core/index.ts +27 -3
  30. package/src/core/skins.ts +28 -3
  31. package/src/core/walk.ts +9 -36
  32. package/src/correspondence.ts +15 -0
  33. package/src/deformbuild.ts +60 -0
  34. package/src/deformmeasure.ts +20 -48
  35. package/src/deformreport.ts +2 -2
  36. package/src/framing.ts +1 -1
  37. package/src/pose.ts +1 -1
  38. package/src/preview.ts +1 -1
  39. package/src/region_joins.ts +54 -0
  40. package/src/render.ts +206 -2836
  41. package/src/render_core.ts +19 -36
  42. package/src/render_shared.ts +2887 -0
  43. package/src/slots.ts +1 -1
  44. package/src/spine_side.ts +123 -0
  45. package/src/validate.ts +11 -91
  46. package/src/assertions/facts/skin_meshes.ts +0 -49
  47. package/src/assertions/model/skin_meshes.ts +0 -90
@@ -49,8 +49,9 @@
49
49
  *
50
50
  * So the class is a computation over the document, and the table below
51
51
  * carries only what the kind contributes — what its application does when it
52
- * writes, the runtime class the timeline loads as and the properties it
53
- * registers. The rest is computed per cell, as the probe reads 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.)
54
55
  *
55
56
  * ## The cells, per kind
56
57
  *
@@ -97,63 +98,52 @@ import { slotTimelinesApply } from './skins.ts';
97
98
  /** What a kind's application does when it writes (the header's *The cells, per kind*). */
98
99
  export type AdditiveMode = 'adds' | 'writes' | 'writes nothing';
99
100
 
100
- /** What a timeline's property ids are followed by: its target's index (and, for an attachment's, the attachment), or nothing. */
101
- export type AdditiveAddress = 'bone' | 'slot' | 'attachment' | 'constraint' | 'none';
102
-
103
- /** One timeline spelling: the runtime class it loads as, the properties it registers in order, what its ids carry, and its mode. */
104
- export interface AdditiveRow {
105
- runtimeClass: string;
106
- properties: readonly string[];
107
- address: AdditiveAddress;
108
- mode: AdditiveMode;
109
- }
110
-
111
- const row = (runtimeClass: string, properties: readonly string[], address: AdditiveAddress, mode: AdditiveMode): AdditiveRow => ({ runtimeClass, properties, address, mode });
112
-
113
101
  /**
114
- * Every timeline spelling a document's animation can hold, as measured (the
115
- * header): the class name each loads as and the property ids it registers were
116
- * read off the loaded timeline objects, and the mode off the probe's cells over
117
- * the seeded population.
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.
118
109
  */
119
- export const ADDITIVE_APPLY: Readonly<Record<string, AdditiveRow>> = {
120
- 'bone rotate': row('RotateTimeline', ['rotate'], 'bone', 'adds'),
121
- 'bone translate': row('TranslateTimeline', ['x', 'y'], 'bone', 'adds'),
122
- 'bone translatex': row('TranslateXTimeline', ['x'], 'bone', 'adds'),
123
- 'bone translatey': row('TranslateYTimeline', ['y'], 'bone', 'adds'),
124
- 'bone scale': row('ScaleTimeline', ['scaleX', 'scaleY'], 'bone', 'adds'),
125
- 'bone scalex': row('ScaleXTimeline', ['scaleX'], 'bone', 'adds'),
126
- 'bone scaley': row('ScaleYTimeline', ['scaleY'], 'bone', 'adds'),
127
- 'bone shear': row('ShearTimeline', ['shearX', 'shearY'], 'bone', 'adds'),
128
- 'bone shearx': row('ShearXTimeline', ['shearX'], 'bone', 'adds'),
129
- 'bone sheary': row('ShearYTimeline', ['shearY'], 'bone', 'adds'),
130
- 'bone inherit': row('InheritTimeline', ['inherit'], 'bone', 'writes'),
131
- 'slot attachment': row('AttachmentTimeline', ['attachment'], 'slot', 'writes'),
132
- 'slot rgba': row('RGBATimeline', ['rgb', 'alpha'], 'slot', 'writes'),
133
- 'slot rgb': row('RGBTimeline', ['rgb'], 'slot', 'writes'),
134
- 'slot alpha': row('AlphaTimeline', ['alpha'], 'slot', 'writes'),
135
- 'slot rgba2': row('RGBA2Timeline', ['rgb', 'alpha', 'rgb2'], 'slot', 'writes'),
136
- 'slot rgb2': row('RGB2Timeline', ['rgb', 'rgb2'], 'slot', 'writes'),
137
- 'attachment deform': row('DeformTimeline', ['deform'], 'attachment', 'adds'),
138
- 'attachment sequence': row('SequenceTimeline', ['sequence'], 'attachment', 'writes'),
139
- ik: row('IkConstraintTimeline', ['ikConstraint'], 'constraint', 'writes'),
140
- transform: row('TransformConstraintTimeline', ['transformConstraint'], 'constraint', 'adds'),
141
- 'path position': row('PathConstraintPositionTimeline', ['pathConstraintPosition'], 'constraint', 'adds'),
142
- 'path spacing': row('PathConstraintSpacingTimeline', ['pathConstraintSpacing'], 'constraint', 'writes'),
143
- 'path mix': row('PathConstraintMixTimeline', ['pathConstraintMix'], 'constraint', 'adds'),
144
- 'physics inertia': row('PhysicsConstraintInertiaTimeline', ['physicsConstraintInertia'], 'constraint', 'writes'),
145
- 'physics strength': row('PhysicsConstraintStrengthTimeline', ['physicsConstraintStrength'], 'constraint', 'writes'),
146
- 'physics damping': row('PhysicsConstraintDampingTimeline', ['physicsConstraintDamping'], 'constraint', 'writes'),
147
- 'physics mass': row('PhysicsConstraintMassTimeline', ['physicsConstraintMass'], 'constraint', 'writes'),
148
- 'physics wind': row('PhysicsConstraintWindTimeline', ['physicsConstraintWind'], 'constraint', 'adds'),
149
- 'physics gravity': row('PhysicsConstraintGravityTimeline', ['physicsConstraintGravity'], 'constraint', 'adds'),
150
- 'physics mix': row('PhysicsConstraintMixTimeline', ['physicsConstraintMix'], 'constraint', 'writes'),
151
- // A reset registers its property with no constraint index, named or not (measured: both read `physicsConstraintReset`).
152
- 'physics reset': row('PhysicsConstraintResetTimeline', ['physicsConstraintReset'], 'none', 'writes nothing'),
153
- 'slider time': row('SliderTimeline', ['sliderTime'], 'constraint', 'adds'),
154
- 'slider mix': row('SliderMixTimeline', ['sliderMix'], 'constraint', 'adds'),
155
- drawOrder: row('DrawOrderTimeline', ['drawOrder'], 'none', 'writes'),
156
- events: row('EventTimeline', ['event'], 'none', 'writes nothing'),
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',
157
147
  };
158
148
 
159
149
  /** One timeline of a document's animation, as the core reads it — enough to pose the probe. */
@@ -208,7 +198,7 @@ export const ADDITIVE_DISPLACEMENT = 0.375;
208
198
 
209
199
  /** A plant: rows of the table read otherwise (the core suite's `CO27`). */
210
200
  export interface AdditivePlant {
211
- rows?: Readonly<Record<string, AdditiveRow>>;
201
+ modes?: Readonly<Record<string, AdditiveMode>>;
212
202
  }
213
203
 
214
204
  /** Every time the probe applies a timeline at: its keys' own times, the midpoints between them, and one past the last. */
@@ -453,11 +443,11 @@ function cellOf(view: AdditiveView, state: 'setup' | 'displaced', t: AdditiveTim
453
443
 
454
444
  /** Every cell of the probe over `views` — each view under the setup state, then the displaced one. */
455
445
  export function additiveCells(views: readonly AdditiveView[], t: AdditiveTimeline, plant: AdditivePlant = {}): AdditiveCell[] {
456
- const rows = plant.rows ?? ADDITIVE_APPLY;
446
+ const modes = plant.modes ?? ADDITIVE_MODE;
457
447
  const spelled = additiveSpelling(t);
458
- const r = rows[spelled];
459
- if (r === undefined) throw new Error(`internal: "${spelled}" is no row of the additive table`);
460
- return views.flatMap((view) => [cellOf(view, 'setup', t, r.mode), cellOf(view, 'displaced', t, r.mode)]);
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)]);
461
451
  }
462
452
 
463
453
  /** The class the probe reads over its cells: `accumulates` where any cell does, `overwrites` where any cell only wrote, `inert` otherwise. */
@@ -470,7 +470,7 @@ import { activeBones, type CompiledDocument, type CoreConstraintKind } from './i
470
470
  import { fillingSkins } from './skins.ts';
471
471
  import { readPathTimelines, slotBonePlan, solvePath, type CorePathRecord, type CorePathTimelines, type SlotBoneEvent } from './constraints_path.ts';
472
472
  import { physicsTimelineCount, readPhysicsTimelines, stepPhysics, type CorePhysicsRecord, type CorePhysicsTimeline, type PhysicsStepContext } from './constraints_physics.ts';
473
- import { applySlider, posedSlider, readSliderTimelines, sliderBonesWhy, type CoreSliderRecord, type CoreSliderTimeline, type SliderApplication } from './constraints_slider.ts';
473
+ import { applySlider, posedSlider, sliderPhysicsTarget, readSliderTimelines, sliderBonesWhy, type CoreSliderRecord, type CoreSliderTimeline, type SliderApplication } from './constraints_slider.ts';
474
474
 
475
475
  const DEG = 180 / RUNTIME_PI;
476
476
  const RAD = RUNTIME_PI / 180;
@@ -917,6 +917,20 @@ export interface SolverRules {
917
917
  sliderAdditiveScaleProduct: boolean;
918
918
  /** A slider's scale key at mix exactly 1 writes `setup·v` itself (issue #989), not `from + (setup·v − from)·1`. */
919
919
  sliderScaleMixOneIsTarget: boolean;
920
+ /**
921
+ * Issue #1049, each the rule `./constraints_slider.ts` *Physics timelines* states — `false` (or the named reading)
922
+ * plants the reading it rejected: a slider's physics keys write the pass's physics records (`false`: write nothing);
923
+ * a non-additive one blends from the CURRENT value (`false`: from the setup value); only `wind` and `gravity` add
924
+ * (`false`: every value kind adds); a `mass` key blends the mass (`false`: the inverse); the write lasts one pass
925
+ * (`false`: what a slider wrote stands on the next step wherever the step's own animation does not key it); a
926
+ * `reset` key fires nothing (`false`: a key at or before the slider's time resets its constraints on every pass).
927
+ */
928
+ sliderWritesPhysics: boolean;
929
+ sliderPhysicsFromCurrent: boolean;
930
+ sliderPhysicsAddsWindGravityOnly: boolean;
931
+ sliderPhysicsBlendsMass: boolean;
932
+ sliderPhysicsLastsOnePass: boolean;
933
+ sliderPhysicsResetIsDead: boolean;
920
934
  }
921
935
 
922
936
  /** The runtime's rules, as measured. */
@@ -939,6 +953,12 @@ export const RUNTIME_SOLVER_RULES: Readonly<SolverRules> = {
939
953
  sliderReposesKeyedBones: true,
940
954
  sliderAdditiveScaleProduct: true,
941
955
  sliderScaleMixOneIsTarget: true,
956
+ sliderWritesPhysics: true,
957
+ sliderPhysicsFromCurrent: true,
958
+ sliderPhysicsAddsWindGravityOnly: true,
959
+ sliderPhysicsBlendsMass: true,
960
+ sliderPhysicsLastsOnePass: true,
961
+ sliderPhysicsResetIsDead: true,
942
962
  };
943
963
 
944
964
  /** The runtime's rules with a plant's over them. */
@@ -1508,6 +1528,8 @@ export function applyConstraints(bones: readonly ModelBone[], world: ReadonlyMap
1508
1528
  // An ik over an inactive bone reads the frame above it as the pass left it (issue #979).
1509
1529
  const updated = records.some((c) => c.kind === 'ik' && c.bones.some((b) => !active.has(b))) ? frameUpdatedBefore(bones, active, records, skipped) : new Map<number, boolean>();
1510
1530
  const snapshots = new Map<number, CoreWorld>();
1531
+ // Issue #1049: under the step a slider's physics timelines write the pass's physics records, which a later physics constraint steps with.
1532
+ const physicsPose = physics === undefined ? undefined : sliderPhysicsTarget(records, active, physics, rules);
1511
1533
  const snap = (i: number, when: 'before' | 'after'): void => {
1512
1534
  for (const [k, e] of plan) if (e !== null && e.at === i && e.when === when && !(k === i && when === 'before')) snapshots.set(k, { ...(state.world.get((records[k] as CorePathRecord).slotBone) as CoreWorld) });
1513
1535
  };
@@ -1521,14 +1543,15 @@ export function applyConstraints(bones: readonly ModelBone[], world: ReadonlyMap
1521
1543
  // Under Physics.none a physics constraint applies nothing; stepped, it moves its bone in world space (`./constraints_physics.ts`).
1522
1544
  if (physics !== undefined) {
1523
1545
  const w = { ...(state.world.get(c.bone) as CoreWorld) };
1524
- if ((physics.step ?? stepPhysics)(c, w, bone(state, c.bone).length ?? 0, physics)) {
1546
+ const posed = physicsPose?.records.get(c.name) ?? c;
1547
+ if ((physics.step ?? stepPhysics)(posed, w, bone(state, c.bone).length ?? 0, physics)) {
1525
1548
  state.world.set(c.bone, w);
1526
1549
  changed = [c.bone];
1527
1550
  inWorld = [c.bone];
1528
1551
  }
1529
1552
  }
1530
1553
  } else if (c.kind === 'slider') {
1531
- changed = applySlider(state, c, applied);
1554
+ changed = applySlider(state, c, applied, physicsPose);
1532
1555
  } else if (c.kind === 'ik') {
1533
1556
  if (c.mix !== 0) {
1534
1557
  const frame = inactiveIkFrame(state, c, i, updated);
@@ -172,7 +172,9 @@
172
172
  * no value, on each of the seven, read exact — the `mass` one NaN in both
173
173
  * dumps). The timeline that
174
174
  * names no constraint (`*`) writes every active constraint whose `…Global`
175
- * flag for that value is on, and resets every active constraint.
175
+ * flag for that value is on, and resets every active constraint. A
176
+ * slider's animation keys them too, at the slider's place in the update
177
+ * order (issue #1049, `./constraints_slider.ts` *Its physics timelines*).
176
178
  *
177
179
  * **reset.** A `reset` key resets its constraints on the step whose apply
178
180
  * crosses it — a key in `(last, t]`, `last` the time the walk last applied
@@ -569,6 +571,12 @@ export interface PhysicsStepContext {
569
571
  * names for that. Absent on every walk but the looping one.
570
572
  */
571
573
  loop?: boolean;
574
+ /** A plant only (issue #1049, `sliderPhysicsLastsOnePass: false`): what the sliders wrote on the step before, by constraint and field. */
575
+ carried?: Map<string, Partial<Record<'inertia' | 'strength' | 'damping' | 'massInverse' | 'wind' | 'gravity' | 'mix', number>>>;
576
+ /** Each `constraint/field` the step's own animation keys — what the planted carried write gives way to. */
577
+ keyedNow?: Set<string>;
578
+ /** A plant only (issue #1049, `sliderPhysicsFromCurrent: false`): each physics record's setup values, before the step's own animation. */
579
+ setupRecords?: Map<string, CorePhysicsRecord>;
572
580
  }
573
581
 
574
582
  /** A constraint's state in `ctx`, created fresh on its first update. */
@@ -813,6 +821,8 @@ export function stepPhysicsRecords(records: readonly CoreConstraintRecord[], key
813
821
  const physics = records.filter((r): r is CorePhysicsRecord => r.kind === 'physics');
814
822
  const posed = posedPhysics(physics, keyed, t, ctx.last, (r) => physicsActive(r, active), {}, ctx.loop === true && t < ctx.last);
815
823
  ctx.last = t;
824
+ ctx.setupRecords = new Map(physics.map((r) => [r.name, r]));
825
+ ctx.keyedNow = new Set(keyed.flatMap((tl) => (tl.kind === 'reset' ? [] : (tl.name === EVERY_GLOBAL_PHYSICS ? unnamedPhysicsTargets(physics, tl.kind, (r) => physicsActive(r, active)).map((r) => r.name) : [tl.name]).map((n) => `${n}/${tl.kind === 'mass' ? 'massInverse' : tl.kind}`))));
816
826
  for (const name of posed.reset) resetPhysicsState(physicsState(ctx, name), before);
817
827
  return records.map((r) => (r.kind === 'physics' ? (posed.records.get(r.name) as CorePhysicsRecord) : r));
818
828
  }
@@ -137,9 +137,52 @@
137
137
  * animation deforms a curve a path constraint walks (unmeasured). A slider's
138
138
  * deform, sequence and draw-order keys are posed since issue #955, after the
139
139
  * sample's own (`./deform.ts`, `./draw_order.ts`; they were `sliderAttachmentsWhy`'s
140
- * absence before). Physics timelines under `Physics.none` pose nothing
141
- * (`./constraints_physics.ts`) and events a slider does not fire, so neither
142
- * leaves anything out.
140
+ * absence before). Its physics timelines are posed since issue #1049 (below),
141
+ * and events a slider does not fire, so neither leaves anything out.
142
+ *
143
+ * ## Its physics timelines (issue #1049)
144
+ *
145
+ * Under `Physics.none` a physics constraint applies nothing
146
+ * (`./constraints_physics.ts`), so neither do a slider's physics keys. Under
147
+ * the step they do, and until issue #1049 the core applied none of them: on
148
+ * the selftest's own builds where a dial's animation keys `wind` or
149
+ * `gravity` of a constraint declared after it, every bone the constraint
150
+ * drives left spine-core from the third step (`tip` worldX
151
+ * 14.87037037037037 against 12.092592592592593 at 0.025 s), and with a slot
152
+ * on that bone `render` drew wrong frames with no refusal. Measured against
153
+ * spine-core 4.3.13's output only — A10's looping walk, `render`'s recipe at
154
+ * 60 and 12 fps and the oracle's stepped grid under `--raw`, on 400 compiled
155
+ * rigs of one or two sliders and one or two physics constraints in random
156
+ * order, every kind keyed, at tolerance 0 — the rule is:
157
+ *
158
+ * - **A slider writes its physics keys into the pass's physics records, at
159
+ * its place in the update order**, at the time it applies its animation at
160
+ * (`at`), after the step's own animation posed them; a physics constraint
161
+ * after it steps with what it wrote, one before it has already stepped.
162
+ * Applying the writes to every constraint of the pass whatever its place
163
+ * read 239 of the 400 rigs off; not applying them, 205.
164
+ * - **A write lasts one pass.** The next step's records start again from the
165
+ * step's own animation: a write that stood where the step's animation does
166
+ * not key read 323 of 400 off.
167
+ * - **Before the timeline's first key it writes nothing**, as for bones.
168
+ * - **`wind`, `gravity` under `additive`: `current + v·mix`.** Every other
169
+ * value (`inertia`, `strength`, `damping`, `mass`, `mix`), and every value
170
+ * of a slider that is not additive: `current + (v − current)·mix`, with
171
+ * `current` the value as the pass holds it (the step's own animation and
172
+ * any slider before). Adding every kind read 104 of 400 off; blending from
173
+ * the setup value (before the step's own animation), 80; writing the key
174
+ * itself at mix exactly 1, 20 — in the last bit.
175
+ * - **A `mass` key blends the mass**, `1 / (m + (v − m)·mix)` with `m =
176
+ * 1 / massInverse`; blending the inverse read 28 of 400 off.
177
+ * - **A `reset` key fires nothing** — the slider applies its animation at one
178
+ * time, from that time, so no key is crossed (the path-slider suite's
179
+ * `PS159` measured the same on the pose); firing a key at or before `at` on
180
+ * every pass read 112 of 400 off.
181
+ * - The timeline naming no constraint (`*`) writes every active constraint
182
+ * whose `…Global` flag for that value is on, as a step's own does.
183
+ *
184
+ * `CO31` holds the rule on the `core_gate` probe rows and a seeded population
185
+ * on all three entries; `CO32` plants each rejected reading (`SolverRules`).
143
186
  *
144
187
  * ## Purity
145
188
  *
@@ -148,8 +191,9 @@
148
191
  */
149
192
  import type { ModelBone } from '../model.ts';
150
193
  import { channelAt, keyIndexAt, type CoreAnimationTimelines, type CoreCurve, type CoreKey } from './animation.ts';
151
- import { sourceValue, TRANSFORM_PROPERTIES, type SolverState, type TransformProperty } from './constraints.ts';
194
+ import { sourceValue, TRANSFORM_PROPERTIES, type CoreConstraintRecord, type SolverRules, type SolverState, type TransformProperty } from './constraints.ts';
152
195
  import type { CompiledDocument, CoreAnimation } from './index.ts';
196
+ import { EVERY_GLOBAL_PHYSICS, physicsActive, physicsState, resetPhysicsState, unnamedPhysicsTargets, type CorePhysicsRecord, type CorePhysicsTimeline, type PhysicsStepContext } from './constraints_physics.ts';
153
197
 
154
198
  /** The fields a slider's record may carry after `kind`, `name`, `declaredIn` (`buildRigConstraint` in `src/compile.ts`). */
155
199
  export const SLIDER_FIELDS = ['animation', 'additive', 'loop', 'mix', 'bone', 'property', 'from', 'to', 'scale', 'max', 'local', 'time', 'skin'] as const;
@@ -160,6 +204,8 @@ export interface CoreSliderRecord {
160
204
  name: string;
161
205
  animation: string;
162
206
  timelines: CoreAnimationTimelines;
207
+ /** Its animation's physics timelines, in the animation's order (issue #1049) — set by `readModel` once the animations' constraint timelines are read, which is after the records. */
208
+ physics: readonly CorePhysicsTimeline[];
163
209
  /** The setup bones its animation keys, by name — what a non-additive key's value is added to. */
164
210
  setup: ReadonlyMap<string, ModelBone>;
165
211
  additive: boolean;
@@ -252,7 +298,7 @@ export function readSliderRecord(raw: Record<string, unknown>, name: string, whe
252
298
  if (b !== undefined) setup.set(b.name, b);
253
299
  }
254
300
  const record: CoreSliderRecord = {
255
- kind: 'slider', name, animation: anim?.name ?? '', timelines: anim?.timelines ?? { declared: 0, duration: 0, bones: [], slots: [], later: [], attachments: [], drawOrder: [], events: [] }, setup,
301
+ kind: 'slider', name, animation: anim?.name ?? '', timelines: anim?.timelines ?? { declared: 0, duration: 0, bones: [], slots: [], later: [], attachments: [], drawOrder: [], events: [] }, physics: [], setup,
256
302
  additive: flag('additive'), loop, mix: num('mix', 1), time: num('time', 0), bone, property,
257
303
  from: num('from', 0), to: num('to', 0), scale: num('scale', 1), local: flag('local'), skin: flag('skin'), listedBySkin: false,
258
304
  };
@@ -380,13 +426,14 @@ function valuesAt(keys: readonly CoreKey[], t: number): number[] | null {
380
426
  * loop to pose again with everything below them. The application is pushed
381
427
  * onto `applied` for the slots.
382
428
  */
383
- export function applySlider(state: SolverState, r: CoreSliderRecord, applied?: SliderApplication[]): string[] {
429
+ export function applySlider(state: SolverState, r: CoreSliderRecord, applied?: SliderApplication[], physics?: SliderPhysicsTarget): string[] {
384
430
  if (r.mix === 0) return [];
385
431
  const time = sliderTime(state, r);
386
432
  const d = r.timelines.duration;
387
433
  const at = r.loop && d !== 0 ? time % d : time;
388
434
  applied?.push({ name: r.name, timelines: r.timelines, at, alpha: r.mix, additive: r.additive, time });
389
435
  const alpha = r.mix;
436
+ if (physics !== undefined && state.rules.sliderWritesPhysics) applySliderPhysics(r, at, alpha, physics);
390
437
  const changed: string[] = [];
391
438
  for (const target of r.timelines.bones) {
392
439
  const index = state.index.get(target.name);
@@ -445,6 +492,89 @@ export function applySlider(state: SolverState, r: CoreSliderRecord, applied?: S
445
492
  return changed;
446
493
  }
447
494
 
495
+ /** The physics records a stepped pass carries, by name — what a slider's physics timelines write and a later physics constraint steps with (issue #1049, the header's *Its physics timelines*). */
496
+ export interface SliderPhysicsTarget {
497
+ records: Map<string, CorePhysicsRecord>;
498
+ /** Each record's setup values, before the step's own animation — what a planted blend from the setup reads. */
499
+ setup: ReadonlyMap<string, CorePhysicsRecord>;
500
+ active: (r: CorePhysicsRecord) => boolean;
501
+ rules: Readonly<SolverRules>;
502
+ ctx: PhysicsStepContext;
503
+ /** What the sliders wrote this pass, by constraint and field — kept only under the planted reading that a write outlasts its pass. */
504
+ written: Map<string, Partial<Record<PhysicsField, number>>>;
505
+ }
506
+
507
+ /** The fields of a physics record a timeline writes. */
508
+ type PhysicsField = 'inertia' | 'strength' | 'damping' | 'massInverse' | 'wind' | 'gravity' | 'mix';
509
+
510
+ /**
511
+ * The physics records of one stepped pass, ready for the sliders to write
512
+ * (issue #1049): every physics record as the step's own animation posed it.
513
+ * Under the planted reading that a slider's write outlasts its pass, what
514
+ * the sliders wrote on the step before stands wherever the step's own
515
+ * animation does not key it (`PhysicsStepContext.keyedNow`).
516
+ */
517
+ export function sliderPhysicsTarget(records: readonly CoreConstraintRecord[], active: ReadonlySet<string>, ctx: PhysicsStepContext, rules: Readonly<SolverRules>): SliderPhysicsTarget {
518
+ const posed = new Map(records.flatMap((r): Array<[string, CorePhysicsRecord]> => (r.kind === 'physics' ? [[r.name, r]] : [])));
519
+ const out = new Map(posed);
520
+ const written = new Map<string, Partial<Record<PhysicsField, number>>>();
521
+ if (!rules.sliderPhysicsLastsOnePass) {
522
+ for (const [name, fields] of ctx.carried ?? new Map<string, Partial<Record<PhysicsField, number>>>()) {
523
+ const r = out.get(name);
524
+ if (r === undefined) continue;
525
+ const next = { ...r };
526
+ const kept: Partial<Record<PhysicsField, number>> = {};
527
+ for (const [f, v] of Object.entries(fields) as Array<[PhysicsField, number]>) {
528
+ if (ctx.keyedNow?.has(`${name}/${f}`)) continue;
529
+ next[f] = v;
530
+ kept[f] = v;
531
+ }
532
+ out.set(name, next);
533
+ written.set(name, kept);
534
+ }
535
+ ctx.carried = written;
536
+ }
537
+ return { records: out, setup: ctx.setupRecords ?? posed, active: (r) => physicsActive(r, active), rules, ctx, written };
538
+ }
539
+
540
+ /** The value kinds a slider's physics timeline adds under `additive` (`./additive.ts`'s table); the rest write. */
541
+ const PHYSICS_ADDS: ReadonlySet<string> = new Set(['wind', 'gravity']);
542
+
543
+ /** A slider's physics timelines applied at `at` with alpha `alpha` onto the pass's physics records (the header's *Its physics timelines*). */
544
+ function applySliderPhysics(r: CoreSliderRecord, at: number, alpha: number, target: SliderPhysicsTarget): void {
545
+ const rules = target.rules;
546
+ for (const tl of r.physics) {
547
+ const named = target.records.get(tl.name);
548
+ const targets = tl.name === EVERY_GLOBAL_PHYSICS ? unnamedPhysicsTargets([...target.records.values()], tl.kind, target.active) : named !== undefined && target.active(named) ? [named] : [];
549
+ if (tl.kind === 'reset') {
550
+ // A slider applies its animation at one time, from that time: no key is crossed, so a reset key fires nothing (PS159).
551
+ if (!rules.sliderPhysicsResetIsDead && tl.keys.some((k) => k.time <= at)) for (const p of targets) resetPhysicsState(physicsState(target.ctx, p.name), target.ctx.time);
552
+ continue;
553
+ }
554
+ const i = keyIndexAt(tl.keys, at);
555
+ if (i < 0) continue;
556
+ const v = channelAt(tl.keys, i, 0, at);
557
+ const adds = r.additive && (PHYSICS_ADDS.has(tl.kind) || !rules.sliderPhysicsAddsWindGravityOnly);
558
+ for (const p of targets) {
559
+ const from = rules.sliderPhysicsFromCurrent ? p : (target.setup.get(p.name) as CorePhysicsRecord);
560
+ const next = { ...p };
561
+ let field: PhysicsField;
562
+ if (tl.kind === 'mass') {
563
+ field = 'massInverse';
564
+ if (rules.sliderPhysicsBlendsMass) {
565
+ const m = 1 / from.massInverse;
566
+ next.massInverse = 1 / (adds ? 1 / p.massInverse + v * alpha : m + (v - m) * alpha);
567
+ } else next.massInverse = adds ? p.massInverse + (1 / v) * alpha : from.massInverse + (1 / v - from.massInverse) * alpha;
568
+ } else {
569
+ field = tl.kind;
570
+ next[field] = adds ? p[field] + v * alpha : from[field] + (v - from[field]) * alpha;
571
+ }
572
+ target.records.set(p.name, next);
573
+ if (!rules.sliderPhysicsLastsOnePass) target.written.set(p.name, { ...target.written.get(p.name), [field]: next[field] });
574
+ }
575
+ }
576
+ }
577
+
448
578
  /** A slot's pose as the sliders move it: what it shows, its light colour and its dark colour (`null` when it states none). */
449
579
  export interface SlotPoseState {
450
580
  placeholder: string | null;
package/src/core/index.ts CHANGED
@@ -352,9 +352,10 @@ export interface CompiledDocument {
352
352
  /**
353
353
  * The skin the document is posed under (issue #932, `./skins.ts`): `all` —
354
354
  * every skin merged, the oracle's `--skin all` and `readModel`'s reading —
355
- * or one skin's name, `underSkin`'s.
355
+ * one skin's name, `underSkin`'s, or `null` — no skin set, the runtime's
356
+ * `Skeleton.skin` before `setSkin` is called (`underNoSkin`, issue #1051).
356
357
  */
357
- skin: string;
358
+ skin: string | null;
358
359
  /** The skeleton's reference scale (issue #958), which every physics record carries too (`CorePhysicsRecord.referenceScale`). */
359
360
  referenceScale: number;
360
361
  bones: ModelBone[];
@@ -390,7 +391,7 @@ export interface CoreStated {
390
391
  /**
391
392
  * The `pages` section, checked: a list of pages, each holding exactly
392
393
  * `CORE_PAGE_FIELDS` — a non-empty name, a size of two positive finite
393
- * numbers, a list of regions — and each region exactly
394
+ * numbers, a list of at least one region (issue #1054) — and each region exactly
394
395
  * `CORE_PAGE_REGION_FIELDS`, a non-empty name and nine finite numbers. Every
395
396
  * fault is named by its path. Nothing is defaulted: a field the writer always
396
397
  * writes is required here.
@@ -422,6 +423,10 @@ function readPages(value: unknown, problems: string[], flags: boolean): ModelPag
422
423
  }
423
424
  const regions: ModelPageRegion[] = [];
424
425
  if (!Array.isArray(raw.regions)) problems.push(`${label}: regions is not a list`);
426
+ // Issue #1054: a page holding no region is one no build writes — the atlas text's block for it would end before any region line, which
427
+ // the round trip refuses (A07_ATLAS_TEXT_SHAPE: "the last page block declares no region") — so a document stating one is refused here by
428
+ // the page's name rather than read, where nothing on the model side would name it.
429
+ else if (raw.regions.length === 0) problems.push(`${label}: regions is an empty list — a page holds at least one region; the atlas text cannot state a page with none (its block would end before a region line, which A07_ATLAS_TEXT_SHAPE refuses), so no build writes one`);
425
430
  else {
426
431
  raw.regions.forEach((region, j) => {
427
432
  const at = `${label}.regions[${j}]`;
@@ -880,6 +885,11 @@ export function readModel(text: string, where = 'the model document'): CompiledD
880
885
  if (isRecord(raw) && animations[i] !== undefined) animations[i].constraints = readConstraintTimelines(raw.constraints, `animations[${i}] "${animations[i].name}"`, constraints, problems);
881
886
  });
882
887
  }
888
+ // A slider's physics timelines are its animation's (issue #1049), read only now.
889
+ for (const c of constraints) {
890
+ const r = c.record;
891
+ if (r?.kind === 'slider') r.physics = animations.find((a) => a.name === r.animation)?.constraints.physicsKeyed ?? [];
892
+ }
883
893
  const editorOrder = spec === CORE_DOCUMENT_SPEC && 'editorOrder' in value ? readEditorOrder(value.editorOrder, skins, animations, problems) : null;
884
894
  if (problems.length > 0) throw new CoreInputError(`${where}: ${problems.length} problem(s): ${problems.join('; ')}`);
885
895
  const stated: CoreStated | null = editorOrder === null ? null : { stage, editorOrder };
@@ -904,6 +914,20 @@ export function underSkin(doc: CompiledDocument, name: string): CompiledDocument
904
914
  return view;
905
915
  }
906
916
 
917
+ /**
918
+ * The document posed with no skin set (issue #1051): no skin's `bones` or
919
+ * constraint lists applied — the default skin's included — and every slot
920
+ * resolved through the default skin alone, or through nothing where the
921
+ * document declares none (`./skins.ts`, *No skin set*, for the measurement).
922
+ * The view a fresh skeleton is in: `render` without `--skin`, A10's walk and
923
+ * `validate()`'s poses (`noSkinView` in `src/render_core.ts`).
924
+ */
925
+ export function underNoSkin(doc: CompiledDocument): CompiledDocument {
926
+ const view: CompiledDocument = { ...doc, skin: null, constraints: doc.constraints.map((c) => (c.record === undefined ? c : { ...c, record: { ...c.record } as CoreConstraintRecord })) };
927
+ resolveSkinView(view);
928
+ return view;
929
+ }
930
+
907
931
  /**
908
932
  * What a skin view decides on the constraint records: whether an applied
909
933
  * skin's list names each (`listedBySkin`, `./skins.ts`), and what each path
package/src/core/skins.ts CHANGED
@@ -54,6 +54,30 @@
54
54
  * not what the slot shows; under `all` every bone some skin names is
55
55
  * active, so a one-skin rig is unchanged.
56
56
  *
57
+ * - **No skin set** (issue #1051): a fresh skeleton whose `setSkin` was never
58
+ * called — `underNoSkin`, the state `render` without `--skin`, A10's walk
59
+ * and `validate()` pose in. No skin's `bones` or constraint lists are
60
+ * applied, the default skin's included, and a slot shows the default
61
+ * skin's record for its placeholder, else nothing — so over a document
62
+ * with skins and no `default` one every slot shows nothing. Measured with
63
+ * `tools/pose_oracle.ts dump --skin none` on 93 skeletons rebuilt through
64
+ * `ingest` and `compile` (31 with no default skin, 31 whose default names
65
+ * skin-required members, 31 with a plain default): `Skeleton.skin` read
66
+ * `null` on all 93; all 311 skin-required bones read inactive — the 51 a
67
+ * default skin names among them, and the 66 a constraint writes — and the
68
+ * 125 bones that are not skin-required under one read active and unposed;
69
+ * all 73 skin-required constraints read inactive and moved nothing when
70
+ * removed — the 12 a default skin lists among them — and the 81 that are
71
+ * not read active; 199 slots whose placeholder the default skin fills
72
+ * showed it and 359 only named skins fill showed nothing; a colour key on
73
+ * a slot whose bone is inactive held its setup colour (50 of 50) and moved
74
+ * it on an active one (46 of 46), and a draw-order key applied either way
75
+ * (93 of 93). The core suite's `CO29` holds the same population against
76
+ * the core under `--raw` at tolerance 0, and `CO30` plants each rejected
77
+ * reading (the old one among them: no skin set read as the default skin,
78
+ * which differs exactly where the default skin names a skin-required
79
+ * member).
80
+ *
57
81
  * Under `all` every skin is applied at once: `bones` and the constraint
58
82
  * lists of every skin count, and a placeholder several skins fill shows the
59
83
  * LAST of them in the Spine file's order — the case `./index.ts` leaves out
@@ -68,15 +92,16 @@ export const CORE_ALL_SKINS = 'all';
68
92
  /** The skin a document names its default skin with — Spine's `SkeletonData.defaultSkin` is the skin of this name. */
69
93
  export const CORE_DEFAULT_SKIN = 'default';
70
94
 
71
- /** The skins whose `bones` and constraint lists are applied: every skin under `all`, the named skin alone otherwise (the header's measurement). */
95
+ /** The skins whose `bones` and constraint lists are applied: every skin under `all`, none with no skin set, the named skin alone otherwise (the header's measurements). */
72
96
  export function appliedSkins(doc: CompiledDocument): CoreSkin[] {
97
+ if (doc.skin === null) return [];
73
98
  return doc.skin === CORE_ALL_SKINS ? doc.skins : doc.skins.filter((k) => k.name === doc.skin);
74
99
  }
75
100
 
76
- /** Under a named skin, the skins a placeholder is looked up in, in precedence order: the named skin, then the default skin (the header's measurement). */
101
+ /** Under a named skin or none, the skins a placeholder is looked up in, in precedence order: the named skin, then the default skin (the header's measurements). */
77
102
  export function lookupSkins(doc: CompiledDocument): CoreSkin[] {
78
103
  if (doc.skin === CORE_ALL_SKINS) return doc.skins;
79
- const named = doc.skins.find((k) => k.name === doc.skin);
104
+ const named = doc.skin === null ? undefined : doc.skins.find((k) => k.name === doc.skin);
80
105
  const fallback = doc.skin === CORE_DEFAULT_SKIN ? undefined : doc.skins.find((k) => k.name === CORE_DEFAULT_SKIN);
81
106
  return [named, fallback].filter((k): k is CoreSkin => k !== undefined);
82
107
  }