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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The skin a document is posed under (issue #932): every skin merged — the
|
|
3
|
+
* oracle's `--skin all` — or one skin by name, `--skin <name>`. Each rule
|
|
4
|
+
* below was measured by posing hand-written skeletons through
|
|
5
|
+
* `tools/pose_oracle.ts dump --skin <name>` (spine-core 4.3.13) and reading
|
|
6
|
+
* the rows it printed; the core suite's `CN` controls hold the same
|
|
7
|
+
* skeletons against the core at tolerance 0.
|
|
8
|
+
*
|
|
9
|
+
* - **What a slot shows under a named skin**: the named skin's record for
|
|
10
|
+
* the placeholder, else the default skin's, else nothing. Three skins
|
|
11
|
+
* `default`, `s1`, `s2` over five slots — a placeholder the default and
|
|
12
|
+
* `s1` and `s2` fill, one the default alone fills, one `s1` alone, one no
|
|
13
|
+
* skin, one `s1` and the default — dumped under `default`, `s1` and `s2`
|
|
14
|
+
* in two file orders (`default, s1, s2` and `s2, s1, default`): `s1`
|
|
15
|
+
* showed `s1`'s record where it filled the placeholder and the default's
|
|
16
|
+
* where only the default did; `s2` showed `s2`'s, then the default's, and
|
|
17
|
+
* nothing for the placeholder only `s1` fills; a placeholder no skin fills
|
|
18
|
+
* showed nothing under every skin; the two file orders read the same row
|
|
19
|
+
* under every named skin. An attachment key switching the slot at a
|
|
20
|
+
* sample resolves the same way. So a named skin has no order question:
|
|
21
|
+
* nothing a placeholder shows depends on the file's skin order, which the
|
|
22
|
+
* model does not hold (the slots' ⚠️ in `./index.ts`).
|
|
23
|
+
* - **The bones a named skin activates**: a skin-required bone is active
|
|
24
|
+
* exactly when the NAMED skin names it or a bone below it — the default
|
|
25
|
+
* skin's `bones` list does not count. Measured on the same probe: a
|
|
26
|
+
* skin-required bone only the default names read active under `default`
|
|
27
|
+
* and inactive under `s1` and `s2`; one `s1` names, and its skin-required
|
|
28
|
+
* parent, active under `s1` only; a bone that is not skin-required under
|
|
29
|
+
* an inactive one stayed active under every skin.
|
|
30
|
+
* - **The constraints a named skin applies**: a skin-required constraint of
|
|
31
|
+
* any kind is applied exactly when the named skin's list for its kind names
|
|
32
|
+
* it — the default skin's list does not count either (`./constraints.ts`'s
|
|
33
|
+
* header, the nine readings per kind).
|
|
34
|
+
*
|
|
35
|
+
* - **A slot on an inactive bone is not animated** (the commander's private
|
|
36
|
+
* finding on this issue: a 19-skin rig read DIFF on 18 of its 19 skin
|
|
37
|
+
* runs). A slot whose bone the applied skin leaves inactive keeps its
|
|
38
|
+
* setup colour, dark colour and attachment under every slot timeline —
|
|
39
|
+
* `rgba`, `rgb`, `alpha`, `rgba2`, `rgb2` and `attachment` — whether an
|
|
40
|
+
* animation plays it or a slider applies it, and a deform timeline on its
|
|
41
|
+
* attachment does not apply either (a mesh weighted to an ACTIVE bone, so
|
|
42
|
+
* the deform would show: moved by 5 under `all` and `s1`, unmoved under
|
|
43
|
+
* `default`, which leaves the slot's bone inactive). Measured on
|
|
44
|
+
* hand-written skeletons under `--skin all`, `s1` (naming the bone) and
|
|
45
|
+
* `default` (not): each timeline moved the slot under the first two and
|
|
46
|
+
* not under the third, and the setup row read the setup under all three
|
|
47
|
+
* (a slider's key included). A slot on an ACTIVE bone that shows nothing
|
|
48
|
+
* (no skin holds its placeholder) IS animated: every colour key applied,
|
|
49
|
+
* and an attachment key switched it. A draw-order key moves a slot on an
|
|
50
|
+
* inactive bone like any other. A sequence timeline is held by the same
|
|
51
|
+
* gate; its effect on such a slot is not observable in the dump — a
|
|
52
|
+
* region's corners on an inactive bone are all zeros whatever the frame.
|
|
53
|
+
* So the predicate is the slot bone's activity (`slotTimelinesApply`),
|
|
54
|
+
* not what the slot shows; under `all` every bone some skin names is
|
|
55
|
+
* active, so a one-skin rig is unchanged.
|
|
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
|
+
*
|
|
81
|
+
* Under `all` every skin is applied at once: `bones` and the constraint
|
|
82
|
+
* lists of every skin count, and a placeholder several skins fill shows the
|
|
83
|
+
* LAST of them in the Spine file's order — the case `./index.ts` leaves out
|
|
84
|
+
* by name, and the one the per-skin dumps exist to judge.
|
|
85
|
+
*/
|
|
86
|
+
import type { ModelSlot } from '../model.ts';
|
|
87
|
+
import type { CompiledDocument, CoreSkin } from './index.ts';
|
|
88
|
+
|
|
89
|
+
/** The skin option that merges every skin — the oracle's `--skin all`. */
|
|
90
|
+
export const CORE_ALL_SKINS = 'all';
|
|
91
|
+
|
|
92
|
+
/** The skin a document names its default skin with — Spine's `SkeletonData.defaultSkin` is the skin of this name. */
|
|
93
|
+
export const CORE_DEFAULT_SKIN = 'default';
|
|
94
|
+
|
|
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). */
|
|
96
|
+
export function appliedSkins(doc: CompiledDocument): CoreSkin[] {
|
|
97
|
+
if (doc.skin === null) return [];
|
|
98
|
+
return doc.skin === CORE_ALL_SKINS ? doc.skins : doc.skins.filter((k) => k.name === doc.skin);
|
|
99
|
+
}
|
|
100
|
+
|
|
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). */
|
|
102
|
+
export function lookupSkins(doc: CompiledDocument): CoreSkin[] {
|
|
103
|
+
if (doc.skin === CORE_ALL_SKINS) return doc.skins;
|
|
104
|
+
const named = doc.skin === null ? undefined : doc.skins.find((k) => k.name === doc.skin);
|
|
105
|
+
const fallback = doc.skin === CORE_DEFAULT_SKIN ? undefined : doc.skins.find((k) => k.name === CORE_DEFAULT_SKIN);
|
|
106
|
+
return [named, fallback].filter((k): k is CoreSkin => k !== undefined);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The skins that could show a slot's placeholder: under `all` every skin
|
|
111
|
+
* filling it — several is the file-order question — and under a named skin
|
|
112
|
+
* the one it resolves to, or none.
|
|
113
|
+
*/
|
|
114
|
+
export function fillingSkins(doc: CompiledDocument, slot: string, placeholder: string): string[] {
|
|
115
|
+
if (doc.skin === CORE_ALL_SKINS) return doc.skins.filter((k) => k.attachments[slot]?.[placeholder] !== undefined).map((k) => k.name);
|
|
116
|
+
const first = lookupSkins(doc).find((k) => k.attachments[slot]?.[placeholder] !== undefined);
|
|
117
|
+
return first === undefined ? [] : [first.name];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Whether a slot's timelines — its colour and attachment timelines, and the deform and sequence timelines on what it shows — apply: `slotTimelinesApply` unless a plant passes another. */
|
|
121
|
+
export type SlotTimelineGate = (doc: CompiledDocument, slot: ModelSlot, active: ReadonlySet<string>) => boolean;
|
|
122
|
+
|
|
123
|
+
/** A slot's timelines apply exactly when its bone is active under the skin posed (the header's measurement). */
|
|
124
|
+
export const slotTimelinesApply: SlotTimelineGate = (_doc, slot, active) => active.has(slot.bone);
|
|
125
|
+
|
|
126
|
+
/** Whether an applied skin's list for `kind` names the constraint — what applies a skin-required one (`./constraints.ts`'s header). */
|
|
127
|
+
export function listedByAppliedSkin(doc: CompiledDocument, kind: keyof CoreSkin['constraints'], name: string): boolean {
|
|
128
|
+
return appliedSkins(doc).some((k) => k.constraints[kind].includes(name));
|
|
129
|
+
}
|
package/src/core/uvs.ts
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The draw's last input the core lacked (issue #967, step 3c of issue #380):
|
|
3
|
+
* each drawn attachment's atlas PAGE and its PAGE UVs — the oracle's `uvs`
|
|
4
|
+
* block, what `src/render.ts`'s `pieceOf` reads off `sequence.regions[index]`
|
|
5
|
+
* and `sequence.getUVs(index)` — and `MeshAttachment.computeUVs`'s job, the
|
|
6
|
+
* mapping texture substitution calls, as pure functions.
|
|
7
|
+
*
|
|
8
|
+
* The page layout is not a model record (`ModelAtlasRect`'s 🔸 in
|
|
9
|
+
* `src/model.ts`: the page, `x`, `y` and `rotate` are the packer's
|
|
10
|
+
* arrangement, not the drawing). It reaches these rules as a lookup
|
|
11
|
+
* (`UvLookup`), so nothing here links the runtime or opens a file: since
|
|
12
|
+
* issue #1016 from the document's own `pages` section, which `build` spells
|
|
13
|
+
* from the atlas it writes (`documentPageLookup` below); for a
|
|
14
|
+
* `rigc-compiled/1` document, which has none, from the atlas beside it, read
|
|
15
|
+
* by rigc's own reader (`parseAtlasText` and `atlasRegionLookup` in
|
|
16
|
+
* `src/atlas.ts`).
|
|
17
|
+
*
|
|
18
|
+
* ## How every rule below was fixed
|
|
19
|
+
*
|
|
20
|
+
* By loading hand-written atlases and skeletons through spine-core 4.3.13
|
|
21
|
+
* (`TextureAtlas`, `AtlasAttachmentLoader`, `SkeletonJson`) and reading what
|
|
22
|
+
* the loaded attachments hold — `sequence.getUVs(i)`, `sequence.regions[i]`,
|
|
23
|
+
* `region.page.name` — and what `MeshAttachment.computeUVs` writes, then
|
|
24
|
+
* comparing a formula over the atlas's own numbers, bit for bit (not on the
|
|
25
|
+
* oracle's grid). The runtime's source was not read. Regions were random:
|
|
26
|
+
* pages of power-of-two and other sizes, one to three pages, `bounds` anywhere
|
|
27
|
+
* on the page, 70% trimmed (`offsets` other than `0, 0, width, height`), and
|
|
28
|
+
* `rotate` spelled `0`, `90`, `180`, `270`, `true`, `false` and, for the
|
|
29
|
+
* branch rules, `45`, `-90`, `360` and `450`; mesh and art UVs spelled with
|
|
30
|
+
* five non-float32 decimals, some outside `[0, 1]`.
|
|
31
|
+
*
|
|
32
|
+
* ### The region's own numbers
|
|
33
|
+
*
|
|
34
|
+
* With `W, H` the page's `size`, the region's `u = x/W`, `v = y/H`, and its far
|
|
35
|
+
* corner `u2 = (x + width)/W`, `v2 = (y + height)/H` — with `width` and
|
|
36
|
+
* `height` exchanged at `rotate: 90` (and `true`) ONLY. 1,500 of 1,500
|
|
37
|
+
* regions, as doubles; exchanged at 270 as well missed 262, never exchanged
|
|
38
|
+
* 484. (That 270 is not exchanged is the runtime's, and the reason
|
|
39
|
+
* `pageFootprint` in `src/atlas.ts` derives the page rectangle rather than
|
|
40
|
+
* reading these two numbers.)
|
|
41
|
+
*
|
|
42
|
+
* ### A region attachment's four UVs
|
|
43
|
+
*
|
|
44
|
+
* In the corner order of its world vertices (bottom-left, upper-left,
|
|
45
|
+
* upper-right, bottom-right — `./vertices.ts`): at `degrees === 90`
|
|
46
|
+
* `(u2, v2) (u, v2) (u, v) (u2, v)`, at every other value — 0, 180, 270 and
|
|
47
|
+
* the odd spellings alike — `(u, v2) (u, v) (u2, v) (u2, v2)`; each held as
|
|
48
|
+
* float32. 1,500 of 1,500 through `Math.fround`; as doubles, 575 missed. The
|
|
49
|
+
* readings rejected, on the same 1,500:
|
|
50
|
+
*
|
|
51
|
+
* | reading | misses |
|
|
52
|
+
* | --- | ---: |
|
|
53
|
+
* | 270 turned as 90 over a transposed rectangle | 262 |
|
|
54
|
+
* | 90's corners the other way round, `(u, v) (u2, v) (u2, v2) (u, v2)` | 487 |
|
|
55
|
+
* | 180 turned, `(u2, v) (u2, v2) (u, v2) (u, v)` | 260 |
|
|
56
|
+
* | the trim inset into the UVs | 724 |
|
|
57
|
+
*
|
|
58
|
+
* The trim does not enter a region's UVs: it enters its corners
|
|
59
|
+
* (`regionCorners` in `./vertices.ts`, through `ModelAtlasRect`).
|
|
60
|
+
*
|
|
61
|
+
* ### A mesh's page UVs — `MeshAttachment.computeUVs`
|
|
62
|
+
*
|
|
63
|
+
* From the attachment's own UVs `(s, t)` — art space, over the untrimmed
|
|
64
|
+
* drawing — per `degrees`, in doubles, starting from `u = x/W`, `v = y/H`:
|
|
65
|
+
*
|
|
66
|
+
* | degrees | `u −=` | `v −=` | span `w, h` | `(u′, v′)` |
|
|
67
|
+
* | --- | --- | --- | --- | --- |
|
|
68
|
+
* | 90 | `(oh − oy − height)/W` | `(ow − ox − width)/H` | `oh/W, ow/H` | `u + t·w, v + (1 − s)·h` |
|
|
69
|
+
* | 180 | `(ow − ox − width)/W` | `oy/H` | `ow/W, oh/H` | `u + (1 − s)·w, v + (1 − t)·h` |
|
|
70
|
+
* | 270 | `oy/W` | `ox/H` | `oh/W, ow/H` | `u + (1 − t)·w, v + s·h` |
|
|
71
|
+
* | any other | `ox/W` | `(oh − oy − height)/H` | `ow/W, oh/H` | `u + s·w, v + t·h` |
|
|
72
|
+
*
|
|
73
|
+
* (`ox, oy, ow, oh` the region's `offsetX, offsetY, originalWidth,
|
|
74
|
+
* originalHeight`; `width, height` its `bounds` size.) The subtraction is its
|
|
75
|
+
* own step: called into a plain array, the rule read 6,000 of 6,000 calls bit
|
|
76
|
+
* for bit in doubles, while one fraction `(x − ox)/W` missed 936 and texels
|
|
77
|
+
* first, `(x − ox + s·ow)/W`, 2,227. Into a plain array the function writes
|
|
78
|
+
* these doubles — 4,000 of 4,000 calls wrote a value that is not a float32 —
|
|
79
|
+
* and what an attachment HOLDS is each through `Math.fround`, because its
|
|
80
|
+
* array is a `Float32Array`: 1,500 of 1,500 meshes, and `computeUVs` over the
|
|
81
|
+
* attachment's region and own UVs reproduced the held array on all 1,500. The
|
|
82
|
+
* rule through `Math.fround` held on 4,000 direct calls into a `Float32Array`,
|
|
83
|
+
* 400 per spelling, the odd spellings taking the last row.
|
|
84
|
+
*
|
|
85
|
+
* ⚠️ **The mesh's own UVs are read as the doubles the text spells, not as
|
|
86
|
+
* float32.** Reading them through `Math.fround` first missed 311 of 1,500
|
|
87
|
+
* meshes (worst 4.8e-7, one float32 step of a UV near 1); every intermediate
|
|
88
|
+
* in float32 missed 844. So `computeUvs` takes the model's `uvs` as they are.
|
|
89
|
+
*
|
|
90
|
+
* ### Which region, on which page
|
|
91
|
+
*
|
|
92
|
+
* - A region or mesh draws the atlas region named by its `path`, else its
|
|
93
|
+
* name — the oracle's `path` cell (`shownRow` in `./index.ts`). The first
|
|
94
|
+
* region of that name in the file wins (two regions named alike, the first
|
|
95
|
+
* drawn), and the name is compared as the atlas line spells it: a region
|
|
96
|
+
* line `art ` is not found as `art`, while CRLF line ends are not part of a
|
|
97
|
+
* name (`parseAtlasText` splits on them).
|
|
98
|
+
* - A **sequence**'s frame `i` draws `path + (start + i)`, the number padded
|
|
99
|
+
* with zeros to `digits` (`padStart`): `start` 1 and `digits` 0 when
|
|
100
|
+
* unstated — a three-frame `x` drew `x1, x2, x3`; `start 8, digits 0` drew
|
|
101
|
+
* `x8, x9, x10`; `start 99, digits 1` `x99, x100`; `start 5, digits 4`
|
|
102
|
+
* `x0005, x0006`. The atlas's own `index:` field is not how a frame is
|
|
103
|
+
* found: regions named `seq` with `index: 1` and `2` loaded as `Region not
|
|
104
|
+
* found in atlas: seq1`. Each frame draws on its own region's page: a
|
|
105
|
+
* four-frame series over two pages drew frames 0–1 on the first and 2–3 on
|
|
106
|
+
* the second, a mesh's frames alike.
|
|
107
|
+
* - The frame drawn is the one the pose shows (`./deform.ts`'s sequence rule,
|
|
108
|
+
* measured there through a region's corners), or the sequence's `setup` (0
|
|
109
|
+
* unstated) where no timeline set one.
|
|
110
|
+
* - A **linked mesh** draws its OWN `path`'s region (and its own sequence's
|
|
111
|
+
* frames — a link stating one drew `l1, l2, l3` from setup frame 2 whatever
|
|
112
|
+
* its source stated; a link stating none drew its own path) over its
|
|
113
|
+
* SOURCE's UVs: a link on page `a.png` whose source sat on `b.png` drew
|
|
114
|
+
* `a.png`, and its uvs were `computeUVs` of its own region over the
|
|
115
|
+
* source's `regionUVs`.
|
|
116
|
+
* - The page cell is the page's name as `parseAtlasText` trims it — the
|
|
117
|
+
* runtime's `page.name` for a page line ` p.png ` read `p.png`.
|
|
118
|
+
*
|
|
119
|
+
* ## What is posed, and what is left out
|
|
120
|
+
*
|
|
121
|
+
* Rows in the pose's draw order, one per slot showing a region, a mesh or a
|
|
122
|
+
* linked mesh — the slots the `attachments` block lists, whatever their bone
|
|
123
|
+
* or alpha. Which record a slot shows and the frame its series is at are the
|
|
124
|
+
* ones `./deform.ts`'s `attachmentStates` computes for the same pose, from the
|
|
125
|
+
* placeholders the sample's own timelines leave (`posedSlots`) and the
|
|
126
|
+
* sliders' applications (`posedBoneWorld`); at setup, from the setup
|
|
127
|
+
* placeholders and the sliders applied to the setup pose. That repeats the
|
|
128
|
+
* walk `poseSetup` and `poseAnimations` make, because neither returns the
|
|
129
|
+
* frame (issue #966 retains it in the raw entry); the gate holds the two
|
|
130
|
+
* walks to one answer by comparing both against the runtime.
|
|
131
|
+
*
|
|
132
|
+
* A region the atlas does not have is refused by name (`CoreInputError`):
|
|
133
|
+
* spine-core refuses to load the same pair. The block is left out, with the
|
|
134
|
+
* reason, where what a slot shows is not posed — the slots or the draw order
|
|
135
|
+
* absent — and where a linked mesh stating its own sequence is stepped by a
|
|
136
|
+
* sequence timeline counted over its source's frames, a case never measured.
|
|
137
|
+
*
|
|
138
|
+
* ## Purity
|
|
139
|
+
*
|
|
140
|
+
* As the rest of the core: nothing from the Spine runtime package, nothing
|
|
141
|
+
* from `src/transform.ts`, no clock, no randomness, no I/O.
|
|
142
|
+
*/
|
|
143
|
+
import { activeBones, constraintRecords, CoreInputError, shownAttachment, type CompiledDocument, type CoreAnimation } from './index.ts';
|
|
144
|
+
import { posedBoneWorld, posedSlots } from './animation.ts';
|
|
145
|
+
import { applyConstraints } from './constraints.ts';
|
|
146
|
+
import type { SliderApplication } from './constraints_slider.ts';
|
|
147
|
+
import { attachmentStates } from './deform.ts';
|
|
148
|
+
import type { ShownGeometry } from './vertices.ts';
|
|
149
|
+
import type { ModelPage, ModelPageRegion } from '../model.ts';
|
|
150
|
+
import { worldTransforms } from './world.ts';
|
|
151
|
+
|
|
152
|
+
/** A region as these rules read it — `AtlasRegion` in `src/atlas.ts` is one, and so is spine-core's `TextureAtlasRegion`. */
|
|
153
|
+
export interface UvRegion {
|
|
154
|
+
x: number;
|
|
155
|
+
y: number;
|
|
156
|
+
width: number;
|
|
157
|
+
height: number;
|
|
158
|
+
offsetX: number;
|
|
159
|
+
offsetY: number;
|
|
160
|
+
originalWidth: number;
|
|
161
|
+
originalHeight: number;
|
|
162
|
+
degrees: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** A page as these rules read it: its trimmed name and its `size`. */
|
|
166
|
+
export interface UvPage {
|
|
167
|
+
name: string;
|
|
168
|
+
width: number;
|
|
169
|
+
height: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The region an atlas draws under a name, and its page — the first of that name, as the header states; `null` when there is none. */
|
|
173
|
+
export type UvLookup = (name: string) => { page: UvPage; region: UvRegion } | null;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The lookup over a document's `pages` section (issue #1016): the first region
|
|
177
|
+
* of a name in file order, pages in order and regions in each page's order —
|
|
178
|
+
* the order `pagesOfAtlas` in `src/model.ts` copies from the atlas, so this is
|
|
179
|
+
* `atlasRegionLookup` over the same text, with no atlas read. The name is
|
|
180
|
+
* compared as stored, untrimmed, as the header's *Which region* states.
|
|
181
|
+
*/
|
|
182
|
+
export function documentPageLookup(pages: readonly ModelPage[]): (name: string) => { page: UvPage; region: ModelPageRegion } | null {
|
|
183
|
+
const first = new Map<string, { page: UvPage; region: ModelPageRegion }>();
|
|
184
|
+
for (const page of pages) {
|
|
185
|
+
const at: UvPage = { name: page.name, width: page.width, height: page.height };
|
|
186
|
+
for (const region of page.regions) if (!first.has(region.name)) first.set(region.name, { page: at, region });
|
|
187
|
+
}
|
|
188
|
+
return (name) => first.get(name) ?? null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The readings the header's tables rejected, each a switch — what the core
|
|
193
|
+
* suite's `CU` plants pass to show the gate names it. The functions below with
|
|
194
|
+
* none set are the rules measured; nothing but a plant sets one.
|
|
195
|
+
*/
|
|
196
|
+
export interface UvReading {
|
|
197
|
+
/** A region at 90 in the other corner order, `(u, v) (u2, v) (u2, v2) (u, v2)` — the rotation flipped. */
|
|
198
|
+
rotationFlipped?: boolean;
|
|
199
|
+
/** A region at 270 turned as one at 90, over a transposed rectangle; a mesh at 270 mapped by 90's `(u + t·w, v + (1 − s)·h)`. */
|
|
200
|
+
turned270?: boolean;
|
|
201
|
+
/** A mesh's page UVs with the trim offsets dropped (`ox = oy = 0`, the original size the kept one). */
|
|
202
|
+
trimDropped?: boolean;
|
|
203
|
+
/** A mesh's own UVs read through `Math.fround` first. */
|
|
204
|
+
artAsFloat32?: boolean;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** A region attachment's four page UVs as the runtime holds them — the header's rule, in the world corners' order. */
|
|
208
|
+
export function regionPageUvs(region: UvRegion, page: UvPage, reading: UvReading = {}): number[] {
|
|
209
|
+
const turned = region.degrees === 90 || (reading.turned270 === true && region.degrees === 270);
|
|
210
|
+
const u = region.x / page.width;
|
|
211
|
+
const v = region.y / page.height;
|
|
212
|
+
const u2 = (region.x + (turned ? region.height : region.width)) / page.width;
|
|
213
|
+
const v2 = (region.y + (turned ? region.width : region.height)) / page.height;
|
|
214
|
+
let out: number[];
|
|
215
|
+
if (turned) out = reading.rotationFlipped === true ? [u, v, u2, v, u2, v2, u, v2] : [u2, v2, u, v2, u, v, u2, v];
|
|
216
|
+
else out = [u, v2, u, v, u2, v, u2, v2];
|
|
217
|
+
return out.map(Math.fround);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* `MeshAttachment.computeUVs`: art-space UVs over the untrimmed drawing mapped
|
|
222
|
+
* onto `region` of `page`, in doubles — the header's table. What texture
|
|
223
|
+
* substitution writes into its plain array; an attachment holds each value
|
|
224
|
+
* through `Math.fround` (`meshPageUvs`).
|
|
225
|
+
*/
|
|
226
|
+
export function computeUvs(region: UvRegion, page: UvPage, art: readonly number[], reading: UvReading = {}): number[] {
|
|
227
|
+
const dropped = reading.trimDropped === true;
|
|
228
|
+
const ox = dropped ? 0 : region.offsetX;
|
|
229
|
+
const oy = dropped ? 0 : region.offsetY;
|
|
230
|
+
const ow = dropped ? region.width : region.originalWidth;
|
|
231
|
+
const oh = dropped ? region.height : region.originalHeight;
|
|
232
|
+
const W = page.width;
|
|
233
|
+
const H = page.height;
|
|
234
|
+
const s = reading.artAsFloat32 === true ? art.map(Math.fround) : art;
|
|
235
|
+
let u = region.x / W;
|
|
236
|
+
let v = region.y / H;
|
|
237
|
+
const out: number[] = [];
|
|
238
|
+
switch (region.degrees) {
|
|
239
|
+
case 90: {
|
|
240
|
+
u -= (oh - oy - region.height) / W;
|
|
241
|
+
v -= (ow - ox - region.width) / H;
|
|
242
|
+
const w = oh / W;
|
|
243
|
+
const h = ow / H;
|
|
244
|
+
for (let i = 0; i + 1 < s.length; i += 2) out.push(u + s[i + 1] * w, v + (1 - s[i]) * h);
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
case 180: {
|
|
248
|
+
u -= (ow - ox - region.width) / W;
|
|
249
|
+
v -= oy / H;
|
|
250
|
+
const w = ow / W;
|
|
251
|
+
const h = oh / H;
|
|
252
|
+
for (let i = 0; i + 1 < s.length; i += 2) out.push(u + (1 - s[i]) * w, v + (1 - s[i + 1]) * h);
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
case 270: {
|
|
256
|
+
u -= oy / W;
|
|
257
|
+
v -= ox / H;
|
|
258
|
+
const w = oh / W;
|
|
259
|
+
const h = ow / H;
|
|
260
|
+
if (reading.turned270 === true) for (let i = 0; i + 1 < s.length; i += 2) out.push(u + s[i + 1] * w, v + (1 - s[i]) * h);
|
|
261
|
+
else for (let i = 0; i + 1 < s.length; i += 2) out.push(u + (1 - s[i + 1]) * w, v + s[i] * h);
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
default: {
|
|
265
|
+
u -= ox / W;
|
|
266
|
+
v -= (oh - oy - region.height) / H;
|
|
267
|
+
const w = ow / W;
|
|
268
|
+
const h = oh / H;
|
|
269
|
+
for (let i = 0; i + 1 < s.length; i += 2) out.push(u + s[i] * w, v + s[i + 1] * h);
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** A mesh's page UVs as the attachment holds them: `computeUvs`, each through `Math.fround`. */
|
|
276
|
+
export function meshPageUvs(region: UvRegion, page: UvPage, art: readonly number[], reading: UvReading = {}): number[] {
|
|
277
|
+
return computeUvs(region, page, art, reading).map(Math.fround);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** A record's series, as the parser reads the four fields: `start` 1, `digits` 0 and `setup` 0 when unstated. */
|
|
281
|
+
export interface UvSequence {
|
|
282
|
+
count: number;
|
|
283
|
+
start: number;
|
|
284
|
+
digits: number;
|
|
285
|
+
setup: number;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Every record's series, by `skin/slot/placeholder` — `readUvSequences`. */
|
|
289
|
+
export type UvSequences = ReadonlyMap<string, UvSequence>;
|
|
290
|
+
|
|
291
|
+
/** The region a series' frame draws: `path + (start + frame)`, padded with zeros to `digits` — the header's rule. */
|
|
292
|
+
export function frameRegionName(path: string, sequence: UvSequence, frame: number): string {
|
|
293
|
+
return path + String(sequence.start + frame).padStart(sequence.digits, '0');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
297
|
+
const whole = (v: unknown, min: number): v is number => typeof v === 'number' && Number.isInteger(v) && v >= min;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Every record's `sequence` in a `rigc-compiled/1` document, as the parser
|
|
301
|
+
* reads it — `readModel` keeps a series' count and nothing else of it, and a
|
|
302
|
+
* frame's region name needs `start` and `digits`. Refuses, naming the path, a
|
|
303
|
+
* field that is not a whole number (`count` of at least 1, the others of at
|
|
304
|
+
* least 0) or a `setup` that is not one of the frames.
|
|
305
|
+
*/
|
|
306
|
+
export function readUvSequences(document: unknown): UvSequences {
|
|
307
|
+
const out = new Map<string, UvSequence>();
|
|
308
|
+
const problems: string[] = [];
|
|
309
|
+
const skins = isRecord(document) && Array.isArray(document.skins) ? document.skins : [];
|
|
310
|
+
skins.forEach((skin: unknown, k: number) => {
|
|
311
|
+
if (!isRecord(skin) || typeof skin.name !== 'string' || !isRecord(skin.attachments)) return;
|
|
312
|
+
for (const [slot, table] of Object.entries(skin.attachments)) {
|
|
313
|
+
if (!isRecord(table)) continue;
|
|
314
|
+
for (const [placeholder, record] of Object.entries(table)) {
|
|
315
|
+
if (!isRecord(record) || record.sequence === undefined) continue;
|
|
316
|
+
const where = `skins[${k}] "${skin.name}".attachments["${slot}"]["${placeholder}"].sequence`;
|
|
317
|
+
const seq = record.sequence;
|
|
318
|
+
if (!isRecord(seq)) {
|
|
319
|
+
problems.push(`${where} is not an object`);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const before = problems.length;
|
|
323
|
+
if (!whole(seq.count, 1)) problems.push(`${where}: count is ${JSON.stringify(seq.count) ?? 'absent'}, not a whole number of at least 1`);
|
|
324
|
+
for (const key of ['start', 'digits', 'setup'] as const) if (seq[key] !== undefined && !whole(seq[key], 0)) problems.push(`${where}: ${key} is ${JSON.stringify(seq[key])}, not a whole number of at least 0`);
|
|
325
|
+
if (problems.length !== before) continue;
|
|
326
|
+
const count = seq.count as number;
|
|
327
|
+
const setup = (seq.setup as number | undefined) ?? 0;
|
|
328
|
+
if (setup >= count) {
|
|
329
|
+
problems.push(`${where}: setup is ${setup}, and a ${count}-frame series has frames 0 to ${count - 1}`);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
out.set(`${skin.name}/${slot}/${placeholder}`, { count, start: (seq.start as number | undefined) ?? 1, digits: (seq.digits as number | undefined) ?? 0, setup });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
if (problems.length > 0) throw new CoreInputError(`the model document's sequences: ${problems.join('; ')}`);
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** What `poseUvs` reads besides the pose: the atlas lookup, the document's series, and a plant's reading. */
|
|
341
|
+
export interface UvSource {
|
|
342
|
+
lookup: UvLookup;
|
|
343
|
+
sequences: UvSequences;
|
|
344
|
+
reading?: UvReading;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** One row of `uvs`: `[slot, attachment, page, uvs]`, the oracle's row. */
|
|
348
|
+
export type CoreUvRow = [string, string, string, Array<number | null>];
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Each slot's shown record and its series' frame at the setup pose — what
|
|
352
|
+
* `poseSetup` hands `poseGeometry`: the setup placeholders, and the sliders
|
|
353
|
+
* applied to the setup bones (`applyConstraints`), each slider's attachment,
|
|
354
|
+
* deform and sequence keys applied in order by `attachmentStates`.
|
|
355
|
+
*/
|
|
356
|
+
export function shownAtSetup(doc: CompiledDocument): { shown: ShownGeometry[]; why: string[] } {
|
|
357
|
+
const active = activeBones(doc);
|
|
358
|
+
const applied: SliderApplication[] = [];
|
|
359
|
+
applyConstraints(doc.bones, worldTransforms(doc.bones, active), active, constraintRecords(doc), null, applied);
|
|
360
|
+
const placeholders = new Map(doc.slots.map((s) => [s.name, s.setup]));
|
|
361
|
+
return attachmentStates(doc, shownAttachment, placeholders, null, applied);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Each slot's shown record and its series' frame at time `t` of `anim` — what
|
|
366
|
+
* `poseAnimations` hands `poseGeometry` under `--physics none`: the sliders
|
|
367
|
+
* `posedBoneWorld` applies at `t`, the placeholders the sample's own
|
|
368
|
+
* timelines leave (`posedSlots`), then `attachmentStates`.
|
|
369
|
+
*/
|
|
370
|
+
export function shownAtSample(doc: CompiledDocument, anim: CoreAnimation, t: number): { shown: ShownGeometry[]; why: string[] } {
|
|
371
|
+
const sliders: SliderApplication[] = [];
|
|
372
|
+
// The bones are posed for what the sliders apply, and only a slider constraint applies one (`applySlider`): with none, the list stays empty and the pose would be read for nothing (issue #1134).
|
|
373
|
+
if (doc.constraints.some((c) => c.kind === 'slider')) posedBoneWorld(doc, anim.timelines, t, {}, anim.constraints, sliders);
|
|
374
|
+
const placeholders = new Map<string, string | null>();
|
|
375
|
+
posedSlots(doc, anim.timelines, t, {}, sliders, placeholders);
|
|
376
|
+
return attachmentStates(doc, shownAttachment, placeholders, { timelines: anim.timelines, t }, sliders);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** One drawn attachment resolved to its atlas region: what `poseUvs` maps, and what a census reads. */
|
|
380
|
+
export interface DrawnRegion {
|
|
381
|
+
slot: string;
|
|
382
|
+
/** The attachment's name — the row's second cell. */
|
|
383
|
+
name: string;
|
|
384
|
+
kind: 'region' | 'mesh' | 'linkedmesh';
|
|
385
|
+
/** The region name drawn: the path, with the frame's number for a series. */
|
|
386
|
+
region: string;
|
|
387
|
+
/** The series' frame drawn, or `null` for a record with no series. */
|
|
388
|
+
frame: number | null;
|
|
389
|
+
found: { page: UvPage; region: UvRegion };
|
|
390
|
+
/** The attachment's own UVs for a mesh — a linked mesh's source's — or `null` for a region. */
|
|
391
|
+
art: readonly number[] | null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Every slot of `order` showing a region, a mesh or a linked mesh, resolved to
|
|
396
|
+
* the atlas region it draws — the header's *Which region, on which page* — or
|
|
397
|
+
* why the pose is not posed. A region the atlas lacks is refused by name.
|
|
398
|
+
*/
|
|
399
|
+
export function drawnRegions(doc: CompiledDocument, shown: readonly ShownGeometry[], order: readonly string[], source: UvSource): { drawn: DrawnRegion[] | null; why: string | null } {
|
|
400
|
+
const bySlot = new Map(shown.map((s) => [s.slot, s]));
|
|
401
|
+
const drawn: DrawnRegion[] = [];
|
|
402
|
+
const unmeasured: string[] = [];
|
|
403
|
+
for (const slot of order) {
|
|
404
|
+
const s = bySlot.get(slot);
|
|
405
|
+
if (s === undefined) continue;
|
|
406
|
+
const g = s.geometry;
|
|
407
|
+
if (g.kind !== 'region' && g.kind !== 'mesh' && g.kind !== 'linkedmesh') continue;
|
|
408
|
+
const record = doc.skins.find((k) => k.name === s.skin)?.attachments[s.slot]?.[s.placeholder];
|
|
409
|
+
if (record === undefined) throw new CoreInputError(`slot "${s.slot}": skin "${s.skin}" files no record under placeholder "${s.placeholder}" (readModel refuses it first)`);
|
|
410
|
+
const path = record.path ?? s.name;
|
|
411
|
+
const sequence = source.sequences.get(`${s.skin}/${s.slot}/${s.placeholder}`);
|
|
412
|
+
if (g.kind === 'linkedmesh' && sequence !== undefined && s.frame !== undefined) {
|
|
413
|
+
const sourceCount = doc.skins.find((k) => k.name === g.skin)?.attachments[g.slot]?.[g.source]?.sequenceCount;
|
|
414
|
+
if (sourceCount !== sequence.count) {
|
|
415
|
+
unmeasured.push(`slot "${s.slot}" shows linked mesh "${s.name}", whose own ${sequence.count}-frame series a sequence timeline steps over its source's ${sourceCount ?? 'unstated'} frame(s)`);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const frame = sequence === undefined ? null : (s.frame ?? sequence.setup);
|
|
420
|
+
const name = sequence === undefined || frame === null ? path : frameRegionName(path, sequence, frame);
|
|
421
|
+
const found = source.lookup(name);
|
|
422
|
+
if (found === null) {
|
|
423
|
+
throw new CoreInputError(
|
|
424
|
+
`slot "${s.slot}" shows "${s.name}", which draws atlas region "${name}"${frame === null ? '' : ` (frame ${frame} of its series over "${path}")`}, and the atlas has no region of that name — spine-core refuses to load the pair ("Region not found in atlas")`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
let art: readonly number[] | null = null;
|
|
428
|
+
if (g.kind === 'mesh') art = g.uvs;
|
|
429
|
+
else if (g.kind === 'linkedmesh') {
|
|
430
|
+
// A linked mesh draws its source's UVs over its own region (the header's *Which region*).
|
|
431
|
+
const from = doc.skins.find((k) => k.name === g.skin)?.attachments[g.slot]?.[g.source]?.geometry;
|
|
432
|
+
if (from?.kind !== 'mesh') throw new CoreInputError(`slot "${s.slot}": the linked mesh's source "${g.source}" carries no uvs (readModel refuses it first)`);
|
|
433
|
+
art = from.uvs;
|
|
434
|
+
}
|
|
435
|
+
drawn.push({ slot: s.slot, name: s.name, kind: g.kind, region: name, frame, found, art });
|
|
436
|
+
}
|
|
437
|
+
if (unmeasured.length > 0) return { drawn: null, why: `${unmeasured.join('; ')} — which of the two counts the runtime steps it over was not measured` };
|
|
438
|
+
return { drawn, why: null };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The `uvs` rows of one pose: `shown` (from `shownAtSetup`/`shownAtSample`) in
|
|
443
|
+
* `order`, the pose's draw order, every number through `round` (the oracle's,
|
|
444
|
+
* `gridRound`) — or why the block is left out. A region the atlas lacks is
|
|
445
|
+
* refused by name.
|
|
446
|
+
*/
|
|
447
|
+
export function poseUvs(doc: CompiledDocument, shown: readonly ShownGeometry[], order: readonly string[], source: UvSource, round: (v: number) => number | null): { rows: CoreUvRow[] | null; why: string | null } {
|
|
448
|
+
const reading = source.reading ?? {};
|
|
449
|
+
const { drawn, why } = drawnRegions(doc, shown, order, source);
|
|
450
|
+
if (drawn === null) return { rows: null, why };
|
|
451
|
+
const rows = drawn.map((d): CoreUvRow => {
|
|
452
|
+
const uvs = d.art === null ? regionPageUvs(d.found.region, d.found.page, reading) : meshPageUvs(d.found.region, d.found.page, d.art, reading);
|
|
453
|
+
return [d.slot, d.name, d.found.page.name, uvs.map(round)];
|
|
454
|
+
});
|
|
455
|
+
return { rows, why: null };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Why a document's `uvs` blocks are not posed under the stepped phase, or null: a slider keying a slot's attachment or a series — its time is read off bones the step moves, and this walk poses them unstepped. */
|
|
459
|
+
export function steppedUvsWhy(doc: CompiledDocument): string | null {
|
|
460
|
+
const keyed = doc.constraints.flatMap((c) => {
|
|
461
|
+
if (c.kind !== 'slider') return [];
|
|
462
|
+
const anim = doc.animations.find((a) => a.name === c.animation);
|
|
463
|
+
if (anim === undefined) return [];
|
|
464
|
+
const switches = anim.timelines.slots.some((sl) => sl.timelines.some((tl) => tl.kind === 'attachment'));
|
|
465
|
+
const frames = anim.timelines.attachments.some((a) => a.sequence !== null);
|
|
466
|
+
return switches || frames ? [`slider "${c.name}" applies animation "${c.animation}", which keys ${switches ? 'a slot\'s attachment' : 'a sequence'}`] : [];
|
|
467
|
+
});
|
|
468
|
+
return keyed.length === 0 ? null : `${keyed.join('; ')} — under --physics step its time is read off stepped bones, and the uvs walk poses the sliders unstepped`;
|
|
469
|
+
}
|