spine-rigc 1.9.0 → 1.9.2

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 (62) hide show
  1. package/cli.ts +48 -17
  2. package/docs/AUTHORING.md +2 -2
  3. package/package.json +1 -1
  4. package/src/assertions/bodies/a09.ts +82 -0
  5. package/src/assertions/bodies/a10.ts +116 -0
  6. package/src/assertions/bodies/a12.ts +30 -0
  7. package/src/assertions/bodies/a24.ts +68 -0
  8. package/src/assertions/bodies/a25.ts +39 -0
  9. package/src/assertions/bodies/a26.ts +61 -0
  10. package/src/assertions/bodies/a29.ts +34 -0
  11. package/src/assertions/bodies/a30.ts +50 -0
  12. package/src/assertions/bodies/a32.ts +44 -0
  13. package/src/assertions/bodies/a34.ts +133 -0
  14. package/src/assertions/bodies/a39.ts +303 -0
  15. package/src/assertions/bodies/a40.ts +128 -0
  16. package/src/assertions/bodies/a43.ts +181 -0
  17. package/src/assertions/bodies/a46.ts +224 -0
  18. package/src/assertions/facts/animation_durations.ts +37 -0
  19. package/src/assertions/facts/bone_timelines.ts +37 -0
  20. package/src/assertions/facts/constraint_targets.ts +56 -0
  21. package/src/assertions/facts/deform_survey.ts +27 -0
  22. package/src/assertions/facts/event_keys.ts +55 -0
  23. package/src/assertions/facts/sequences.ts +85 -0
  24. package/src/assertions/facts/skeleton_roster.ts +45 -0
  25. package/src/assertions/facts/slider_composition.ts +78 -0
  26. package/src/assertions/facts/stepped_poses.ts +74 -0
  27. package/src/assertions/facts/two_colour.ts +52 -0
  28. package/src/assertions/inward_advance.ts +58 -0
  29. package/src/assertions/model/animation_durations.ts +57 -0
  30. package/src/assertions/model/bone_timelines.ts +56 -0
  31. package/src/assertions/model/constraint_targets.ts +82 -0
  32. package/src/assertions/model/constraints.ts +34 -16
  33. package/src/assertions/model/deform_survey.ts +24 -0
  34. package/src/assertions/model/event_keys.ts +45 -0
  35. package/src/assertions/model/index.ts +83 -2
  36. package/src/assertions/model/linked_meshes.ts +2 -2
  37. package/src/assertions/model/mesh_attachments.ts +1 -1
  38. package/src/assertions/model/region_joins.ts +20 -12
  39. package/src/assertions/model/sequences.ts +157 -0
  40. package/src/assertions/model/skeleton_roster.ts +23 -0
  41. package/src/assertions/model/skin_entries.ts +18 -8
  42. package/src/assertions/model/skin_members.ts +5 -3
  43. package/src/assertions/model/skin_meshes.ts +1 -1
  44. package/src/assertions/model/slider_composition.ts +191 -0
  45. package/src/assertions/model/slot_colour.ts +5 -4
  46. package/src/assertions/model/stepped_poses.ts +89 -0
  47. package/src/assertions/model/two_colour.ts +61 -0
  48. package/src/assertions/model/vertex_polygons.ts +9 -8
  49. package/src/bonedist.ts +18 -6
  50. package/src/compile.ts +114 -14
  51. package/src/core/additive.ts +468 -0
  52. package/src/core/constraints.ts +12 -2
  53. package/src/core/constraints_physics.ts +31 -8
  54. package/src/core/raw.ts +77 -16
  55. package/src/core/walk.ts +202 -0
  56. package/src/deformmeasure.ts +35 -2270
  57. package/src/deformstructure.ts +60 -18
  58. package/src/deformsurvey.ts +2295 -0
  59. package/src/nonfinite.ts +54 -0
  60. package/src/render.ts +200 -59
  61. package/src/render_core.ts +5 -0
  62. package/src/validate.ts +791 -1558
@@ -8,7 +8,9 @@
8
8
  * - **Constraints**: the document's `constraints` in order — update order, the
9
9
  * order the emitter writes them in — each skin-required exactly when it
10
10
  * states `skin: true`, the field the emitter writes through unchanged.
11
- * - **Skins**: in the emitter's order (`editorSkinOrder`, `src/compile.ts`); a
11
+ * - **Skins**: in the file's order (`fileSkinOrder`, `src/compile.ts`: a
12
+ * `rigc-compiled/3` document's `editorOrder`, else the emitter's
13
+ * `editorSkinOrder`, issue #1034); a
12
14
  * skin's bones as it lists them, each the FIRST bone of that name; its
13
15
  * constraints kind by kind in the emitter's key order
14
16
  * (`RIG_SKIN_CONSTRAINT_KEYS`, which `emitSkins` walks), each the first
@@ -20,7 +22,7 @@
20
22
  *
21
23
  * Links nothing from the runtime.
22
24
  */
23
- import { editorSkinOrder } from '../../compile.ts';
25
+ import { fileSkinOrder, skinsInFileOrder } from '../../compile.ts';
24
26
  import { RIG_SKIN_CONSTRAINT_KEYS } from '../../rig.ts';
25
27
  import type { MemberBone, MemberConstraint, MemberSkin, SkinMemberFacts } from '../facts/skin_members.ts';
26
28
  import { isObj } from '../values.ts';
@@ -42,7 +44,7 @@ export function modelSkinMembers(read: ReadDocument): SkinMemberFacts {
42
44
  for (const constraint of stated) {
43
45
  constraints.push({ name: String(constraint.name), kind: String(constraint.kind), skinRequired: constraint.skin === true });
44
46
  }
45
- const skins: MemberSkin[] = editorSkinOrder(doc.skins).map((skin) => {
47
+ const skins: MemberSkin[] = skinsInFileOrder(doc.skins, fileSkinOrder(doc)).map(({ skin }) => {
46
48
  const listedBones = skin.bones.map((name) => {
47
49
  const found = boneByName.get(name);
48
50
  if (found === undefined) throw new Error(`skin "${skin.name}" lists bone "${name}", which is not a bone of the document`);
@@ -4,7 +4,7 @@
4
4
  * file's walk order.
5
5
  *
6
6
  * - **The order** is `fileOrderedEntries`'s (`./skin_entries.ts`): the skins by
7
- * the emitter's `editorSkinOrder`, the slots in the document's slot order, a
7
+ * the file's order (`fileSkinOrder`, issue #1034), the slots in the document's slot order, a
8
8
  * slot's records in its table's order — the walk the selftest measures equal
9
9
  * to spine-core's `getAttachments()`.
10
10
  * - **A linked mesh's geometry is its source's**, read through the core's own
@@ -0,0 +1,191 @@
1
+ /**
2
+ * The model side's supply of `SliderCompositionFacts` (issue #1025, cut 4c-5
3
+ * of step 4c of #380): the document's sliders in its `constraints` order, the
4
+ * timelines of each animation they apply in the order the runtime builds
5
+ * them, and what each does when applied additively, as the core computes it.
6
+ *
7
+ * - **A slider's fields** are the core's record (`readSliderRecord`): `mix`,
8
+ * `additive` and `skin` read at the parser's value where the document
9
+ * leaves them out. The skins naming it are the document's skins in the
10
+ * file's order (`fileSkinOrder`), each whose `slider` list names it.
11
+ * - **The timelines, in the runtime's order** (`../facts/slider_composition.ts`'
12
+ * ⭐): the slot and bone timelines as the file states them (`fileSlotTimelines`,
13
+ * `fileBoneTimelines` — the emitter's own passes over the document's
14
+ * order), the constraint timelines in the order the runtime builds them
15
+ * (`modelConstraintTimelineSources`), the attachments' deform and sequence
16
+ * timelines at their three levels in the order the file keys them
17
+ * (`keyedOrder` over the document's order, deform before sequence —
18
+ * `emitAttachmentTimelines`' order), then the draw order and the events.
19
+ * - **The words.** Each timeline's class name and properties are the core's
20
+ * table (`ADDITIVE_APPLY`, `src/core/additive.ts`), the one value here that
21
+ * is not the document's, measured off the loaded timelines; an id's index is
22
+ * the target's position in the document's bones, slots or constraints —
23
+ * which the emitter writes in that order — `-1` for a physics timeline
24
+ * naming none; a deform's attachment is named as the runtime names it, the
25
+ * record's `name`, else its placeholder.
26
+ * - **The behaviour** is the core's probe over the document under no skin set
27
+ * (`noSkinView`, which refuses by name a document whose skins it was not
28
+ * measured over) and under each skin (`underSkin`): `additiveCells`, and the
29
+ * class `additiveBehaviour` reads off them. The core suite's `CO27` holds
30
+ * both to the runtime's probe; the selftest asks this supplier every
31
+ * question the body asks the runtime's (`VF14`).
32
+ *
33
+ * Links nothing from the runtime.
34
+ */
35
+ import { fileSkinOrder, keyedOrder } from '../../compile.ts';
36
+ import { ADDITIVE_APPLY, additiveBehaviour, additiveCells, additiveSpelling, type AdditiveTimeline, type AdditiveView } from '../../core/additive.ts';
37
+ import { EVERY_GLOBAL_PHYSICS, type PhysicsTimelineKind } from '../../core/constraints_physics.ts';
38
+ import { underSkin, type CompiledDocument } from '../../core/index.ts';
39
+ import type { BoneTimelineKind, SlotTimelineKind } from '../../core/animation.ts';
40
+ import { noSkinView } from '../../render_core.ts';
41
+ import { entryAddress } from '../facts/sequences.ts';
42
+ import type { SliderCompositionFacts, SliderFact, SliderTimelineFact } from '../facts/slider_composition.ts';
43
+ import { modelConstraintTimelineSources } from './constraints.ts';
44
+ import { fileBoneTimelines } from './bone_timelines.ts';
45
+ import { fileSlotTimelines } from './slot_colour.ts';
46
+ import type { ReadDocument } from './parse.ts';
47
+ import { isObj } from '../values.ts';
48
+
49
+ /** One timeline of an animation: the facts' reading of it and the core's. */
50
+ export interface ModelSliderTimeline {
51
+ fact: SliderTimelineFact;
52
+ core: AdditiveTimeline;
53
+ }
54
+
55
+ /** Names in the order each first appears, put in the order a JSON object keyed by them lists them (`keyedOrder`). */
56
+ const keyedFirsts = (names: readonly string[]): string[] => keyedOrder(names.filter((name, i) => names.indexOf(name) === i));
57
+
58
+ /** The words of one timeline: the table's class and properties, each id addressed by `index` (and `attachment`), and the sentence's naming. */
59
+ function factOf(doc: CompiledDocument, core: AdditiveTimeline): SliderTimelineFact {
60
+ const row = ADDITIVE_APPLY[additiveSpelling(core)];
61
+ if (row === undefined) throw new Error(`internal: "${additiveSpelling(core)}" is no row of the additive table`);
62
+ const property = (p: string, index: number | null, attachment: string | null, names: string) => ({
63
+ id: [p, ...(index === null ? [] : [String(index)]), ...(attachment === null ? [] : [attachment])].join('|'),
64
+ property: p,
65
+ names,
66
+ });
67
+ const slotIndex = (name: string): number => doc.slots.findIndex((s) => s.name === name);
68
+ let properties: SliderTimelineFact['properties'];
69
+ switch (core.group) {
70
+ case 'bone': {
71
+ const index = doc.bones.findIndex((b) => b.name === core.bone);
72
+ properties = row.properties.map((p) => property(p, index, null, `bone "${core.bone}" ${p}`));
73
+ break;
74
+ }
75
+ case 'slot':
76
+ properties = row.properties.map((p) => property(p, slotIndex(core.slot), null, `slot "${core.slot}" ${p}`));
77
+ break;
78
+ case 'attachment': {
79
+ const record = doc.skins.find((k) => k.name === core.skin)?.attachments[core.slot]?.[core.attachment];
80
+ const shown = record?.name ?? core.attachment;
81
+ const address = entryAddress(core.skin, core.slot, core.attachment);
82
+ properties = row.properties.map((p) => property(p, slotIndex(core.slot), address, core.word === 'deform' ? `slot "${core.slot}" deform of "${shown}"` : `slot "${core.slot}" ${p}`));
83
+ break;
84
+ }
85
+ case 'drawOrder':
86
+ case 'events':
87
+ properties = row.properties.map((p) => property(p, null, null, `the skeleton's ${p}`));
88
+ break;
89
+ default: {
90
+ const index = core.group === 'physics' && core.constraint === EVERY_GLOBAL_PHYSICS ? -1 : doc.constraints.findIndex((c) => c.kind === core.group && c.name === core.constraint);
91
+ properties = row.properties.map((p) => property(p, row.address === 'none' ? null : index, null, index >= 0 ? `constraint "${core.constraint}" ${p}` : `the skeleton's ${p}`));
92
+ }
93
+ }
94
+ return { runtimeClass: row.runtimeClass, properties };
95
+ }
96
+
97
+ /** Every timeline of animation `name`, in the order the runtime builds them (the header). */
98
+ export function modelAnimationTimelines(read: ReadDocument, name: string): ModelSliderTimeline[] {
99
+ const { doc } = read;
100
+ const anim = doc.animations.find((a) => a.name === name);
101
+ if (anim === undefined) return [];
102
+ const t = anim.timelines;
103
+ const out: AdditiveTimeline[] = [];
104
+ for (const { slot, timelines } of fileSlotTimelines(read).filter((s) => s.animation === name)) {
105
+ const target = t.slots.find((s) => s.name === slot);
106
+ for (const word of Object.keys(timelines)) {
107
+ const keys = target?.timelines.find((x) => x.kind === word)?.keys;
108
+ if (keys !== undefined) out.push({ group: 'slot', word: word as SlotTimelineKind, slot, keys });
109
+ }
110
+ }
111
+ for (const { bone, timelines } of fileBoneTimelines(read).filter((b) => b.animation === name)) {
112
+ const target = t.bones.find((b) => b.name === bone);
113
+ for (const word of isObj(timelines) ? Object.keys(timelines) : []) {
114
+ const keys = target?.timelines.find((x) => x.kind === word)?.keys;
115
+ if (keys !== undefined) out.push({ group: 'bone', word: word as BoneTimelineKind, bone, keys });
116
+ }
117
+ }
118
+ for (const source of modelConstraintTimelineSources(read).filter((s) => s.animation === name)) {
119
+ switch (source.kind) {
120
+ case 'ik': {
121
+ const keys = anim.constraints.ik.find((x) => x.name === source.name)?.keys;
122
+ if (keys !== undefined) out.push({ group: 'ik', word: 'ik', constraint: source.name, keys });
123
+ break;
124
+ }
125
+ case 'transform':
126
+ out.push({ group: 'transform', word: 'transform', constraint: source.name, keys: source.keys });
127
+ break;
128
+ case 'path':
129
+ out.push({ group: 'path', word: source.word as 'position' | 'spacing' | 'mix', constraint: source.name, keys: source.keys });
130
+ break;
131
+ case 'physics':
132
+ out.push({ group: 'physics', word: source.word as PhysicsTimelineKind, constraint: source.name, keys: source.keys });
133
+ break;
134
+ case 'slider':
135
+ out.push({ group: 'slider', word: source.word as 'time' | 'mix', constraint: source.name, keys: source.keys });
136
+ break;
137
+ }
138
+ }
139
+ const attachments = t.attachments;
140
+ for (const skin of keyedFirsts(attachments.map((a) => a.skin))) {
141
+ const inSkin = attachments.filter((a) => a.skin === skin);
142
+ for (const slot of keyedFirsts(inSkin.map((a) => a.slot))) {
143
+ const inSlot = inSkin.filter((a) => a.slot === slot);
144
+ for (const attachment of keyedFirsts(inSlot.map((a) => a.attachment))) {
145
+ for (const a of inSlot.filter((x) => x.attachment === attachment)) {
146
+ if (a.deform !== null) out.push({ group: 'attachment', word: 'deform', skin, slot, attachment, keys: a.deform });
147
+ if (a.sequence !== null) out.push({ group: 'attachment', word: 'sequence', skin, slot, attachment, keys: a.sequence });
148
+ }
149
+ }
150
+ }
151
+ }
152
+ if (t.drawOrder.length > 0) out.push({ group: 'drawOrder', word: 'drawOrder', keys: t.drawOrder });
153
+ if (t.events.length > 0) out.push({ group: 'events', word: 'events', times: t.events.map((k) => k.time) });
154
+ return out.map((core) => ({ fact: factOf(doc, core), core }));
155
+ }
156
+
157
+ /** The views the probe poses under: no skin set, then every skin in the file's order. */
158
+ export function additiveViews(doc: CompiledDocument): AdditiveView[] {
159
+ return [{ name: '(none)', doc: noSkinView(doc) }, ...fileSkinOrder(doc).map((skin) => ({ name: skin.name, doc: underSkin(doc, skin.name) }))];
160
+ }
161
+
162
+ export function modelSliderComposition(read: ReadDocument): SliderCompositionFacts {
163
+ const { doc } = read;
164
+ const skinOrder = fileSkinOrder(doc).map((skin) => skin.name);
165
+ const walked = new Map<string, ModelSliderTimeline[]>();
166
+ const timelinesOf = (name: string): ModelSliderTimeline[] => {
167
+ const held = walked.get(name);
168
+ if (held !== undefined) return held;
169
+ const fresh = modelAnimationTimelines(read, name);
170
+ walked.set(name, fresh);
171
+ return fresh;
172
+ };
173
+ const sliders: SliderFact[] = [];
174
+ doc.constraints.forEach((c, index) => {
175
+ const r = c.record;
176
+ if (r?.kind !== 'slider') return;
177
+ const animation = doc.animations.some((a) => a.name === r.animation) ? { name: r.animation, timelines: timelinesOf(r.animation).map((x) => x.fact) } : null;
178
+ const skins = skinOrder.filter((name) => doc.skins.some((k) => k.name === name && k.constraints.slider.includes(r.name)));
179
+ sliders.push({ name: r.name, index, mix: r.mix, additive: r.additive, skinRequired: r.skin, skins, animation });
180
+ });
181
+ let views: AdditiveView[] | null = null;
182
+ return {
183
+ sliders,
184
+ behaviour: (animation, at) => {
185
+ const timeline = timelinesOf(animation)[at];
186
+ if (timeline === undefined) throw new Error(`internal: animation "${animation}" has no timeline ${at}`);
187
+ views ??= additiveViews(doc);
188
+ return additiveBehaviour(additiveCells(views, timeline.core));
189
+ },
190
+ };
191
+ }
@@ -5,8 +5,9 @@
5
5
  * holds each animation's slot timelines in the order they are applied, with
6
6
  * every key as the compiler built it; the Spine file keys the animations in
7
7
  * the editor's order and passes every key through two passes. So the
8
- * animations are put in order by `editorAnimationOrder` (`src/compile.ts`,
9
- * the function the emitter is handed), and the timelines are laid out as the
8
+ * animations are put in the file's order by `fileAnimationOrder`
9
+ * (`src/compile.ts`: a `rigc-compiled/3` document's `editorOrder`, else the
10
+ * emitter's own rule over its names, issue #1034), and the timelines are laid out as the
10
11
  * file lays them and handed to the emitter's own `withoutParserDefaults` and
11
12
  * `inEditorKeyOrder` (`src/keyorder.ts`) — so a key field the emitter would
12
13
  * leave out at the parser's default is left out here too, and a body
@@ -26,7 +27,7 @@
26
27
  *
27
28
  * Links nothing from the runtime.
28
29
  */
29
- import { editorAnimationOrder } from '../../compile.ts';
30
+ import { fileAnimationOrder } from '../../compile.ts';
30
31
  import type { CompiledDocument } from '../../core/index.ts';
31
32
  import { poseRawAnimation } from '../../core/raw.ts';
32
33
  import { inEditorKeyOrder, withoutParserDefaults } from '../../keyorder.ts';
@@ -42,7 +43,7 @@ export function fileSlotTimelines(read: ReadDocument): SlotTimelines[] {
42
43
  const byName = new Map<string, Json>();
43
44
  for (const anim of list(read.json.animations)) if (typeof anim.name === 'string') byName.set(anim.name, anim);
44
45
  const animations: Json = {};
45
- for (const name of editorAnimationOrder([...byName.keys()])) {
46
+ for (const name of fileAnimationOrder(read.doc)) {
46
47
  const slots: Json = {};
47
48
  for (const slot of list((byName.get(name) as Json).slots)) {
48
49
  if (typeof slot.name !== 'string') continue;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The model side's supply of `SteppedPoseFacts` (issue #1025, cut 4c-5a of
3
+ * step 4c of #380): every pose A10 reads, posed by rigc's core.
4
+ *
5
+ * - **The roster**: the document's bones, and its animations in the file's
6
+ * order (`fileAnimationOrder`, `src/compile.ts`), each with
7
+ * `CoreAnimationTimelines.duration` — the last key of every timeline as
8
+ * float32, the value `tools/pose_oracle.ts compare` holds equal to
9
+ * spine-core's on its `animations` roster.
10
+ * - **The setup pose and each animation's walk**: the core's looping walk
11
+ * (`src/core/walk.ts` — `poseWalkSetup`, `poseLoopingWalk`), which reads
12
+ * A10's recipe as measured and returns a value that is not finite in place
13
+ * rather than refusing it. It is held equal to spine-core's walk at
14
+ * tolerance 0 by `tools/core_gate.ts`'s walk block and the selftest's
15
+ * `CO25`, and the poses A10 asks for here are compared with spine-core's on
16
+ * every call with a model in hand (`VF13`).
17
+ * - **A bone's mode**: the mode the core's bone timelines pose at the time
18
+ * (`posedBones`, a fresh non-looping track: the time held at the duration),
19
+ * folded as the runtime's lookup folds it (`foldInheritMode`). `readModel`
20
+ * refuses a setup `inheritMode` and an `inherit` key that fold to no mode by
21
+ * name, so on this side every bone of every pose holds one and the answer is
22
+ * `null` — derived from the pose rather than assumed, so a reader that
23
+ * started admitting such a spelling would show up here as a bone with none.
24
+ * - **Under no skin set**, as `validate()` poses (`noSkinView`), which refuses
25
+ * by name a document whose default skin it cannot read that way.
26
+ *
27
+ * Links nothing from the runtime.
28
+ */
29
+ import { fileAnimationOrder } from '../../compile.ts';
30
+ import { foldInheritMode, type CompiledDocument } from '../../core/index.ts';
31
+ import { posedBones } from '../../core/animation.ts';
32
+ import { poseLoopingWalk, poseWalkSetup, type WalkPose } from '../../core/walk.ts';
33
+ import { noSkinView } from '../../render_core.ts';
34
+ import type { ModelBone } from '../../model.ts';
35
+ import type { SteppedFrame, SteppedPoseFacts } from '../facts/stepped_poses.ts';
36
+ import type { ReadDocument } from './parse.ts';
37
+
38
+ /** A bone's posed mode as the facts spell it: `null` for a mode, else the value held. */
39
+ const modeOf = (bone: ModelBone): string | null => (foldInheritMode(bone.inheritMode ?? 'normal') === null ? String(bone.inheritMode) : null);
40
+
41
+ /** A channel the walk computed; a slot row holds `null` only where a rounding wrote one, which the walk does not. */
42
+ const channel = (v: number | null): number => (v === null ? Number.NaN : v);
43
+
44
+ /** One walk pose as A10 reads it, each bone's mode taken from `posed` (the bones the timelines posed at the pose's time). */
45
+ function frameOf(pose: WalkPose, posed: readonly ModelBone[]): SteppedFrame {
46
+ const modes = new Map(posed.map((b) => [b.name, modeOf(b)]));
47
+ return {
48
+ bones: pose.bones.map((b) => ({ name: b.name, a: b.a, b: b.b, c: b.c, d: b.d, worldX: b.worldX, worldY: b.worldY, inherit: modes.get(b.name) ?? null })),
49
+ drawn: pose.drawn.map((d) => ({ slot: d.slot, attachment: d.attachment, vertices: d.vertices })),
50
+ slots: pose.slots.map((row) => ({
51
+ name: row[0],
52
+ colour: [channel(row[2]), channel(row[3]), channel(row[4]), channel(row[5])] as const,
53
+ dark: row[6] === null ? null : ([channel(row[6][0]), channel(row[6][1]), channel(row[6][2])] as const),
54
+ })),
55
+ };
56
+ }
57
+
58
+ export function modelSteppedPoses(read: ReadDocument): SteppedPoseFacts {
59
+ let view: CompiledDocument | null = null;
60
+ const viewOf = (): CompiledDocument => (view ??= noSkinView(read.doc));
61
+ const animationOf = (name: string) => {
62
+ const anim = viewOf().animations.find((a) => a.name === name);
63
+ if (anim === undefined) throw new Error(`internal: the model side was asked to pose animation "${name}", which the document does not hold`);
64
+ return anim;
65
+ };
66
+ return {
67
+ boneCount: read.doc.bones.length,
68
+ steppedAnimations: fileAnimationOrder(read.doc).flatMap((name) => {
69
+ const anim = read.doc.animations.find((a) => a.name === name);
70
+ return anim === undefined ? [] : [{ name, duration: anim.timelines.duration }];
71
+ }),
72
+ hasAnimation: (name) => read.doc.animations.some((a) => a.name === name),
73
+ hasBone: (name) => read.doc.bones.some((b) => b.name === name),
74
+ posedInherit: (animation, bone, time) => {
75
+ const anim = animationOf(animation);
76
+ const t = time < anim.timelines.duration ? time : anim.timelines.duration;
77
+ const posed = posedBones(viewOf(), anim.timelines, t).find((b) => b.name === bone);
78
+ return posed === undefined ? undefined : modeOf(posed);
79
+ },
80
+ // The document's stated mode — asked only of a bone posing none, which the reader leaves this side no document to hold.
81
+ statedInherit: (name) => `${JSON.stringify(read.doc.bones.find((b) => b.name === name)?.inheritMode)}`,
82
+ setup: () => frameOf(poseWalkSetup(viewOf()), viewOf().bones),
83
+ walk: (animation, step, frames) => {
84
+ const anim = animationOf(animation);
85
+ const poses = poseLoopingWalk(viewOf(), animation, new Array<number>(frames).fill(step));
86
+ return poses.slice(1).map((pose) => frameOf(pose, posedBones(viewOf(), anim.timelines, pose.animationTime)));
87
+ },
88
+ };
89
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The model side's supply of `TwoColourFacts` (issue #1025, cut 4c-3 of step
3
+ * 4c of #380).
4
+ *
5
+ * - **The stated dark colours**: the document's slots in their order — the
6
+ * emitted slot array's — each `dark` as the document holds it, which the
7
+ * emitter writes into the slot unchanged (`emitSlot`). `readModel` refuses
8
+ * a `dark` that is not six or eight hex digits, so a spelling the runtime
9
+ * would load as NaN or drop has no document to arrive in.
10
+ * - **The loaded dark colour**: the core's reading of that hex (`readColour`,
11
+ * its first three channels) — the reading the setup pose's slot rows carry,
12
+ * which `tools/pose_oracle.ts compare` holds equal to spine-core's on its
13
+ * `setup.slots` block, on the grid and under `--raw`.
14
+ * - **The slot timelines**: `fileSlotTimelines` (`./slot_colour.ts`), the walk
15
+ * A45 reads — the file's order and spelling, through the emitter's own
16
+ * passes.
17
+ * - **A slot's posed colours**: the core's raw entry (`poseRawAnimation`,
18
+ * `src/core/raw.ts`) taking one step of the time on a fresh track, under the
19
+ * view the core poses with no skin set (`noSkinView`), as A45's posed colour
20
+ * is taken — the slot row's light channels and its dark colour, NaN where the
21
+ * row reads no number. No gate samples the times A43 asks for (a key's own
22
+ * time), so the selftest compares these values with spine-core's at every
23
+ * time the body asks, on every call with a model in hand (`VF12`).
24
+ *
25
+ * Links nothing from the runtime.
26
+ */
27
+ import { readColour, type CompiledDocument } from '../../core/index.ts';
28
+ import { poseRawAnimation } from '../../core/raw.ts';
29
+ import { noSkinView } from '../../render_core.ts';
30
+ import type { DarkColour, TwoColourFacts } from '../facts/two_colour.ts';
31
+ import type { ReadDocument } from './parse.ts';
32
+ import { fileSlotTimelines } from './slot_colour.ts';
33
+
34
+ /** A channel the pose read as no number, as a non-finite channel reads on the runtime's side. */
35
+ const channel = (v: number | null): number => (v === null ? Number.NaN : v);
36
+
37
+ export function modelTwoColour(read: ReadDocument): TwoColourFacts {
38
+ let view: CompiledDocument | null = null;
39
+ const slotDarks = read.doc.slots.flatMap((slot) => (typeof slot.dark === 'string' ? [{ slot: slot.name, dark: slot.dark }] : []));
40
+ return {
41
+ slotDarks,
42
+ loadedDark: (name): DarkColour | null => {
43
+ const dark = read.doc.slots.find((s) => s.name === name)?.dark;
44
+ if (dark === undefined) return null;
45
+ const [r, g, b] = readColour(dark);
46
+ return { r, g, b };
47
+ },
48
+ slotTimelines: fileSlotTimelines(read),
49
+ hasAnimation: (name) => read.doc.animations.some((a) => a.name === name),
50
+ posedTint: (animation, slot, time) => {
51
+ view ??= noSkinView(read.doc);
52
+ const row = poseRawAnimation(view, animation, [time], {}, 'animation')[1].slots.find((r) => r[0] === slot);
53
+ if (row === undefined) return undefined;
54
+ const dark = row[6];
55
+ return {
56
+ light: { r: channel(row[2]), g: channel(row[3]), b: channel(row[4]), a: channel(row[5]) },
57
+ dark: dark === null ? null : { r: channel(dark[0]), g: channel(dark[1]), b: channel(dark[2]) },
58
+ };
59
+ },
60
+ };
61
+ }
@@ -10,10 +10,11 @@
10
10
  * (`readVertices`); the selftest compares it with spine-core's on every
11
11
  * call. A path's `closed` and `lengths` are the core's reading of the record
12
12
  * (`readGeometry`: `closed` absent reads the parser's `false`).
13
- * - **The clip ends** in the order the file spells them: the skins by the
14
- * emitter's `editorSkinOrder`, each skin's slot keys by its
15
- * `editorSlotKeyOrder` (both `src/compile.ts`, the functions `emitSkins` is
16
- * handed), each slot's records in table order — every clipping record that
13
+ * - **The clip ends** in the order the file spells them: the skins and each
14
+ * skin's slot keys in the order `fileSkinOrder` gives (`src/compile.ts`,
15
+ * issue #1034: a `rigc-compiled/3` document's `editorOrder`, else the
16
+ * emitter's `editorSkinOrder` and `editorSlotKeyOrder`, the functions
17
+ * `emitSkins` is handed), each slot's records in table order — every clipping record that
17
18
  * states an `end`, with the `end` as it states it.
18
19
  *
19
20
  * `encoding` is empty on every polygon: the document states the weighted
@@ -21,7 +22,7 @@
21
22
  *
22
23
  * Links nothing from the runtime.
23
24
  */
24
- import { editorSkinOrder, editorSlotKeyOrder } from '../../compile.ts';
25
+ import { fileSkinOrder, skinsInFileOrder } from '../../compile.ts';
25
26
  import { shownRow } from '../../core/index.ts';
26
27
  import type { ClipEnd, PolygonEntry, PolygonFacts } from '../facts/vertex_polygons.ts';
27
28
  import { isObj, type Json } from '../values.ts';
@@ -40,9 +41,9 @@ export function documentRecord(read: ReadDocument, skin: string, slot: string, p
40
41
  /** Every record of the document in the order the Spine file spells it: skins, then each skin's slot keys, then the slot's table. */
41
42
  export function fileSpelledEntries(read: ReadDocument): Array<{ skin: string; slot: string; placeholder: string; record: Json }> {
42
43
  const out: Array<{ skin: string; slot: string; placeholder: string; record: Json }> = [];
43
- for (const skin of editorSkinOrder(read.doc.skins)) {
44
- for (const [slot, table] of Object.entries(editorSlotKeyOrder(skin.attachments))) {
45
- for (const placeholder of Object.keys(table)) out.push({ skin: skin.name, slot, placeholder, record: documentRecord(read, skin.name, slot, placeholder) });
44
+ for (const { skin, slots } of skinsInFileOrder(read.doc.skins, fileSkinOrder(read.doc))) {
45
+ for (const slot of slots) {
46
+ for (const placeholder of Object.keys(skin.attachments[slot] ?? {})) out.push({ skin: skin.name, slot, placeholder, record: documentRecord(read, skin.name, slot, placeholder) });
46
47
  }
47
48
  }
48
49
  return out;
package/src/bonedist.ts CHANGED
@@ -77,7 +77,7 @@
77
77
  */
78
78
  import { readFileSync } from 'node:fs';
79
79
  import {
80
- loadPosable,
80
+ loadPosedSkeleton,
81
81
  PROTOCOL_FPS,
82
82
  sampleAnimation,
83
83
  sampleSetupPose,
@@ -184,7 +184,7 @@ export interface SkeletonSize {
184
184
  degenerate: boolean;
185
185
  }
186
186
 
187
- function skeletonSize(posable: Posable): SkeletonSize {
187
+ function skeletonSize(posable: Pick<Posable, 'data'>): SkeletonSize {
188
188
  const setup = sampleSetupPose(posable.data, { bones: true }).at(0);
189
189
  const bones = setup?.bones ?? [];
190
190
  const rootData = posable.data.bones.find((b) => b.parent === null) ?? posable.data.bones.at(0);
@@ -338,10 +338,17 @@ export interface BoneDistReport {
338
338
  export interface BoneDistOptions {
339
339
  candidateSkeleton: string;
340
340
  candidateAtlas: string;
341
- candidateAtlasDir: string;
341
+ /**
342
+ * Where the candidate's atlas pages are. Not read since issue #1042: no
343
+ * figure here samples a pixel, so no page is opened, and a pair whose pages
344
+ * are elsewhere measures the same. Kept optional so a caller that passes it
345
+ * still compiles.
346
+ */
347
+ candidateAtlasDir?: string;
342
348
  referenceSkeleton: string;
343
349
  referenceAtlas: string;
344
- referenceAtlasDir: string;
350
+ /** The reference's, as `candidateAtlasDir` — not read. */
351
+ referenceAtlasDir?: string;
345
352
  /** A correspondence file path, or `identity`. */
346
353
  bones: string;
347
354
  fps?: number;
@@ -398,8 +405,13 @@ function extremesOf(values: number[]): Extremes {
398
405
  export function boneDistance(options: BoneDistOptions): BoneDistReport {
399
406
  const fps = options.fps ?? PROTOCOL_FPS;
400
407
  if (!Number.isFinite(fps) || fps <= 0) throw new BoneDistError('fps must be a positive number');
401
- const candidate = loadPosable(options.candidateSkeleton, options.candidateAtlas, options.candidateAtlasDir);
402
- const reference = loadPosable(options.referenceSkeleton, options.referenceAtlas, options.referenceAtlasDir);
408
+ // Each side through spine-core, refused by name where its skeleton does not load against its atlas (issue #1042),
409
+ // and without its page images: nothing here samples a pixel (`loadPosedSkeleton`).
410
+ const posedFor = (side: 'candidate' | 'reference', skeleton: string, atlas: string): Pick<Posable, 'data'> => ({
411
+ data: loadPosedSkeleton(skeleton, atlas, `bonedist's ${side}: every bone's world transform is read off spine-core's pose of it`),
412
+ });
413
+ const candidate = posedFor('candidate', options.candidateSkeleton, options.candidateAtlas);
414
+ const reference = posedFor('reference', options.referenceSkeleton, options.referenceAtlas);
403
415
 
404
416
  const candidateBoneNames = candidate.data.bones.map((b) => b.name);
405
417
  const referenceBoneNames = new Set(reference.data.bones.map((b) => b.name));