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.
- package/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +13 -0
- package/LICENSE +30 -0
- package/NOTICE.md +145 -0
- package/README.md +817 -3
- package/bin/rigc.cjs +83 -0
- package/cli.ts +61 -0
- package/cli_core.ts +46 -0
- package/docs/AUTHORING.md +9923 -0
- package/docs/FACE.md +1948 -0
- package/docs/INGEST.md +1488 -0
- package/docs/MOTION.md +1241 -0
- package/docs/PROMPTING.md +109 -0
- package/docs/RIGGING.md +1441 -0
- package/docs/SPEC_COVERAGE.md +357 -0
- package/package.json +108 -4
- package/skills/rigc/SKILL.md +133 -0
- package/skills/rigc-face/SKILL.md +60 -0
- package/skills/rigc-ingest/SKILL.md +78 -0
- package/skills/rigc-motion/SKILL.md +51 -0
- package/skills/rigc-rigging/SKILL.md +49 -0
- package/src/areaband.ts +159 -0
- package/src/assertions/bodies/a01.ts +23 -0
- package/src/assertions/bodies/a02.ts +21 -0
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a04.ts +40 -0
- package/src/assertions/bodies/a05.ts +56 -0
- package/src/assertions/bodies/a06.ts +245 -0
- package/src/assertions/bodies/a07.ts +68 -0
- package/src/assertions/bodies/a08.ts +76 -0
- package/src/assertions/bodies/a09.ts +82 -0
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a12.ts +30 -0
- package/src/assertions/bodies/a13.ts +51 -0
- package/src/assertions/bodies/a14.ts +35 -0
- package/src/assertions/bodies/a15.ts +97 -0
- package/src/assertions/bodies/a16.ts +24 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a18.ts +62 -0
- package/src/assertions/bodies/a19.ts +404 -0
- package/src/assertions/bodies/a20.ts +122 -0
- package/src/assertions/bodies/a21.ts +190 -0
- package/src/assertions/bodies/a22.ts +39 -0
- package/src/assertions/bodies/a23.ts +305 -0
- package/src/assertions/bodies/a24.ts +68 -0
- package/src/assertions/bodies/a25.ts +39 -0
- package/src/assertions/bodies/a26.ts +61 -0
- package/src/assertions/bodies/a27.ts +33 -0
- package/src/assertions/bodies/a28.ts +70 -0
- package/src/assertions/bodies/a29.ts +34 -0
- package/src/assertions/bodies/a30.ts +50 -0
- package/src/assertions/bodies/a31.ts +61 -0
- package/src/assertions/bodies/a32.ts +44 -0
- package/src/assertions/bodies/a33.ts +110 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a35.ts +160 -0
- package/src/assertions/bodies/a36.ts +81 -0
- package/src/assertions/bodies/a37.ts +77 -0
- package/src/assertions/bodies/a38.ts +73 -0
- package/src/assertions/bodies/a39.ts +303 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/bodies/a42.ts +97 -0
- package/src/assertions/bodies/a43.ts +181 -0
- package/src/assertions/bodies/a44.ts +23 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/bodies/a46.ts +224 -0
- package/src/assertions/bodies/a47.ts +126 -0
- package/src/assertions/bodies/a48.ts +83 -0
- package/src/assertions/bodies/a49.ts +81 -0
- package/src/assertions/bodies/a50.ts +97 -0
- package/src/assertions/constraint_words.ts +169 -0
- package/src/assertions/emitted/index.ts +148 -0
- package/src/assertions/facts/animated_bones.ts +30 -0
- package/src/assertions/facts/animation_durations.ts +37 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/atlas_regions.ts +52 -0
- package/src/assertions/facts/bone_timelines.ts +37 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/constraints.ts +155 -0
- package/src/assertions/facts/deform_survey.ts +27 -0
- package/src/assertions/facts/event_keys.ts +55 -0
- package/src/assertions/facts/linked_meshes.ts +38 -0
- package/src/assertions/facts/mesh_attachments.ts +100 -0
- package/src/assertions/facts/region_joins.ts +34 -0
- package/src/assertions/facts/sequences.ts +85 -0
- package/src/assertions/facts/skeleton_roster.ts +45 -0
- package/src/assertions/facts/skin_entries.ts +37 -0
- package/src/assertions/facts/skin_members.ts +53 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/facts/stage.ts +27 -0
- package/src/assertions/facts/stage_box.ts +65 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/facts/two_colour.ts +52 -0
- package/src/assertions/facts/vertex_polygons.ts +53 -0
- package/src/assertions/footprints.ts +367 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/inward_advance.ts +58 -0
- package/src/assertions/kinds.ts +105 -0
- package/src/assertions/mesh_kinds.ts +56 -0
- package/src/assertions/model/animated_bones.ts +38 -0
- package/src/assertions/model/animation_durations.ts +57 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/atlas_regions.ts +76 -0
- package/src/assertions/model/bone_timelines.ts +58 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/constraints.ts +233 -0
- package/src/assertions/model/declared.ts +125 -0
- package/src/assertions/model/deform_survey.ts +24 -0
- package/src/assertions/model/event_keys.ts +45 -0
- package/src/assertions/model/given.ts +45 -0
- package/src/assertions/model/index.ts +398 -0
- package/src/assertions/model/linked_meshes.ts +24 -0
- package/src/assertions/model/mesh_attachments.ts +119 -0
- package/src/assertions/model/parse.ts +146 -0
- package/src/assertions/model/region_joins.ts +67 -0
- package/src/assertions/model/runtime_timelines.ts +78 -0
- package/src/assertions/model/sequences.ts +157 -0
- package/src/assertions/model/skeleton_roster.ts +23 -0
- package/src/assertions/model/skin_entries.ts +69 -0
- package/src/assertions/model/skin_members.ts +64 -0
- package/src/assertions/model/slider_composition.ts +193 -0
- package/src/assertions/model/slot_colour.ts +81 -0
- package/src/assertions/model/stage.ts +28 -0
- package/src/assertions/model/stage_box.ts +51 -0
- package/src/assertions/model/stepped_poses.ts +105 -0
- package/src/assertions/model/two_colour.ts +61 -0
- package/src/assertions/model/vertex_polygons.ts +72 -0
- package/src/assertions/reasons.ts +129 -0
- package/src/assertions/region_lookups.ts +61 -0
- package/src/assertions/report.ts +189 -0
- package/src/assertions/values.ts +39 -0
- package/src/atlas.ts +2870 -0
- package/src/ballot.ts +866 -0
- package/src/bonedist.ts +643 -0
- package/src/chainfit.ts +2752 -0
- package/src/chains.ts +170 -0
- package/src/check.ts +4303 -0
- package/src/checkpics.ts +295 -0
- package/src/cli/core_commands.ts +1627 -0
- package/src/cli/repack.ts +414 -0
- package/src/cli/shared.ts +2776 -0
- package/src/cli/spine_commands.ts +820 -0
- package/src/compile.ts +9414 -0
- package/src/core/additive.ts +458 -0
- package/src/core/animation.ts +1050 -0
- package/src/core/clipping.ts +696 -0
- package/src/core/constraints.ts +1876 -0
- package/src/core/constraints_path.ts +964 -0
- package/src/core/constraints_physics.ts +881 -0
- package/src/core/constraints_slider.ts +635 -0
- package/src/core/deform.ts +613 -0
- package/src/core/draw_order.ts +125 -0
- package/src/core/events.ts +135 -0
- package/src/core/hooks.ts +249 -0
- package/src/core/index.ts +1400 -0
- package/src/core/raw.ts +739 -0
- package/src/core/skins.ts +129 -0
- package/src/core/uvs.ts +469 -0
- package/src/core/vertices.ts +490 -0
- package/src/core/walk.ts +197 -0
- package/src/core/world.ts +289 -0
- package/src/correspondence.ts +15 -0
- package/src/deformbuild.ts +60 -0
- package/src/deformgen.ts +630 -0
- package/src/deformmeasure.ts +732 -0
- package/src/deformreport.ts +373 -0
- package/src/deformstructure.ts +386 -0
- package/src/deformsurvey.ts +2162 -0
- package/src/depth.ts +784 -0
- package/src/diff.ts +2252 -0
- package/src/emit.ts +134 -0
- package/src/emit_spine.ts +854 -0
- package/src/errors.ts +53 -0
- package/src/framing.ts +819 -0
- package/src/generation.ts +139 -0
- package/src/ingest.ts +2293 -0
- package/src/json-position.ts +253 -0
- package/src/keyorder.ts +587 -0
- package/src/keys.ts +486 -0
- package/src/ladder.ts +121 -0
- package/src/mesh.ts +2382 -0
- package/src/meshcompare.ts +1188 -0
- package/src/meshquality.ts +2042 -0
- package/src/meshrasters.ts +944 -0
- package/src/meshreduce.ts +1425 -0
- package/src/model.ts +1245 -0
- package/src/motion.ts +809 -0
- package/src/nonfinite.ts +54 -0
- package/src/package_meta.ts +48 -0
- package/src/png.ts +297 -0
- package/src/pose.ts +2324 -0
- package/src/preview.ts +434 -0
- package/src/region_joins.ts +54 -0
- package/src/render.ts +1013 -0
- package/src/render_core.ts +871 -0
- package/src/render_shared.ts +2958 -0
- package/src/repack.ts +495 -0
- package/src/rig.ts +2941 -0
- package/src/slots.ts +892 -0
- package/src/spine_side.ts +138 -0
- package/src/timelines.ts +837 -0
- package/src/trackgen.ts +364 -0
- package/src/transform.ts +310 -0
- package/src/types.ts +1797 -0
- package/src/validate.ts +3875 -0
- package/tools/contact.ts +126 -0
- package/tools/editor_roundtrip.ts +1641 -0
- package/tools/font5x7.ts +101 -0
- package/tools/measure_contact_depth.ts +105 -0
- package/tools/plate.ts +508 -0
- package/tools/png_probe.mjs +72 -0
|
@@ -0,0 +1,193 @@
|
|
|
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
|
|
20
|
+
* validator's table (`RUNTIME_TIMELINE`, `./runtime_timelines.ts`; the
|
|
21
|
+
* core's until issue #1054), the one value here that is not the document's,
|
|
22
|
+
* measured off the loaded timelines; an id's index is
|
|
23
|
+
* the target's position in the document's bones, slots or constraints —
|
|
24
|
+
* which the emitter writes in that order — `-1` for a physics timeline
|
|
25
|
+
* naming none; a deform's attachment is named as the runtime names it, the
|
|
26
|
+
* record's `name`, else its placeholder.
|
|
27
|
+
* - **The behaviour** is the core's probe over the document under no skin set
|
|
28
|
+
* (`noSkinView`, the view measured in issue #1051) and under each skin
|
|
29
|
+
* (`underSkin`): `additiveCells`, and the
|
|
30
|
+
* class `additiveBehaviour` reads off them. The core suite's `CO27` holds
|
|
31
|
+
* both to the runtime's probe; the selftest asks this supplier every
|
|
32
|
+
* question the body asks the runtime's (`VF14`).
|
|
33
|
+
*
|
|
34
|
+
* Links nothing from the runtime.
|
|
35
|
+
*/
|
|
36
|
+
import { fileSkinOrder, keyedOrder } from '../../compile.ts';
|
|
37
|
+
import { additiveBehaviour, additiveCells, additiveSpelling, type AdditiveTimeline, type AdditiveView } from '../../core/additive.ts';
|
|
38
|
+
import { RUNTIME_TIMELINE } from './runtime_timelines.ts';
|
|
39
|
+
import { EVERY_GLOBAL_PHYSICS, type PhysicsTimelineKind } from '../../core/constraints_physics.ts';
|
|
40
|
+
import { underSkin, type CompiledDocument } from '../../core/index.ts';
|
|
41
|
+
import type { BoneTimelineKind, SlotTimelineKind } from '../../core/animation.ts';
|
|
42
|
+
import { noSkinView } from '../../render_core.ts';
|
|
43
|
+
import { entryAddress } from '../facts/sequences.ts';
|
|
44
|
+
import type { SliderCompositionFacts, SliderFact, SliderTimelineFact } from '../facts/slider_composition.ts';
|
|
45
|
+
import { modelConstraintTimelineSources } from './constraints.ts';
|
|
46
|
+
import { fileBoneTimelines } from './bone_timelines.ts';
|
|
47
|
+
import { fileSlotTimelines } from './slot_colour.ts';
|
|
48
|
+
import type { ReadDocument } from './parse.ts';
|
|
49
|
+
import { isObj } from '../values.ts';
|
|
50
|
+
|
|
51
|
+
/** One timeline of an animation: the facts' reading of it and the core's. */
|
|
52
|
+
export interface ModelSliderTimeline {
|
|
53
|
+
fact: SliderTimelineFact;
|
|
54
|
+
core: AdditiveTimeline;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Names in the order each first appears, put in the order a JSON object keyed by them lists them (`keyedOrder`). */
|
|
58
|
+
const keyedFirsts = (names: readonly string[]): string[] => keyedOrder(names.filter((name, i) => names.indexOf(name) === i));
|
|
59
|
+
|
|
60
|
+
/** The words of one timeline: the table's class and properties, each id addressed by `index` (and `attachment`), and the sentence's naming. */
|
|
61
|
+
function factOf(doc: CompiledDocument, core: AdditiveTimeline): SliderTimelineFact {
|
|
62
|
+
const row = RUNTIME_TIMELINE[additiveSpelling(core)];
|
|
63
|
+
if (row === undefined) throw new Error(`internal: "${additiveSpelling(core)}" is no row of the runtime's timeline table`);
|
|
64
|
+
const property = (p: string, index: number | null, attachment: string | null, names: string) => ({
|
|
65
|
+
id: [p, ...(index === null ? [] : [String(index)]), ...(attachment === null ? [] : [attachment])].join('|'),
|
|
66
|
+
property: p,
|
|
67
|
+
names,
|
|
68
|
+
});
|
|
69
|
+
const slotIndex = (name: string): number => doc.slots.findIndex((s) => s.name === name);
|
|
70
|
+
let properties: SliderTimelineFact['properties'];
|
|
71
|
+
switch (core.group) {
|
|
72
|
+
case 'bone': {
|
|
73
|
+
const index = doc.bones.findIndex((b) => b.name === core.bone);
|
|
74
|
+
properties = row.properties.map((p) => property(p, index, null, `bone "${core.bone}" ${p}`));
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case 'slot':
|
|
78
|
+
properties = row.properties.map((p) => property(p, slotIndex(core.slot), null, `slot "${core.slot}" ${p}`));
|
|
79
|
+
break;
|
|
80
|
+
case 'attachment': {
|
|
81
|
+
const record = doc.skins.find((k) => k.name === core.skin)?.attachments[core.slot]?.[core.attachment];
|
|
82
|
+
const shown = record?.name ?? core.attachment;
|
|
83
|
+
const address = entryAddress(core.skin, core.slot, core.attachment);
|
|
84
|
+
properties = row.properties.map((p) => property(p, slotIndex(core.slot), address, core.word === 'deform' ? `slot "${core.slot}" deform of "${shown}"` : `slot "${core.slot}" ${p}`));
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case 'drawOrder':
|
|
88
|
+
case 'events':
|
|
89
|
+
properties = row.properties.map((p) => property(p, null, null, `the skeleton's ${p}`));
|
|
90
|
+
break;
|
|
91
|
+
default: {
|
|
92
|
+
const index = core.group === 'physics' && core.constraint === EVERY_GLOBAL_PHYSICS ? -1 : doc.constraints.findIndex((c) => c.kind === core.group && c.name === core.constraint);
|
|
93
|
+
properties = row.properties.map((p) => property(p, row.address === 'none' ? null : index, null, index >= 0 ? `constraint "${core.constraint}" ${p}` : `the skeleton's ${p}`));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { runtimeClass: row.runtimeClass, properties };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Every timeline of animation `name`, in the order the runtime builds them (the header). */
|
|
100
|
+
export function modelAnimationTimelines(read: ReadDocument, name: string): ModelSliderTimeline[] {
|
|
101
|
+
const { doc } = read;
|
|
102
|
+
const anim = doc.animations.find((a) => a.name === name);
|
|
103
|
+
if (anim === undefined) return [];
|
|
104
|
+
const t = anim.timelines;
|
|
105
|
+
const out: AdditiveTimeline[] = [];
|
|
106
|
+
for (const { slot, timelines } of fileSlotTimelines(read).filter((s) => s.animation === name)) {
|
|
107
|
+
const target = t.slots.find((s) => s.name === slot);
|
|
108
|
+
for (const word of Object.keys(timelines)) {
|
|
109
|
+
const keys = target?.timelines.find((x) => x.kind === word)?.keys;
|
|
110
|
+
if (keys !== undefined) out.push({ group: 'slot', word: word as SlotTimelineKind, slot, keys });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (const { bone, timelines } of fileBoneTimelines(read).filter((b) => b.animation === name)) {
|
|
114
|
+
const target = t.bones.find((b) => b.name === bone);
|
|
115
|
+
for (const word of isObj(timelines) ? Object.keys(timelines) : []) {
|
|
116
|
+
const keys = target?.timelines.find((x) => x.kind === word)?.keys;
|
|
117
|
+
if (keys !== undefined) out.push({ group: 'bone', word: word as BoneTimelineKind, bone, keys });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const source of modelConstraintTimelineSources(read).filter((s) => s.animation === name)) {
|
|
121
|
+
switch (source.kind) {
|
|
122
|
+
case 'ik': {
|
|
123
|
+
const keys = anim.constraints.ik.find((x) => x.name === source.name)?.keys;
|
|
124
|
+
if (keys !== undefined) out.push({ group: 'ik', word: 'ik', constraint: source.name, keys });
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case 'transform':
|
|
128
|
+
out.push({ group: 'transform', word: 'transform', constraint: source.name, keys: source.keys });
|
|
129
|
+
break;
|
|
130
|
+
case 'path':
|
|
131
|
+
out.push({ group: 'path', word: source.word as 'position' | 'spacing' | 'mix', constraint: source.name, keys: source.keys });
|
|
132
|
+
break;
|
|
133
|
+
case 'physics':
|
|
134
|
+
out.push({ group: 'physics', word: source.word as PhysicsTimelineKind, constraint: source.name, keys: source.keys });
|
|
135
|
+
break;
|
|
136
|
+
case 'slider':
|
|
137
|
+
out.push({ group: 'slider', word: source.word as 'time' | 'mix', constraint: source.name, keys: source.keys });
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const attachments = t.attachments;
|
|
142
|
+
for (const skin of keyedFirsts(attachments.map((a) => a.skin))) {
|
|
143
|
+
const inSkin = attachments.filter((a) => a.skin === skin);
|
|
144
|
+
for (const slot of keyedFirsts(inSkin.map((a) => a.slot))) {
|
|
145
|
+
const inSlot = inSkin.filter((a) => a.slot === slot);
|
|
146
|
+
for (const attachment of keyedFirsts(inSlot.map((a) => a.attachment))) {
|
|
147
|
+
for (const a of inSlot.filter((x) => x.attachment === attachment)) {
|
|
148
|
+
if (a.deform !== null) out.push({ group: 'attachment', word: 'deform', skin, slot, attachment, keys: a.deform });
|
|
149
|
+
if (a.sequence !== null) out.push({ group: 'attachment', word: 'sequence', skin, slot, attachment, keys: a.sequence });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (t.drawOrder.length > 0) out.push({ group: 'drawOrder', word: 'drawOrder', keys: t.drawOrder });
|
|
155
|
+
if (t.events.length > 0) out.push({ group: 'events', word: 'events', times: t.events.map((k) => k.time) });
|
|
156
|
+
return out.map((core) => ({ fact: factOf(doc, core), core }));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The views the probe poses under: no skin set, then every skin in the file's order. */
|
|
160
|
+
export function additiveViews(doc: CompiledDocument): AdditiveView[] {
|
|
161
|
+
return [{ name: '(none)', doc: noSkinView(doc) }, ...fileSkinOrder(doc).map((skin) => ({ name: skin.name, doc: underSkin(doc, skin.name) }))];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function modelSliderComposition(read: ReadDocument): SliderCompositionFacts {
|
|
165
|
+
const { doc } = read;
|
|
166
|
+
const skinOrder = fileSkinOrder(doc).map((skin) => skin.name);
|
|
167
|
+
const walked = new Map<string, ModelSliderTimeline[]>();
|
|
168
|
+
const timelinesOf = (name: string): ModelSliderTimeline[] => {
|
|
169
|
+
const held = walked.get(name);
|
|
170
|
+
if (held !== undefined) return held;
|
|
171
|
+
const fresh = modelAnimationTimelines(read, name);
|
|
172
|
+
walked.set(name, fresh);
|
|
173
|
+
return fresh;
|
|
174
|
+
};
|
|
175
|
+
const sliders: SliderFact[] = [];
|
|
176
|
+
doc.constraints.forEach((c, index) => {
|
|
177
|
+
const r = c.record;
|
|
178
|
+
if (r?.kind !== 'slider') return;
|
|
179
|
+
const animation = doc.animations.some((a) => a.name === r.animation) ? { name: r.animation, timelines: timelinesOf(r.animation).map((x) => x.fact) } : null;
|
|
180
|
+
const skins = skinOrder.filter((name) => doc.skins.some((k) => k.name === name && k.constraints.slider.includes(r.name)));
|
|
181
|
+
sliders.push({ name: r.name, index, mix: r.mix, additive: r.additive, skinRequired: r.skin, skins, animation });
|
|
182
|
+
});
|
|
183
|
+
let views: AdditiveView[] | null = null;
|
|
184
|
+
return {
|
|
185
|
+
sliders,
|
|
186
|
+
behaviour: (animation, at) => {
|
|
187
|
+
const timeline = timelinesOf(animation)[at];
|
|
188
|
+
if (timeline === undefined) throw new Error(`internal: animation "${animation}" has no timeline ${at}`);
|
|
189
|
+
views ??= additiveViews(doc);
|
|
190
|
+
return additiveBehaviour(additiveCells(views, timeline.core));
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `SlotColourFacts` (issue #1025, step 4c of #380).
|
|
3
|
+
*
|
|
4
|
+
* - **The slot timelines, in the file's order and spelling.** The document
|
|
5
|
+
* holds each animation's slot timelines in the order they are applied, with
|
|
6
|
+
* every key as the compiler built it; the Spine file keys the animations in
|
|
7
|
+
* the editor's order and passes every key through two passes. So 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
|
|
11
|
+
* file lays them and handed to the emitter's own `withoutParserDefaults` and
|
|
12
|
+
* `inEditorKeyOrder` (`src/keyorder.ts`) — so a key field the emitter would
|
|
13
|
+
* leave out at the parser's default is left out here too, and a body
|
|
14
|
+
* printing `JSON.stringify(key.value)` prints what it prints over the file.
|
|
15
|
+
* Nothing here restates either rule.
|
|
16
|
+
* - **Whether an animation exists**: the document's own roster.
|
|
17
|
+
* - **A slot's posed colour**: the core's raw entry (`poseRawAnimation`,
|
|
18
|
+
* `src/core/raw.ts`) taking one step of the time on a fresh non-looping
|
|
19
|
+
* track, under the view the core poses with no skin set (`noSkinView`,
|
|
20
|
+
* `src/render_core.ts`) — the reading of what `validate()` does through
|
|
21
|
+
* spine-core (a fresh skeleton, no skin, setup pose, the track stepped to the
|
|
22
|
+
* time and applied). Its slot rows are the ones `CR03` holds bit for bit to
|
|
23
|
+
* spine-core on every frame of the nineteen recipes; that the two readings
|
|
24
|
+
* print the same A45 line is the selftest's supplier check, on every call
|
|
25
|
+
* that poses one (`VF02`). A channel the pose read as no number is NaN, as a
|
|
26
|
+
* non-finite channel reads on the runtime's side.
|
|
27
|
+
*
|
|
28
|
+
* Links nothing from the runtime.
|
|
29
|
+
*/
|
|
30
|
+
import { fileAnimationOrder } from '../../compile.ts';
|
|
31
|
+
import type { CompiledDocument } from '../../core/index.ts';
|
|
32
|
+
import { poseRawAnimation } from '../../core/raw.ts';
|
|
33
|
+
import { inEditorKeyOrder, withoutParserDefaults } from '../../keyorder.ts';
|
|
34
|
+
import { noSkinView } from '../../render_core.ts';
|
|
35
|
+
import type { SlotColourFacts, SlotTimelines } from '../facts/slot_colour.ts';
|
|
36
|
+
import { isObj, type Json } from '../values.ts';
|
|
37
|
+
import type { ReadDocument } from './parse.ts';
|
|
38
|
+
|
|
39
|
+
const list = (v: unknown): Json[] => (Array.isArray(v) ? v.filter(isObj) : []);
|
|
40
|
+
|
|
41
|
+
/** Every animation's slot timelines as the Spine file states them: `animations.<name>.slots.<slot>.<timeline> = keys`. */
|
|
42
|
+
export function fileSlotTimelines(read: ReadDocument): SlotTimelines[] {
|
|
43
|
+
const byName = new Map<string, Json>();
|
|
44
|
+
for (const anim of list(read.json.animations)) if (typeof anim.name === 'string') byName.set(anim.name, anim);
|
|
45
|
+
const animations: Json = {};
|
|
46
|
+
for (const name of fileAnimationOrder(read.doc)) {
|
|
47
|
+
const slots: Json = {};
|
|
48
|
+
for (const slot of list((byName.get(name) as Json).slots)) {
|
|
49
|
+
if (typeof slot.name !== 'string') continue;
|
|
50
|
+
const timelines: Json = {};
|
|
51
|
+
for (const timeline of list(slot.timelines)) {
|
|
52
|
+
if (typeof timeline.name === 'string') timelines[timeline.name] = list(timeline.keys).map((key) => ({ ...key }));
|
|
53
|
+
}
|
|
54
|
+
slots[slot.name] = timelines;
|
|
55
|
+
}
|
|
56
|
+
// An animation keying no slot writes no `slots` group — `emitAnimation`'s `if (animation.slots.size)`.
|
|
57
|
+
animations[name] = Object.keys(slots).length > 0 ? { slots } : {};
|
|
58
|
+
}
|
|
59
|
+
const file = inEditorKeyOrder(withoutParserDefaults({ animations }));
|
|
60
|
+
const out: SlotTimelines[] = [];
|
|
61
|
+
for (const [animation, anim] of Object.entries(file.animations)) {
|
|
62
|
+
if (!isObj(anim) || !isObj(anim.slots)) continue;
|
|
63
|
+
for (const [slot, timelines] of Object.entries(anim.slots)) if (isObj(timelines)) out.push({ animation, slot, timelines });
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function modelSlotColour(read: ReadDocument): SlotColourFacts {
|
|
69
|
+
let view: CompiledDocument | null = null;
|
|
70
|
+
return {
|
|
71
|
+
slotTimelines: fileSlotTimelines(read),
|
|
72
|
+
hasAnimation: (name) => read.doc.animations.some((a) => a.name === name),
|
|
73
|
+
posedSlot: (animation, slot, time) => {
|
|
74
|
+
view ??= noSkinView(read.doc);
|
|
75
|
+
const row = poseRawAnimation(view, animation, [time], {}, 'animation')[1].slots.find((r) => r[0] === slot);
|
|
76
|
+
if (row === undefined) return undefined;
|
|
77
|
+
const channel = (v: number | null): number => (v === null ? Number.NaN : v);
|
|
78
|
+
return { color: { r: channel(row[2]), g: channel(row[3]), b: channel(row[4]), a: channel(row[5]) } };
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `StageFacts` (issue #1025, step 4c of #380): the
|
|
3
|
+
* stage a `rigc-compiled/3` document states (`stated.stage`, issue #1026), or,
|
|
4
|
+
* for a `/2` or `/1` document, which does not, the stage its caller gives.
|
|
5
|
+
* A `/3` document given one anyway is refused (`refuseGivenBeside`).
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ A caller that gives no stage at all is refused by name rather than read as
|
|
8
|
+
* "the skeleton declares none": those are two different statements, and only
|
|
9
|
+
* the second is one the caller made. A stage stated as `null` IS that second
|
|
10
|
+
* statement, and reads as both extents absent, the way the runtime loads a
|
|
11
|
+
* header with neither key.
|
|
12
|
+
*
|
|
13
|
+
* Links nothing from the runtime.
|
|
14
|
+
*/
|
|
15
|
+
import type { StageFacts } from '../facts/stage.ts';
|
|
16
|
+
import { refuseGivenBeside, type ModelGiven } from './given.ts';
|
|
17
|
+
import type { ReadDocument } from './parse.ts';
|
|
18
|
+
|
|
19
|
+
export function modelStage(read: ReadDocument, given: ModelGiven | undefined): StageFacts {
|
|
20
|
+
refuseGivenBeside(read, given);
|
|
21
|
+
// The document's own statement (issue #1026): `null` is a skeleton that declares none, read as both extents absent.
|
|
22
|
+
if (read.doc.stated !== null) {
|
|
23
|
+
const stage = read.doc.stated.stage;
|
|
24
|
+
return { width: stage?.width, height: stage?.height };
|
|
25
|
+
}
|
|
26
|
+
if (given === undefined) throw new Error(`the caller gave no stage; a ${read.doc.spec} document does not state one, so the model side cannot read it`);
|
|
27
|
+
return { width: given.stage?.width, height: given.stage?.height };
|
|
28
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `StageBoxFacts` (issue #1168): the box the
|
|
3
|
+
* document's `stage.box` asks for, and what the document's `default` skin
|
|
4
|
+
* holds there — its record as the runtime would load it, and its world
|
|
5
|
+
* vertices at the setup pose as rigc's core poses them (`poseSetup` under no
|
|
6
|
+
* skin, constraints applied, then `worldVertices`, the function every vertex
|
|
7
|
+
* attachment of the core is posed by — no second arithmetic for the box).
|
|
8
|
+
*
|
|
9
|
+
* A `/2` or `/1` document states no stage and asks nothing; a `/3` document
|
|
10
|
+
* whose stage states no box asks nothing, and neither does one that declares no
|
|
11
|
+
* stage. The core's poses are held to spine-core's at tolerance 0 by
|
|
12
|
+
* `tools/core_gate.ts`, which is what lets this side and the round trip print
|
|
13
|
+
* one line.
|
|
14
|
+
*
|
|
15
|
+
* Links nothing from the runtime.
|
|
16
|
+
*/
|
|
17
|
+
import { activeBones, poseSetup, rawNumber, underNoSkin } from '../../core/index.ts';
|
|
18
|
+
import { worldVertices } from '../../core/vertices.ts';
|
|
19
|
+
import { SKIP_NO_STAGE_BOX, type StageBoxFacts, type StageBoxHeld } from '../facts/stage_box.ts';
|
|
20
|
+
import type { ReadDocument } from './parse.ts';
|
|
21
|
+
|
|
22
|
+
export function modelStageBox(read: ReadDocument): StageBoxFacts {
|
|
23
|
+
const doc = read.doc;
|
|
24
|
+
const stage = doc.stated?.stage;
|
|
25
|
+
if (doc.stated === null || stage === undefined || stage === null) return { asked: null, why: SKIP_NO_STAGE_BOX, slot: false, held: null };
|
|
26
|
+
if (stage.box === undefined) return { asked: null, why: SKIP_NO_STAGE_BOX, slot: false, held: null };
|
|
27
|
+
const asked = { slot: stage.box.slot, attachment: stage.box.attachment, stage: { x: stage.x, y: stage.y, width: stage.width, height: stage.height } };
|
|
28
|
+
const slot = doc.slots.find((s) => s.name === asked.slot);
|
|
29
|
+
if (slot === undefined) return { asked, why: '', slot: false, held: null };
|
|
30
|
+
const record = doc.skins.find((k) => k.name === 'default')?.attachments[asked.slot]?.[asked.attachment];
|
|
31
|
+
if (record === undefined) return { asked, why: '', slot: true, held: null };
|
|
32
|
+
const g = record.geometry;
|
|
33
|
+
const type = record.kind === 'linkedmesh' ? 'mesh' : record.kind;
|
|
34
|
+
if (g === undefined || g.kind !== 'boundingbox') return { asked, why: '', slot: true, held: { type, weighted: false, stored: [], world: '' } };
|
|
35
|
+
const vertices = g.vertices;
|
|
36
|
+
const stored = vertices.weighted ? [] : vertices.xy.map(Math.fround);
|
|
37
|
+
const held: StageBoxHeld = { type, weighted: vertices.weighted, stored, world: poseWorld(read, slot.bone, vertices) };
|
|
38
|
+
return { asked, why: '', slot: true, held };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The box's world vertices at the setup pose under no skin, or why there are none. */
|
|
42
|
+
function poseWorld(read: ReadDocument, bone: string, vertices: Parameters<typeof worldVertices>[0]): number[] | string {
|
|
43
|
+
const view = underNoSkin(read.doc);
|
|
44
|
+
if (!activeBones(view).has(bone)) return `its bone "${bone}" is inactive with no skin set, so nothing poses it`;
|
|
45
|
+
const posed = poseSetup(view, { round: rawNumber });
|
|
46
|
+
const world = posed.world;
|
|
47
|
+
if (world === null) return posed.absent.find(([block]) => block === 'setup.bones')?.[1] ?? "rigc's core does not pose this setup pose";
|
|
48
|
+
const frame = world.get(bone);
|
|
49
|
+
if (frame === undefined) return `its bone "${bone}" has no setup world transform`;
|
|
50
|
+
return worldVertices(vertices, frame, world);
|
|
51
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
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`). Since issue #1179's second part
|
|
17
|
+
* the poses come through that walk's scan assembler (`scanWalkSetup`,
|
|
18
|
+
* `scanLoopingWalk`): the same walk, each pose carrying what A10 reads — the
|
|
19
|
+
* bones' matrix and origin, the slot rows, the drawn attachments' slot, name
|
|
20
|
+
* and vertices — and none of the bones' getter readings and oracle rows or
|
|
21
|
+
* the drawn rows' UVs, triangles and colour, which nothing it reads is
|
|
22
|
+
* computed from. The core suite's `CO43` holds the scan to the public walk's
|
|
23
|
+
* numbers bit for bit, `CO44` what it leaves out to move none of them, and
|
|
24
|
+
* `CO45` that the vertex arrays it hands on uncopied are each pose's own.
|
|
25
|
+
* - **A bone's mode**: the mode the core's bone timelines pose at the time
|
|
26
|
+
* (`posedBones`, a fresh non-looping track: the time held at the duration),
|
|
27
|
+
* folded as the runtime's lookup folds it (`foldInheritMode`). `readModel`
|
|
28
|
+
* refuses a setup `inheritMode` and an `inherit` key that fold to no mode by
|
|
29
|
+
* name, so on this side every bone of every pose holds one and the answer is
|
|
30
|
+
* `null` — derived from the pose rather than assumed, so a reader that
|
|
31
|
+
* started admitting such a spelling would show up here as a bone with none.
|
|
32
|
+
* - **Under no skin set**, as `validate()` poses (`noSkinView`, the view
|
|
33
|
+
* measured in issue #1051: no skin's bones or constraints applied, a slot
|
|
34
|
+
* looked up in the default skin alone).
|
|
35
|
+
*
|
|
36
|
+
* Links nothing from the runtime.
|
|
37
|
+
*/
|
|
38
|
+
import { fileAnimationOrder } from '../../compile.ts';
|
|
39
|
+
import { foldInheritMode, type CompiledDocument } from '../../core/index.ts';
|
|
40
|
+
import { posedBones } from '../../core/animation.ts';
|
|
41
|
+
import { scanLoopingWalk, scanWalkSetup } from '../../core/walk.ts';
|
|
42
|
+
import type { ScanPose } from '../../core/raw.ts';
|
|
43
|
+
import { noSkinView } from '../../render_core.ts';
|
|
44
|
+
import type { ModelBone } from '../../model.ts';
|
|
45
|
+
import type { SteppedFrame, SteppedPoseFacts } from '../facts/stepped_poses.ts';
|
|
46
|
+
import type { ReadDocument } from './parse.ts';
|
|
47
|
+
|
|
48
|
+
/** A bone's posed mode as the facts spell it: `null` for a mode, else the value held. */
|
|
49
|
+
const modeOf = (bone: ModelBone): string | null => (foldInheritMode(bone.inheritMode ?? 'normal') === null ? String(bone.inheritMode) : null);
|
|
50
|
+
|
|
51
|
+
/** A channel the walk computed; a slot row holds `null` only where a rounding wrote one, which the walk does not. */
|
|
52
|
+
const channel = (v: number | null): number => (v === null ? Number.NaN : v);
|
|
53
|
+
|
|
54
|
+
/** Each bone's posed mode as the facts spell it (`modeOf`), by name. */
|
|
55
|
+
const modesOf = (posed: readonly ModelBone[]): ReadonlyMap<string, string | null> => new Map(posed.map((b) => [b.name, modeOf(b)]));
|
|
56
|
+
|
|
57
|
+
/** One scan pose as A10 reads it, each bone's mode taken from `modes` (`modesOf` the bones the timelines posed at the pose's time); the drawn attachments are the scan's own rows, already in the shape A10 reads. */
|
|
58
|
+
function frameOf(pose: ScanPose, modes: ReadonlyMap<string, string | null>): SteppedFrame {
|
|
59
|
+
return {
|
|
60
|
+
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 })),
|
|
61
|
+
drawn: pose.drawn,
|
|
62
|
+
slots: pose.slots.map((row) => ({
|
|
63
|
+
name: row[0],
|
|
64
|
+
colour: [channel(row[2]), channel(row[3]), channel(row[4]), channel(row[5])] as const,
|
|
65
|
+
dark: row[6] === null ? null : ([channel(row[6][0]), channel(row[6][1]), channel(row[6][2])] as const),
|
|
66
|
+
})),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function modelSteppedPoses(read: ReadDocument): SteppedPoseFacts {
|
|
71
|
+
let view: CompiledDocument | null = null;
|
|
72
|
+
const viewOf = (): CompiledDocument => (view ??= noSkinView(read.doc));
|
|
73
|
+
const animationOf = (name: string) => {
|
|
74
|
+
const anim = viewOf().animations.find((a) => a.name === name);
|
|
75
|
+
if (anim === undefined) throw new Error(`internal: the model side was asked to pose animation "${name}", which the document does not hold`);
|
|
76
|
+
return anim;
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
boneCount: read.doc.bones.length,
|
|
80
|
+
steppedAnimations: fileAnimationOrder(read.doc).flatMap((name) => {
|
|
81
|
+
const anim = read.doc.animations.find((a) => a.name === name);
|
|
82
|
+
return anim === undefined ? [] : [{ name, duration: anim.timelines.duration }];
|
|
83
|
+
}),
|
|
84
|
+
hasAnimation: (name) => read.doc.animations.some((a) => a.name === name),
|
|
85
|
+
hasBone: (name) => read.doc.bones.some((b) => b.name === name),
|
|
86
|
+
posedInherit: (animation, bone, time) => {
|
|
87
|
+
const anim = animationOf(animation);
|
|
88
|
+
const t = time < anim.timelines.duration ? time : anim.timelines.duration;
|
|
89
|
+
const posed = posedBones(viewOf(), anim.timelines, t).find((b) => b.name === bone);
|
|
90
|
+
return posed === undefined ? undefined : modeOf(posed);
|
|
91
|
+
},
|
|
92
|
+
// The document's stated mode — asked only of a bone posing none, which the reader leaves this side no document to hold.
|
|
93
|
+
statedInherit: (name) => `${JSON.stringify(read.doc.bones.find((b) => b.name === name)?.inheritMode)}`,
|
|
94
|
+
setup: () => frameOf(scanWalkSetup(viewOf()), modesOf(viewOf().bones)),
|
|
95
|
+
walk: (animation, step, frames) => {
|
|
96
|
+
const anim = animationOf(animation);
|
|
97
|
+
const poses = scanLoopingWalk(viewOf(), animation, new Array<number>(frames).fill(step));
|
|
98
|
+
// A bone's posed mode moves only by an `inherit` timeline: every other one `posedBones` reads leaves the setup's mode on its copy. So an animation
|
|
99
|
+
// keying none poses the setup's modes at every time, and they are read once rather than posed again at each of its frames (issue #1179).
|
|
100
|
+
const keysInherit = anim.timelines.bones.some((target) => target.timelines.some((tl) => tl.kind === 'inherit'));
|
|
101
|
+
const setupModes = keysInherit ? null : modesOf(viewOf().bones);
|
|
102
|
+
return poses.slice(1).map((pose) => frameOf(pose, setupModes ?? modesOf(posedBones(viewOf(), anim.timelines, pose.animationTime))));
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `PolygonFacts` (issue #1025, step 4c of #380):
|
|
3
|
+
* the document's bounding boxes, clipping attachments and paths, and the
|
|
4
|
+
* clip ends it states.
|
|
5
|
+
*
|
|
6
|
+
* - **The polygons** in the loaded walk's order, `fileOrderedEntries`
|
|
7
|
+
* (`./skin_entries.ts`). A polygon's `worldVerticesLength` is twice the
|
|
8
|
+
* `vertexCount` the record states — the number the emitter copies into the
|
|
9
|
+
* file, which the parser doubles into the length it reads the vertices to
|
|
10
|
+
* (`readVertices`); the selftest compares it with spine-core's on every
|
|
11
|
+
* call. A path's `closed` and `lengths` are the core's reading of the record
|
|
12
|
+
* (`readGeometry`: `closed` absent reads the parser's `false`).
|
|
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
|
|
18
|
+
* states an `end`, with the `end` as it states it.
|
|
19
|
+
*
|
|
20
|
+
* `encoding` is empty on every polygon: the document states the weighted
|
|
21
|
+
* form outright, so a run's decode has no subject here.
|
|
22
|
+
*
|
|
23
|
+
* Links nothing from the runtime.
|
|
24
|
+
*/
|
|
25
|
+
import { fileSkinOrder, skinsInFileOrder } from '../../compile.ts';
|
|
26
|
+
import { shownRow } from '../../core/index.ts';
|
|
27
|
+
import type { ClipEnd, PolygonEntry, PolygonFacts } from '../facts/vertex_polygons.ts';
|
|
28
|
+
import { isObj, type Json } from '../values.ts';
|
|
29
|
+
import { fileOrderedEntries } from './skin_entries.ts';
|
|
30
|
+
import type { ReadDocument } from './parse.ts';
|
|
31
|
+
|
|
32
|
+
/** The document's own record of one skin entry, as the JSON spells it. */
|
|
33
|
+
export function documentRecord(read: ReadDocument, skin: string, slot: string, placeholder: string): Json {
|
|
34
|
+
const skins = Array.isArray(read.json.skins) ? read.json.skins.filter(isObj) : [];
|
|
35
|
+
const table = skins.find((s) => s.name === skin)?.attachments;
|
|
36
|
+
const record = isObj(table) && isObj(table[slot]) ? (table[slot] as Json)[placeholder] : undefined;
|
|
37
|
+
if (!isObj(record)) throw new Error(`internal: skin "${skin}" slot "${slot}" placeholder "${placeholder}" is a record the reader returned and the document does not spell`);
|
|
38
|
+
return record;
|
|
39
|
+
}
|
|
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. */
|
|
42
|
+
export function fileSpelledEntries(read: ReadDocument): Array<{ skin: string; slot: string; placeholder: string; record: Json }> {
|
|
43
|
+
const out: Array<{ skin: string; slot: string; placeholder: string; record: Json }> = [];
|
|
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) });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const WHAT: Readonly<Record<string, string>> = { boundingbox: 'bounding box', clipping: 'clipping attachment', path: 'path' };
|
|
53
|
+
|
|
54
|
+
export function modelPolygonFacts(read: ReadDocument): PolygonFacts {
|
|
55
|
+
const polygons: PolygonEntry[] = [];
|
|
56
|
+
for (const entry of fileOrderedEntries(read)) {
|
|
57
|
+
const g = entry.record.geometry;
|
|
58
|
+
if (g === undefined || (g.kind !== 'boundingbox' && g.kind !== 'clipping' && g.kind !== 'path')) continue;
|
|
59
|
+
const stated = documentRecord(read, entry.skin, entry.slot, entry.placeholder).vertexCount;
|
|
60
|
+
polygons.push({
|
|
61
|
+
what: `${WHAT[g.kind]} "${shownRow(entry).name}"`,
|
|
62
|
+
worldVerticesLength: typeof stated === 'number' ? stated * 2 : Number.NaN,
|
|
63
|
+
path: g.kind === 'path' ? { closed: g.closed, lengths: g.lengths } : null,
|
|
64
|
+
encoding: [],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const clipEnds: ClipEnd[] = [];
|
|
68
|
+
for (const { slot, placeholder, record } of fileSpelledEntries(read)) {
|
|
69
|
+
if (record.kind === 'clipping' && record.end !== undefined) clipEnds.push({ placeholder, slot, end: record.end });
|
|
70
|
+
}
|
|
71
|
+
return { polygons, clipEnds, slots: read.doc.slots.map((s) => s.name) };
|
|
72
|
+
}
|