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
package/src/render.ts
ADDED
|
@@ -0,0 +1,1013 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rasteriser — one code path for reference frames and for candidates.
|
|
3
|
+
*
|
|
4
|
+
* ⭐ Why this is a module and not a script. `bench/render_reference.ts` renders
|
|
5
|
+
* the official export to the PNG frames an authoring agent is allowed to see;
|
|
6
|
+
* `rigc check` renders the agent's own candidate and compares it against those
|
|
7
|
+
* frames. If those two drew pixels differently, every number `check` reports
|
|
8
|
+
* would carry the difference between two renderers on top of the difference
|
|
9
|
+
* between two rigs — and the second is the only one anybody wants to read. So
|
|
10
|
+
* there is exactly one rasteriser, and both callers are thin.
|
|
11
|
+
*
|
|
12
|
+
* ## What it draws
|
|
13
|
+
*
|
|
14
|
+
* Region and mesh attachments, in draw order, tinted by slot colour x attachment
|
|
15
|
+
* colour. Both are **affine** texture maps and neither divides by w:
|
|
16
|
+
*
|
|
17
|
+
* - a **region** is one quad. `spine-core` computes its four world vertices on
|
|
18
|
+
* the CPU, a destination pixel maps back into the region's rectangle by
|
|
19
|
+
* inverting one 2x2, and there is no triangle split at all;
|
|
20
|
+
* - a **mesh** is a triangle list. `MeshAttachment.computeWorldVertices` does the
|
|
21
|
+
* work — it is the runtime's own routine, so weighted vertices resolve through
|
|
22
|
+
* their bones and a `deform` timeline's offsets are applied, exactly the way a
|
|
23
|
+
* real runtime would. Each triangle is then filled with barycentric UV
|
|
24
|
+
* interpolation.
|
|
25
|
+
*
|
|
26
|
+
* A **clipping attachment** draws no pixel of its own and removes the pixels of
|
|
27
|
+
* every slot from the one carrying it through its `end` slot. `piecesOf` runs
|
|
28
|
+
* spine-core's own `SkeletonClipping` beside the draw-order walk, in the call
|
|
29
|
+
* sequence spine-webgl's `SkeletonRenderer.draw` runs, so a slot inside a clip
|
|
30
|
+
* reaches the rasteriser as the geometry the runtime draws rather than as the
|
|
31
|
+
* attachment's whole (issue #844).
|
|
32
|
+
*
|
|
33
|
+
* ⭐ **Sampling is bilinear on both paths, and the source is straight alpha —
|
|
34
|
+
* so the interpolation is premultiplied.** One filter rather than two is not a
|
|
35
|
+
* detail: `check` measures a candidate against reference frames, and a mesh
|
|
36
|
+
* triangle sampled nearest against a reference sampled bilinear would put a
|
|
37
|
+
* filter difference into the residual where only a rig difference belongs.
|
|
38
|
+
* Bilinear rather than nearest because the region path was already bilinear and
|
|
39
|
+
* the five committed rungs are rendered with it.
|
|
40
|
+
*
|
|
41
|
+
* Straight alpha is a property of the SOURCE, not a licence to average it
|
|
42
|
+
* channel by channel: a transparent texel's `(0, 0, 0, 0)` is the absence of a
|
|
43
|
+
* colour, and giving it a vote drew a dark rim along every region edge — over
|
|
44
|
+
* the top of whatever was behind the part, and into `check`'s residual on the
|
|
45
|
+
* candidate side. `bilinear` weights each colour by its own alpha and divides
|
|
46
|
+
* back out; see it for what that does and does not move (issue #292).
|
|
47
|
+
*
|
|
48
|
+
* ⚠️ **Region rasterising is untouched by the mesh path**, deliberately. A region
|
|
49
|
+
* could be drawn as two triangles and very nearly the same pixels would come out;
|
|
50
|
+
* "very nearly" would have silently rewritten five rungs of committed reference
|
|
51
|
+
* frames. `rasteriseQuad` still owns regions, `rasteriseMesh` owns meshes, and
|
|
52
|
+
* `rasterisePiece` picks.
|
|
53
|
+
*
|
|
54
|
+
* ## The fill rule, and why a mesh needs one
|
|
55
|
+
*
|
|
56
|
+
* Two triangles that share an edge must cover the pixels along it exactly once.
|
|
57
|
+
* Include the boundary in both and every interior edge of a mesh blends twice —
|
|
58
|
+
* a visible lattice of seams wherever the art is not opaque. Exclude it in both
|
|
59
|
+
* and the seams become holes.
|
|
60
|
+
*
|
|
61
|
+
* So `rasteriseMesh` normalises each triangle's winding and applies the standard
|
|
62
|
+
* **top-left rule**: a pixel centre exactly on an edge belongs to the triangle
|
|
63
|
+
* only when that edge is a top or a left one. The two triangles sharing an edge
|
|
64
|
+
* traverse it in opposite directions, so exactly one of them calls it top-left —
|
|
65
|
+
* which is the property that makes the rule watertight without an epsilon.
|
|
66
|
+
*
|
|
67
|
+
* ## Two conventions this file owns
|
|
68
|
+
*
|
|
69
|
+
* - **Spine world is y up; an image is y down.** The projection from world to
|
|
70
|
+
* frame pixels lives in `projector` and nowhere else.
|
|
71
|
+
* - **The framing box is measured at `FRAMING_FPS`, whatever rate frames are
|
|
72
|
+
* written at.** The union of the posed vertices depends on WHICH TIMES you
|
|
73
|
+
* sample, so taking it at the output rate made the viewport a property of the
|
|
74
|
+
* rate: rung 1's `balls` framed to 256x240 at 12 fps and 256x239 at 24 fps.
|
|
75
|
+
* One pixel is enough to be a trap — the two sets look comparable, an author
|
|
76
|
+
* measures a distance in one and a time in the other, and the scale between
|
|
77
|
+
* them is silently off.
|
|
78
|
+
*
|
|
79
|
+
* ⚠️ Two notes on where this sits. It imports `spine-core`, which `src/` is
|
|
80
|
+
* otherwise careful about: posing a skeleton *is* running the runtime, and there
|
|
81
|
+
* is no honest way to render one without it. (Since issue #968 that holds for a
|
|
82
|
+
* Spine export: a rigc build is posed by rigc's own core through
|
|
83
|
+
* `./render_core.ts`, and this file keeps the runtime for the export's poser,
|
|
84
|
+
* the export's atlas pages and texture substitution — see *which poser* below.
|
|
85
|
+
* Since issue #1014 a rigc build the core poses touches none of it: what a
|
|
86
|
+
* render reads off the candidate besides the pose comes from its model
|
|
87
|
+
* document and the skeleton's own JSON (`loadCandidate`), and since issue
|
|
88
|
+
* #1020 its pages and a `--texture-from` atlas through rigc's own reader, so
|
|
89
|
+
* spine-core is reached only where it poses an export or a fallback.) The rule that matters is unchanged
|
|
90
|
+
* — `src/compile.ts` must stay independent of the runtime so the compiler and
|
|
91
|
+
* the gate are not checking each other's assumptions — and this file is neither.
|
|
92
|
+
* It also imports `tools/plate.ts` for the PNG codec, which is dependency-free.
|
|
93
|
+
*
|
|
94
|
+
* ## What is here, and what is not (issue #1052)
|
|
95
|
+
*
|
|
96
|
+
* Only what names the runtime: spine-core's implementation of the posing seam
|
|
97
|
+
* (`spinePoser`), loading a Spine export (`posableFromText`, `loadPosedSkeleton`,
|
|
98
|
+
* and the half of `loadCandidate` an export takes), the atlas class's readings
|
|
99
|
+
* (`atlasPageNames`, the `spine` reader of a texture substitution), and what
|
|
100
|
+
* reads a posed spine-core skeleton (`piecesOf`, `boneSnapshots`,
|
|
101
|
+
* `posedNumbersOf`, the rest table). Everything else — the frame-set contract,
|
|
102
|
+
* the samplers, the candidate and its poser choice, the geometry export, the
|
|
103
|
+
* framing and the rasteriser — is `./render_shared.ts`, which links nothing of
|
|
104
|
+
* the runtime, and every name it holds that this file exported is re-exported
|
|
105
|
+
* from here. That module reaches this one's half through the seam
|
|
106
|
+
* (`./spine_side.ts`): loading this file registers it (`registerSpinePosing`,
|
|
107
|
+
* at the bottom), so every program that imports it renders exactly as before,
|
|
108
|
+
* and one that does not refuses an export by name.
|
|
109
|
+
*/
|
|
110
|
+
import {
|
|
111
|
+
AnimationState,
|
|
112
|
+
AnimationStateData,
|
|
113
|
+
AtlasAttachmentLoader,
|
|
114
|
+
ClippingAttachment,
|
|
115
|
+
MeshAttachment,
|
|
116
|
+
Physics,
|
|
117
|
+
RegionAttachment,
|
|
118
|
+
Skeleton,
|
|
119
|
+
SkeletonClipping,
|
|
120
|
+
SkeletonJson,
|
|
121
|
+
TextureAtlas,
|
|
122
|
+
TextureAtlasRegion,
|
|
123
|
+
SkeletonData,
|
|
124
|
+
type Slot,
|
|
125
|
+
} from '@esotericsoftware/spine-core';
|
|
126
|
+
import { readFileSync } from 'node:fs';
|
|
127
|
+
// `dirname` is read by no body here today: RC23's `bonedist-pages` plant puts a `loadPosable(…, dirname(atlasPath))` back into
|
|
128
|
+
// `loadPosedSkeleton`, and kept, the plant reads red for its own reason rather than for a missing name.
|
|
129
|
+
import { dirname, join } from 'node:path';
|
|
130
|
+
import { Plate, readPlate } from '../tools/plate.ts';
|
|
131
|
+
import { clipSourceOf, corePoser, inactiveBoneSnapshot, subsetOver, unposedBones } from './render_core.ts';
|
|
132
|
+
import { type PosedVertices, type WorldTransform } from './nonfinite.ts';
|
|
133
|
+
import {
|
|
134
|
+
atlasScales,
|
|
135
|
+
choosePosers,
|
|
136
|
+
pairRefusal,
|
|
137
|
+
refuseUnchosen,
|
|
138
|
+
regionKey,
|
|
139
|
+
type AttachmentPose,
|
|
140
|
+
type AttachmentRest,
|
|
141
|
+
type BoneSnapshot,
|
|
142
|
+
type DrawOptions,
|
|
143
|
+
type MakeCorePoser,
|
|
144
|
+
type Piece,
|
|
145
|
+
type PieceTexture,
|
|
146
|
+
type PoseOptions,
|
|
147
|
+
type Posed,
|
|
148
|
+
type Poser,
|
|
149
|
+
type PoserChoice,
|
|
150
|
+
type PoserName,
|
|
151
|
+
type SkeletonFacts,
|
|
152
|
+
type SkinRoster,
|
|
153
|
+
type SlotSubset,
|
|
154
|
+
type SubstituteRegion,
|
|
155
|
+
} from './render_shared.ts';
|
|
156
|
+
import { POSING_RUNTIME_TAIL, registerSpinePosing, SpineRuntimeError, spineRuntimeSentence } from './spine_side.ts';
|
|
157
|
+
|
|
158
|
+
// Every name `./render_shared.ts` holds that this file exported before issue #1052, re-exported so a dependant's
|
|
159
|
+
// import resolves where it always did.
|
|
160
|
+
export {
|
|
161
|
+
BACKGROUND,
|
|
162
|
+
CandidateAtlasError,
|
|
163
|
+
CandidatePairError,
|
|
164
|
+
EMPTY_FOOTPRINT,
|
|
165
|
+
FRAMES_SIDECAR,
|
|
166
|
+
FRAMES_SPEC,
|
|
167
|
+
FRAMING_FPS,
|
|
168
|
+
GEOMETRY_COORDINATES,
|
|
169
|
+
GEOMETRY_FILE,
|
|
170
|
+
GEOMETRY_SPEC,
|
|
171
|
+
GeometryError,
|
|
172
|
+
PAD,
|
|
173
|
+
POSER_NAMES,
|
|
174
|
+
PROTOCOL_FPS,
|
|
175
|
+
PoserChoiceError,
|
|
176
|
+
SETUP_POSE_DIR,
|
|
177
|
+
SHEET_COLUMNS,
|
|
178
|
+
SHEET_FILE,
|
|
179
|
+
SHEET_GAP,
|
|
180
|
+
SHEET_LABEL,
|
|
181
|
+
SHEET_RULE,
|
|
182
|
+
SHEET_TILE,
|
|
183
|
+
SUBSTITUTE_PAGE,
|
|
184
|
+
SlotSubsetError,
|
|
185
|
+
UnframeablePoseError,
|
|
186
|
+
atlasScales,
|
|
187
|
+
bilinear,
|
|
188
|
+
bilinearChannels,
|
|
189
|
+
blitPiece,
|
|
190
|
+
contactSheet,
|
|
191
|
+
fill,
|
|
192
|
+
frameGeometry,
|
|
193
|
+
framingViewport,
|
|
194
|
+
geometryFileOf,
|
|
195
|
+
geometryText,
|
|
196
|
+
loadCandidate,
|
|
197
|
+
nonFinitePoseOf,
|
|
198
|
+
pageFor,
|
|
199
|
+
projector,
|
|
200
|
+
rasteriseMesh,
|
|
201
|
+
rasterisePiece,
|
|
202
|
+
rasteriseQuad,
|
|
203
|
+
refuseUnchosen,
|
|
204
|
+
regionTrim,
|
|
205
|
+
renderFrame,
|
|
206
|
+
sampleAll,
|
|
207
|
+
sampleAnimation,
|
|
208
|
+
sampleSetupPose,
|
|
209
|
+
sidecarViewport,
|
|
210
|
+
skeletonFacts,
|
|
211
|
+
slotsOnUnposedBones,
|
|
212
|
+
substituteTexture,
|
|
213
|
+
textureSubstitutionFromText,
|
|
214
|
+
throughPoser,
|
|
215
|
+
trimmedUnionBounds,
|
|
216
|
+
unframeableSentence,
|
|
217
|
+
unionBounds,
|
|
218
|
+
viewportFor,
|
|
219
|
+
viewportOfSize,
|
|
220
|
+
} from './render_shared.ts';
|
|
221
|
+
export type {
|
|
222
|
+
AttachmentPose,
|
|
223
|
+
AttachmentRest,
|
|
224
|
+
BoneSnapshot,
|
|
225
|
+
Candidate,
|
|
226
|
+
ClipSource,
|
|
227
|
+
DrawOptions,
|
|
228
|
+
EmitPixel,
|
|
229
|
+
Footprint,
|
|
230
|
+
Frame,
|
|
231
|
+
FrameGeometry,
|
|
232
|
+
FrameSet,
|
|
233
|
+
FramesSidecar,
|
|
234
|
+
GeometryBone,
|
|
235
|
+
GeometryFile,
|
|
236
|
+
GeometryFrame,
|
|
237
|
+
MakeCorePoser,
|
|
238
|
+
Mesh,
|
|
239
|
+
Piece,
|
|
240
|
+
PieceCommon,
|
|
241
|
+
PieceTexture,
|
|
242
|
+
PoseOptions,
|
|
243
|
+
Posed,
|
|
244
|
+
Poser,
|
|
245
|
+
PoserChoice,
|
|
246
|
+
PoserName,
|
|
247
|
+
Quad,
|
|
248
|
+
RegionTrim,
|
|
249
|
+
SkeletonFacts,
|
|
250
|
+
SkinRoster,
|
|
251
|
+
SlotSubset,
|
|
252
|
+
SubstituteRegion,
|
|
253
|
+
SubstitutionReader,
|
|
254
|
+
TextureSubstitution,
|
|
255
|
+
UvWindow,
|
|
256
|
+
Viewport,
|
|
257
|
+
} from './render_shared.ts';
|
|
258
|
+
// The refusal of a run that needs the runtime and cannot use it — `./spine_side.ts`'s since issue #1052, where an
|
|
259
|
+
// entry that registered no Spine side is refused in the same words.
|
|
260
|
+
export { SpineRuntimeError };
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Resolve `slots` / `hidden` against `data` as posed under `skin`, or refuse by name.
|
|
264
|
+
*
|
|
265
|
+
* `undefined` when neither is set — the whole rig, which is the ordinary case and
|
|
266
|
+
* costs nothing. Refused, each naming what would have worked:
|
|
267
|
+
*
|
|
268
|
+
* - **both at once** — one statement two ways, as `--rig` with `--cut` is;
|
|
269
|
+
* - **a name the skeleton does not declare** — with every slot it does declare,
|
|
270
|
+
* in draw order, and how many;
|
|
271
|
+
* - **a slot whose attachments live only under skins this pose does not
|
|
272
|
+
* resolve through.** Every slot is declared at the skeleton's top level, so
|
|
273
|
+
* "a slot only a named skin declares" is not a shape the format has — what a
|
|
274
|
+
* skin declares is the slot's ART. A pose resolves an attachment through the
|
|
275
|
+
* skin it was set to and then the default skin (`Skeleton.getAttachment`), so
|
|
276
|
+
* a slot none of whose attachments is in either of those draws nothing in
|
|
277
|
+
* every frame, and `--slot` on it would be a blank picture that looks like an
|
|
278
|
+
* answer. The refusal names the skin(s) that do carry it.
|
|
279
|
+
*
|
|
280
|
+
* ⚠️ A declared slot with no attachment in ANY skin is accepted: it draws
|
|
281
|
+
* nothing under every skin, so there is no skin to name and no picture of it
|
|
282
|
+
* that a different invocation would produce.
|
|
283
|
+
*/
|
|
284
|
+
export function slotSubsetOf(
|
|
285
|
+
data: SkeletonData,
|
|
286
|
+
opts: Pick<PoseOptions, 'slots' | 'hidden'> | undefined,
|
|
287
|
+
skin: string | undefined,
|
|
288
|
+
): SlotSubset | undefined {
|
|
289
|
+
// The rule is `subsetOver`'s (`./render_core.ts`), shared with the core
|
|
290
|
+
// poser; this is the roster a parsed Spine skeleton gives it.
|
|
291
|
+
return subsetOver(
|
|
292
|
+
{
|
|
293
|
+
declared: data.slots.map((slot) => slot.name),
|
|
294
|
+
carriers: (name) => {
|
|
295
|
+
const index = data.findSlot(name)?.index ?? -1;
|
|
296
|
+
return data.skins.filter((s) => s.getAttachments().some((entry) => entry.slotIndex === index)).map((s) => s.name);
|
|
297
|
+
},
|
|
298
|
+
defaultSkin: data.defaultSkin?.name ?? null,
|
|
299
|
+
},
|
|
300
|
+
opts,
|
|
301
|
+
skin,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* A fresh skeleton with `skin` applied, or refused by name.
|
|
307
|
+
*
|
|
308
|
+
* ## Why the skin goes on before `setupPose`, and why nothing else is needed
|
|
309
|
+
*
|
|
310
|
+
* `Skeleton.setSkin` (spine-core 4.3.13 `Skeleton.js:279-311`) attaches the new
|
|
311
|
+
* skin's art into each slot's pose and calls `updateCache`, which is what turns
|
|
312
|
+
* on a `skinRequired` bone or constraint the skin names. Every sampler below
|
|
313
|
+
* then calls `skeleton.setupPose()`, and `setupPose` → `setupPoseSlots`
|
|
314
|
+
* (`Skeleton.js:231-249`) re-resolves each slot's setup attachment through
|
|
315
|
+
* `Slot.setupPose` → `Skeleton.getAttachment`, which checks `this.skin` first
|
|
316
|
+
* and `SkeletonData.defaultSkin` second (`Skeleton.js:335-346`). So the setup
|
|
317
|
+
* pose of a skinned skeleton is already the skin's, and the extra
|
|
318
|
+
* `setSlotsToSetupPose()` a 4.1-era recipe prescribes has no 4.3 spelling to
|
|
319
|
+
* call: the method is named `setupPoseSlots` here, and `setupPose()` runs it.
|
|
320
|
+
*
|
|
321
|
+
* `setSkin(string)` exists too, but its by-name half throws
|
|
322
|
+
* `Skin not found: <name>` (`Skeleton.js:286-291`) — a message that names the
|
|
323
|
+
* miss and not the alternatives. Everything in a rig resolves by name and a miss
|
|
324
|
+
* is refused **by name, with the names that would have worked**, so the lookup
|
|
325
|
+
* happens here and the runtime is handed a `Skin` it cannot fail on.
|
|
326
|
+
*/
|
|
327
|
+
function skeletonUnderSkin(data: SkeletonData, skin: string | undefined): Skeleton {
|
|
328
|
+
const skeleton = new Skeleton(data);
|
|
329
|
+
if (skin === undefined) return skeleton;
|
|
330
|
+
const found = data.findSkin(skin);
|
|
331
|
+
if (!found) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`no skin ${JSON.stringify(skin)} in this skeleton; it declares [${
|
|
334
|
+
data.skins.map((s) => s.name).join(', ') || 'none'
|
|
335
|
+
}]`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
skeleton.setSkin(found);
|
|
339
|
+
return skeleton;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Every bone's world transform in the skeleton's own declaration order — a bone
|
|
344
|
+
* the posed skin leaves unposed (inactive, or below an inactive bone) written as the zero transform
|
|
345
|
+
* (`inactiveBoneSnapshot` in `./render_core.ts`: it is not posed, and what a
|
|
346
|
+
* constraint left in its matrix is not a pose — issue #968).
|
|
347
|
+
*/
|
|
348
|
+
export function boneSnapshots(skeleton: Skeleton): BoneSnapshot[] {
|
|
349
|
+
const unposed = unposedBones(skeleton.bones.map((bone) => ({ name: bone.data.name, parent: bone.parent?.data.name ?? null, active: bone.active })));
|
|
350
|
+
return skeleton.bones.map((bone) => {
|
|
351
|
+
// Not posed under this skin: the seam's zero snapshot (`inactiveBoneSnapshot`, issue #968).
|
|
352
|
+
if (unposed.has(bone.data.name)) return inactiveBoneSnapshot(bone.data.name);
|
|
353
|
+
const pose = bone.appliedPose;
|
|
354
|
+
return {
|
|
355
|
+
name: bone.data.name,
|
|
356
|
+
worldX: pose.worldX,
|
|
357
|
+
worldY: pose.worldY,
|
|
358
|
+
a: pose.a,
|
|
359
|
+
b: pose.b,
|
|
360
|
+
c: pose.c,
|
|
361
|
+
d: pose.d,
|
|
362
|
+
rotationX: pose.getWorldRotationX(),
|
|
363
|
+
rotationY: pose.getWorldRotationY(),
|
|
364
|
+
scaleX: pose.getWorldScaleX(),
|
|
365
|
+
scaleY: pose.getWorldScaleY(),
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** A loaded skeleton and every atlas page it can sample, keyed by page name. */
|
|
371
|
+
export interface Posable {
|
|
372
|
+
data: SkeletonData;
|
|
373
|
+
pages: Map<string, Plate>;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Load a skeleton, its atlas and every page the atlas declares.
|
|
378
|
+
*
|
|
379
|
+
* Every page, not the first: rigc emits **one part per page**, so a rigc
|
|
380
|
+
* candidate for rung 1 has eight of them. `render_reference.ts` used to insist
|
|
381
|
+
* on exactly one because an editor export packs into one — that assumption is
|
|
382
|
+
* true of the reference and false of every candidate, and a renderer both sides
|
|
383
|
+
* share cannot hold it.
|
|
384
|
+
*/
|
|
385
|
+
export function loadPosable(skeletonPath: string, atlasPath: string, atlasDir: string): Posable {
|
|
386
|
+
return posableFromText(readFileSync(skeletonPath, 'utf8'), readFileSync(atlasPath, 'utf8'), atlasDir);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The page names an atlas declares, in the order it declares them.
|
|
391
|
+
*
|
|
392
|
+
* Through `TextureAtlas` rather than by reading the lines: a page name and a
|
|
393
|
+
* region name are both unindented in the atlas format, so anything that told them
|
|
394
|
+
* apart here would be a second opinion about the file's syntax — and the one
|
|
395
|
+
* caller that needs this list (`rigc preview`, embedding each page) has to agree
|
|
396
|
+
* exactly with the player that will ask for them by name.
|
|
397
|
+
*/
|
|
398
|
+
export function atlasPageNames(atlasText: string): string[] {
|
|
399
|
+
return new TextureAtlas(atlasText).pages.map((page) => page.name);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Same, for artifacts held in memory rather than on disk. */
|
|
403
|
+
export function posableFromText(skeletonText: string, atlasText: string, atlasDir: string): Posable {
|
|
404
|
+
const atlas = new TextureAtlas(atlasText);
|
|
405
|
+
const pages = new Map<string, Plate>();
|
|
406
|
+
for (const page of atlas.pages) pages.set(page.name, readPlate(join(atlasDir, page.name)));
|
|
407
|
+
const data = new SkeletonJson(new AtlasAttachmentLoader(atlas)).readSkeletonData(JSON.parse(skeletonText));
|
|
408
|
+
return { data, pages };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** The facts as spine-core loaded them — an export's reading, and every candidate's before issue #1014; `atlasText` the atlas it was loaded through. */
|
|
412
|
+
export function spineFacts(data: SkeletonData, atlasText: string | null): SkeletonFacts {
|
|
413
|
+
return {
|
|
414
|
+
atlasScales: atlasText === null ? null : atlasScales(atlasText),
|
|
415
|
+
animations: data.animations.map((a) => a.name),
|
|
416
|
+
skins: data.skins.map((s) => s.name),
|
|
417
|
+
// `SkeletonJson` copies both header fields across unconditionally, so an omitted extent is `undefined` here, not 0.
|
|
418
|
+
declaresStage: typeof data.width === 'number' && typeof data.height === 'number',
|
|
419
|
+
bones: data.bones.map((bone) => ({ name: bone.name, parent: bone.parent === null ? null : bone.parent.name })),
|
|
420
|
+
slots: data.slots.map((slot) => ({ name: slot.name, bone: slot.boneData.name })),
|
|
421
|
+
subset: (opts, skin) => slotSubsetOf(data, opts, skin),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Touch the runtime once, before anything is parsed through it, and refuse by name when it cannot be used. */
|
|
426
|
+
function requireSpineRuntime(label: string, why: string): void {
|
|
427
|
+
try {
|
|
428
|
+
// A property read on the class the load starts from: no runtime code runs, and a runtime that cannot be used throws here.
|
|
429
|
+
void TextureAtlas.prototype;
|
|
430
|
+
} catch (err) {
|
|
431
|
+
// The sentence an entry that registered no Spine side is refused in too (`./spine_side.ts`), with the runtime's own words as the reason.
|
|
432
|
+
throw new SpineRuntimeError(spineRuntimeSentence(label, why, (err as Error).message, POSING_RUNTIME_TAIL));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* The one place a candidate's skeleton is handed to the runtime's parser
|
|
438
|
+
* against its atlas (issues #1033, #1042): a pair it cannot load is `refuse`'s
|
|
439
|
+
* refusal, given the runtime's own message, rather than the runtime's throw
|
|
440
|
+
* and a stack. The JSON is parsed outside the catch: a file that is not JSON
|
|
441
|
+
* is not something the runtime said, and the CLI refuses it by name before it
|
|
442
|
+
* gets here (`readSkeletonText`).
|
|
443
|
+
*/
|
|
444
|
+
function spineSkeletonData(skeletonText: string, atlasText: string, refuse: (runtime: string) => Error): SkeletonData {
|
|
445
|
+
const json = JSON.parse(skeletonText);
|
|
446
|
+
const reader = new SkeletonJson(new AtlasAttachmentLoader(new TextureAtlas(atlasText)));
|
|
447
|
+
try {
|
|
448
|
+
return reader.readSkeletonData(json);
|
|
449
|
+
} catch (err) {
|
|
450
|
+
throw refuse(err instanceof Error ? err.message : String(err));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* A skeleton a user named, posed through spine-core for what its bones do —
|
|
456
|
+
* `bonedist`'s two sides (issue #1042). A pair that does not load is refused
|
|
457
|
+
* by name (`CandidatePairError`, `pairRefusal` with `poses`), `why` saying
|
|
458
|
+
* which side and what the pose is read for.
|
|
459
|
+
*
|
|
460
|
+
* ⭐ The skeleton data alone, and no page: `bonedist` reads bone world
|
|
461
|
+
* transforms and samples no pixel, and the atlas's regions — which the
|
|
462
|
+
* runtime resolves every attachment against — are in the atlas text. Reading
|
|
463
|
+
* the page PNGs as `loadPosable` does made a pair whose pages are elsewhere
|
|
464
|
+
* die on an ENOENT for images the command never looks at; the figures are the
|
|
465
|
+
* same with or without them (the PR of #1042 measures it).
|
|
466
|
+
*
|
|
467
|
+
* `loadPosable` and `posableFromText` stay the unguarded loaders, for pairs
|
|
468
|
+
* the caller wrote itself (the tools and the selftest), where the runtime's
|
|
469
|
+
* own message is what the caller reads.
|
|
470
|
+
*/
|
|
471
|
+
export function loadPosedSkeleton(skeletonPath: string, atlasPath: string, why: string): SkeletonData {
|
|
472
|
+
const paths = { skeleton: skeletonPath, atlas: atlasPath };
|
|
473
|
+
return spineSkeletonData(readFileSync(skeletonPath, 'utf8'), readFileSync(atlasPath, 'utf8'), (runtime) => pairRefusal(skeletonPath, paths, why, runtime, 'poses'));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** What every sampler takes: a poser, or spine-core's parsed skeleton, which is posed through `spinePoser`. */
|
|
477
|
+
export type PoseSource = Poser | SkeletonData;
|
|
478
|
+
|
|
479
|
+
// ---------------------------------------------------------------------------
|
|
480
|
+
// the spine-core implementation of the seam
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* `Poser` over spine-core: the runtime's own `Skeleton`, `AnimationState` and
|
|
485
|
+
* `SkeletonClipping`, stepped the way a runtime steps them.
|
|
486
|
+
*
|
|
487
|
+
* The pose is driven through `AnimationState` rather than `Animation.apply`
|
|
488
|
+
* because that is the path a runtime actually takes, and 4.3's
|
|
489
|
+
* `Animation.apply` takes a `MixFrom` that only the state machine has any
|
|
490
|
+
* business choosing. Frame 0 applies, updates by 0 and resets physics; every
|
|
491
|
+
* later frame is one `state.update(1/fps)`, apply, `skeleton.update(1/fps)` and
|
|
492
|
+
* `Physics.update` — one continuous trajectory, so the track time of frame `i`
|
|
493
|
+
* is the sum of `i` steps.
|
|
494
|
+
*/
|
|
495
|
+
export function spinePoser(data: SkeletonData): Poser {
|
|
496
|
+
return {
|
|
497
|
+
animations: data.animations.map((a) => ({ name: a.name, duration: a.duration })),
|
|
498
|
+
bones: data.bones.map((bone) => ({ name: bone.name, parent: bone.parent?.name ?? null })),
|
|
499
|
+
slots: data.slots.map((slot) => ({ name: slot.name, bone: slot.boneData.name })),
|
|
500
|
+
subset: (opts, skin) => slotSubsetOf(data, opts, skin),
|
|
501
|
+
setup: (skin) => spinePosed(setupPosed(data, skin)),
|
|
502
|
+
animation: (name, skin, fps, count, visit) => {
|
|
503
|
+
const skeleton = skeletonUnderSkin(data, skin);
|
|
504
|
+
const state = new AnimationState(new AnimationStateData(data));
|
|
505
|
+
// Not looping: the last frame sits at the animation's duration, and a looping
|
|
506
|
+
// entry would wrap it back onto the first pose.
|
|
507
|
+
state.setAnimation(0, name, false);
|
|
508
|
+
skeleton.setupPose();
|
|
509
|
+
const step = 1 / fps;
|
|
510
|
+
const posed = spinePosed(skeleton);
|
|
511
|
+
for (let i = 0; i <= count; i++) {
|
|
512
|
+
if (i > 0) {
|
|
513
|
+
state.update(step);
|
|
514
|
+
state.apply(skeleton);
|
|
515
|
+
skeleton.update(step);
|
|
516
|
+
skeleton.updateWorldTransform(Physics.update);
|
|
517
|
+
} else {
|
|
518
|
+
state.apply(skeleton);
|
|
519
|
+
skeleton.update(0);
|
|
520
|
+
skeleton.updateWorldTransform(Physics.reset);
|
|
521
|
+
}
|
|
522
|
+
visit(i, posed);
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
rest: (skin, shown) => restOf(data, skin, shown),
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** A spine-core skeleton as it stands posed, read through the seam's `Posed`. */
|
|
530
|
+
function spinePosed(skeleton: Skeleton): Posed {
|
|
531
|
+
return {
|
|
532
|
+
pieces: (draw) => drawPieces(skeleton, draw),
|
|
533
|
+
bones: () => boneSnapshots(skeleton),
|
|
534
|
+
attachments: () => attachmentsOf(skeleton),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* A fresh skeleton under `skin`, stepped into its setup pose — the one recipe
|
|
540
|
+
* `sampleSetupPose` and the geometry export's `rest` table both pose from, so
|
|
541
|
+
* the two cannot come to describe different rest poses.
|
|
542
|
+
*/
|
|
543
|
+
function setupPosed(data: SkeletonData, skin: string | undefined): Skeleton {
|
|
544
|
+
const skeleton = skeletonUnderSkin(data, skin);
|
|
545
|
+
skeleton.setupPose();
|
|
546
|
+
skeleton.update(0);
|
|
547
|
+
skeleton.updateWorldTransform(Physics.reset);
|
|
548
|
+
return skeleton;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Both posers for the skeleton at `skeletonPath` drawn through the atlas at
|
|
553
|
+
* `atlasPath`, with spine-core's parse of it already in hand (`data`) — the
|
|
554
|
+
* choice `loadCandidate` makes, for a caller that loaded the pair itself.
|
|
555
|
+
* `forced` is `--poser`; `core` on an input that cannot carry it is refused by
|
|
556
|
+
* name (`PoserChoiceError`).
|
|
557
|
+
*/
|
|
558
|
+
export function candidatePosers(
|
|
559
|
+
data: SkeletonData,
|
|
560
|
+
skeletonPath: string,
|
|
561
|
+
atlasPath: string,
|
|
562
|
+
forced: PoserName | undefined,
|
|
563
|
+
/** What builds the core poser: `corePoser` — the suite's `RC02`, `RC43` and `RC44` pass planted or counted copies, and nothing else passes any. */
|
|
564
|
+
make: MakeCorePoser = corePoser,
|
|
565
|
+
): PoserChoice {
|
|
566
|
+
return refuseUnchosen(choosePosers(skeletonPath, atlasPath, readFileSync(atlasPath, 'utf8'), forced, () => data, make).choice);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* The posed drawables of one frame, in draw order.
|
|
571
|
+
*
|
|
572
|
+
* Regions and meshes take the same three steps — resolve the sequence index,
|
|
573
|
+
* ask `spine-core` for the world vertices, read the page UVs back off the same
|
|
574
|
+
* sequence — and differ only in which runtime call does step two. An attachment
|
|
575
|
+
* type that is neither is skipped rather than refused: a bounding box, a point
|
|
576
|
+
* and a clipping attachment are all things a rig legitimately carries and none
|
|
577
|
+
* of them draws a pixel.
|
|
578
|
+
*
|
|
579
|
+
* ## A clipping attachment is skipped as a piece and applied as a mask
|
|
580
|
+
*
|
|
581
|
+
* It draws nothing, and it removes what every slot from its own through its
|
|
582
|
+
* `end` slot draws outside its polygon. Skipping it outright drew those pixels —
|
|
583
|
+
* measured on a port whose eye masks clip the irises: the blink frame read MAE
|
|
584
|
+
* 1.07 against 0.53 at an open eye, with both irises drawn over closed lids,
|
|
585
|
+
* where spine-webgl reads 0.16 (issue #844). So spine-core's own
|
|
586
|
+
* `SkeletonClipping` runs beside the walk, and the call sequence is
|
|
587
|
+
* spine-webgl's `SkeletonRenderer.draw` (branch `4.3`) step for step: at a
|
|
588
|
+
* clipping attachment `clipEnd(slot)` then `clipStart(skeleton, slot, clip)` and
|
|
589
|
+
* nothing drawn; at every other slot, drawn or not, `clipEnd(slot)` after it,
|
|
590
|
+
* which is what ends a clip AT its end slot rather than before it; `clipEnd()`
|
|
591
|
+
* after the walk. The polygon, its convex decomposition, `inverse` and `convex`
|
|
592
|
+
* are the clipper's, so there is no second opinion about any of them here.
|
|
593
|
+
*
|
|
594
|
+
* A slot inside a clip hands its world vertices, its triangles — a region's are
|
|
595
|
+
* the runtime's own `0 1 2 2 3 0` — and its UVs to `clipTrianglesUnpacked`, and
|
|
596
|
+
* the piece carries what comes back. ⚠️ **Only when the clipper says it clipped**,
|
|
597
|
+
* exactly as spine-webgl uses the result only when `clipTriangles` returns
|
|
598
|
+
* true: an attachment wholly inside the polygon draws its own geometry, so a
|
|
599
|
+
* region there is still a `Quad` and its pixels are the unclipped ones to the
|
|
600
|
+
* bit. One that is cut becomes a `Mesh` — the clipper's output is a triangle
|
|
601
|
+
* list — and one wholly outside becomes a mesh with no triangle, which keeps the
|
|
602
|
+
* slot in the frame, undrawn, rather than absent.
|
|
603
|
+
*
|
|
604
|
+
* ⭐ A cut piece's pixels are sampled at each drawn triangle's SOURCE triangle's
|
|
605
|
+
* affine UV (`Mesh.source`), not at its own float32 corner UVs (issue #964):
|
|
606
|
+
* which convex pieces a clipper cuts a concave polygon into is the clipper's —
|
|
607
|
+
* spine-core's, the core's, or spine-core's under another spelling of the same
|
|
608
|
+
* polygon — and only this keeps the picture from depending on it. Measured: the
|
|
609
|
+
* same notched square spelled from four start vertices moved up to 18 pixels one
|
|
610
|
+
* level against itself through spine-core before, 0 after; the 19 tree rows'
|
|
611
|
+
* renders did not move (spineboy-pro's portal, the one clipped row, included).
|
|
612
|
+
* An unclipped piece carries no `source` and is rasterised as before, byte for
|
|
613
|
+
* byte.
|
|
614
|
+
*
|
|
615
|
+
* The clip is applied whatever `slots`/`hidden` draw: a hidden clip still masks
|
|
616
|
+
* what is shown, so a subset frame is the whole frame's pixels for those slots.
|
|
617
|
+
* `unclipped` turns it off for the framing box alone — see its note.
|
|
618
|
+
*/
|
|
619
|
+
export function piecesOf(skeleton: Skeleton, opts?: PoseOptions): Piece[] {
|
|
620
|
+
// A skin is chosen before a skeleton is posed, and this one is already posed —
|
|
621
|
+
// so there is nothing honest to do with the name except say so. Silently
|
|
622
|
+
// ignoring it is the shape of defect #571 itself: a skin asked for, no skin
|
|
623
|
+
// applied, and a picture that looks like an answer.
|
|
624
|
+
if (opts?.skin !== undefined) {
|
|
625
|
+
throw new Error(
|
|
626
|
+
`piecesOf was asked for skin ${JSON.stringify(opts.skin)}, and it reads a skeleton that is already posed. ` +
|
|
627
|
+
'Ask a sampler for it — sampleSetupPose/sampleAnimation/sampleAll take { skin } — or call ' +
|
|
628
|
+
'skeleton.setSkin(...) and skeleton.setupPose() before this.',
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
// Resolved against the skeleton as it was posed — its own slots and the skin
|
|
632
|
+
// it was set to — so a name that draws nothing is refused here, where the one
|
|
633
|
+
// application point is, rather than matching no piece in silence.
|
|
634
|
+
const subset = slotSubsetOf(skeleton.data, opts, skeleton.skin?.name);
|
|
635
|
+
return drawPieces(skeleton, { subset, unclipped: opts?.unclipped === true, texture: opts?.texture === true });
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** `piecesOf`'s walk over a posed spine-core skeleton, the subset already resolved — the spine poser's `Posed.pieces`. */
|
|
639
|
+
function drawPieces(skeleton: Skeleton, draw: DrawOptions): Piece[] {
|
|
640
|
+
const { subset } = draw;
|
|
641
|
+
const named = subset === undefined ? undefined : new Set(subset.names);
|
|
642
|
+
const clipper = draw.unclipped ? null : new SkeletonClipping();
|
|
643
|
+
const pieces: Piece[] = [];
|
|
644
|
+
for (const slot of skeleton.drawOrder.appliedPose) {
|
|
645
|
+
const attachment = slot.appliedPose.attachment;
|
|
646
|
+
if (attachment instanceof ClippingAttachment) {
|
|
647
|
+
if (clipper !== null) {
|
|
648
|
+
clipper.clipEnd(slot);
|
|
649
|
+
// spine-webgl ends the clip at a slot whose bone is inactive and starts
|
|
650
|
+
// none there; the order of the two calls is the same either way.
|
|
651
|
+
if (slot.bone.active) clipper.clipStart(skeleton, slot, attachment);
|
|
652
|
+
}
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const drawn = subset === undefined || named === undefined || named.has(slot.data.name) === (subset.mode === 'slots');
|
|
656
|
+
const piece = drawn ? pieceOf(skeleton, slot, draw.texture, clipper) : null;
|
|
657
|
+
if (piece !== null) pieces.push(piece);
|
|
658
|
+
clipper?.clipEnd(slot);
|
|
659
|
+
}
|
|
660
|
+
clipper?.clipEnd();
|
|
661
|
+
return pieces;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** The runtime's own triangulation of a region's quad — spine-webgl's `QUAD_TRIANGLES`. */
|
|
665
|
+
const QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0];
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* One slot's posed drawable, or `null` for an attachment that draws nothing —
|
|
669
|
+
* clipped by `clipper` when a clip is active over it (see `piecesOf`).
|
|
670
|
+
*/
|
|
671
|
+
function pieceOf(
|
|
672
|
+
skeleton: Skeleton,
|
|
673
|
+
slot: Slot,
|
|
674
|
+
withTexture: boolean,
|
|
675
|
+
clipper: SkeletonClipping | null,
|
|
676
|
+
): Piece | null {
|
|
677
|
+
const pose = slot.appliedPose;
|
|
678
|
+
const attachment = pose.attachment;
|
|
679
|
+
if (!attachment) return null;
|
|
680
|
+
const isMesh = attachment instanceof MeshAttachment;
|
|
681
|
+
if (!isMesh && !(attachment instanceof RegionAttachment)) return null;
|
|
682
|
+
|
|
683
|
+
const index = attachment.sequence.resolveIndex(pose);
|
|
684
|
+
const region = attachment.sequence.regions[index];
|
|
685
|
+
if (!(region instanceof TextureAtlasRegion)) {
|
|
686
|
+
throw new Error(
|
|
687
|
+
`slot "${slot.data.name}" attachment "${attachment.name}" resolved to no atlas region; ` +
|
|
688
|
+
'the attachment names a region the atlas does not have',
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
const tint = tintOf(slot, attachment);
|
|
692
|
+
// The dark colour is the SLOT's alone — an attachment has a `color` and no
|
|
693
|
+
// dark one, so there is nothing to multiply it by. Read off `appliedPose`
|
|
694
|
+
// like the light colour, so an `rgba2` timeline reaches the picture.
|
|
695
|
+
const darkPose = pose.darkColor;
|
|
696
|
+
const dark: [number, number, number] | undefined =
|
|
697
|
+
darkPose === null ? undefined : [darkPose.r, darkPose.g, darkPose.b];
|
|
698
|
+
const common = { tint, dark, slot: slot.data.name, page: region.page.name };
|
|
699
|
+
const texture = withTexture ? artUvsOf(attachment, region) : undefined;
|
|
700
|
+
const uvs = attachment.sequence.getUVs(index);
|
|
701
|
+
|
|
702
|
+
let piece: Piece;
|
|
703
|
+
let triangles: number[];
|
|
704
|
+
const world = worldVerticesOf(skeleton, slot, attachment);
|
|
705
|
+
if (isMesh) {
|
|
706
|
+
triangles = attachment.triangles;
|
|
707
|
+
piece = { kind: 'mesh', ...common, texture, world, uvs, triangles };
|
|
708
|
+
} else {
|
|
709
|
+
triangles = QUAD_TRIANGLES;
|
|
710
|
+
piece = { kind: 'region', ...common, texture, world, uvs };
|
|
711
|
+
}
|
|
712
|
+
if (clipper === null || !clipper.isClipping()) return piece;
|
|
713
|
+
return clippedPiece(piece, triangles, uvs, clipper);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** Slot colour x attachment colour, straight alpha — what a piece is tinted by and a geometry entry records. */
|
|
717
|
+
function tintOf(slot: Slot, attachment: MeshAttachment | RegionAttachment): [number, number, number, number] {
|
|
718
|
+
const colour = slot.appliedPose.color;
|
|
719
|
+
const own = attachment.color;
|
|
720
|
+
return [colour.r * own.r, colour.g * own.g, colour.b * own.b, colour.a * own.a];
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* One attachment's world vertices on `slot`, whole, by the runtime's own routine.
|
|
725
|
+
*
|
|
726
|
+
* The one place either kind is asked for them: `pieceOf` draws what this returns
|
|
727
|
+
* and `attachmentsOf` records it, so a drawn frame and its geometry export
|
|
728
|
+
* cannot disagree about where a vertex is.
|
|
729
|
+
*
|
|
730
|
+
* `worldVerticesLength` is 2 per vertex whether or not the mesh is weighted —
|
|
731
|
+
* the weight runs live in `vertices`, not here — so this is the full output
|
|
732
|
+
* length and the whole mesh is computed in one call. Deform offsets, if the
|
|
733
|
+
* slot's pose carries any, are applied inside it; a region's offsets are read
|
|
734
|
+
* for the sequence frame the slot's pose resolves.
|
|
735
|
+
*/
|
|
736
|
+
function worldVerticesOf(skeleton: Skeleton, slot: Slot, attachment: MeshAttachment | RegionAttachment): number[] {
|
|
737
|
+
if (attachment instanceof MeshAttachment) {
|
|
738
|
+
const world = new Array<number>(attachment.worldVerticesLength).fill(0);
|
|
739
|
+
attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, world, 0, 2);
|
|
740
|
+
return world;
|
|
741
|
+
}
|
|
742
|
+
const world = new Array<number>(8).fill(0);
|
|
743
|
+
attachment.computeWorldVertices(slot, attachment.getOffsets(slot.appliedPose), world, 0, 2);
|
|
744
|
+
return world;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Every slot's region or mesh attachment, whole, in the posed draw order — see
|
|
749
|
+
* `Frame.attachments`. No subset and no clip: those are `piecesOf`'s business.
|
|
750
|
+
*/
|
|
751
|
+
function attachmentsOf(skeleton: Skeleton): AttachmentPose[] {
|
|
752
|
+
const out: AttachmentPose[] = [];
|
|
753
|
+
for (const slot of skeleton.drawOrder.appliedPose) {
|
|
754
|
+
const attachment = slot.appliedPose.attachment;
|
|
755
|
+
if (!(attachment instanceof MeshAttachment) && !(attachment instanceof RegionAttachment)) continue;
|
|
756
|
+
out.push({
|
|
757
|
+
slot: slot.data.name,
|
|
758
|
+
attachment: attachment.name,
|
|
759
|
+
vertices: worldVerticesOf(skeleton, slot, attachment),
|
|
760
|
+
color: tintOf(slot, attachment),
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
return out;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* `piece` as the active clip leaves it, or `piece` itself when the clipper cut
|
|
768
|
+
* nothing — see `piecesOf`.
|
|
769
|
+
*
|
|
770
|
+
* The page UVs and the original-art UVs (`PieceTexture`, when the piece carries
|
|
771
|
+
* them) each go through their own `clipTrianglesUnpacked` call over the same
|
|
772
|
+
* vertices and triangles. The clipper's geometry depends on the positions alone
|
|
773
|
+
* and it interpolates a UV set barycentrically inside each source triangle, so
|
|
774
|
+
* the two calls cut the same polygons and each UV set lands on them — which is
|
|
775
|
+
* what lets `substituteTexture` re-seat a clipped piece exactly as it re-seats a
|
|
776
|
+
* whole one, through the drawing's own coordinates. The vertex count is compared
|
|
777
|
+
* all the same, because a clipped piece whose two UV sets disagreed on it would
|
|
778
|
+
* sample the wrong texels and say nothing.
|
|
779
|
+
*/
|
|
780
|
+
function clippedPiece(piece: Piece, triangles: number[], uvs: Float32Array, clipper: SkeletonClipping): Piece {
|
|
781
|
+
if (!clipper.clipTrianglesUnpacked(piece.world, 0, triangles, triangles.length, uvs, 2)) return piece;
|
|
782
|
+
const world = Array.from(clipper.clippedVerticesTyped);
|
|
783
|
+
const clippedUvs = Array.from(clipper.clippedUVsTyped);
|
|
784
|
+
const clipped = Array.from(clipper.clippedTrianglesTyped);
|
|
785
|
+
// Which source triangle each drawn triangle was cut from (`Mesh.source`): the clipper cuts triangle by triangle, so each source
|
|
786
|
+
// triangle is clipped alone and its drawn triangles counted; the concatenation is held to the whole call's output, vertex for vertex.
|
|
787
|
+
const sources: number[] = [];
|
|
788
|
+
const again: number[] = [];
|
|
789
|
+
for (let t = 0; t + 2 < triangles.length; t += 3) {
|
|
790
|
+
clipper.clipTrianglesUnpacked(piece.world, 0, triangles.slice(t, t + 3), 3, uvs, 2);
|
|
791
|
+
for (let k = 0; k < clipper.clippedTrianglesTyped.length; k += 3) sources.push(t / 3);
|
|
792
|
+
again.push(...clipper.clippedVerticesTyped);
|
|
793
|
+
}
|
|
794
|
+
if (sources.length !== clipped.length / 3 || again.length !== world.length || again.some((v, i) => v !== world[i])) {
|
|
795
|
+
throw new Error(`slot "${piece.slot}": the clipper cut ${clipped.length / 3} triangle(s) in one call and ${sources.length} triangle by triangle, over other vertices — the source of each drawn triangle cannot be named`);
|
|
796
|
+
}
|
|
797
|
+
let texture = piece.texture;
|
|
798
|
+
const source = clipSourceOf(piece.world, Array.from(uvs), triangles, sources, texture?.artUvs);
|
|
799
|
+
if (texture !== undefined) {
|
|
800
|
+
clipper.clipTrianglesUnpacked(piece.world, 0, triangles, triangles.length, texture.artUvs, 2);
|
|
801
|
+
const artUvs = Array.from(clipper.clippedUVsTyped);
|
|
802
|
+
if (artUvs.length !== clippedUvs.length) {
|
|
803
|
+
throw new Error(
|
|
804
|
+
`slot "${piece.slot}": the clip cut ${clippedUvs.length / 2} vertices for the page UVs and ` +
|
|
805
|
+
`${artUvs.length / 2} for the original-art UVs over the same geometry`,
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
texture = { region: texture.region, artUvs, sourceArtUvs: source.artUvs };
|
|
809
|
+
}
|
|
810
|
+
const { tint, dark, slot, page } = piece;
|
|
811
|
+
return { kind: 'mesh', tint, dark, slot, page, texture, world, uvs: clippedUvs, triangles: clipped, source: { world: source.world, uvs: source.uvs } };
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* The rest table: every (slot, attachment) the given frames show, in order of
|
|
816
|
+
* first appearance, posed on one setup skeleton with the slot's deform empty.
|
|
817
|
+
*
|
|
818
|
+
* The attachment is resolved the way the pose resolved it — `Skeleton.getAttachment`,
|
|
819
|
+
* the skin first and the default skin second — so a name means the object the
|
|
820
|
+
* frames drew. A region's rest corners are read for the sequence frame the setup
|
|
821
|
+
* pose resolves, which is the frame its setup pose draws.
|
|
822
|
+
*/
|
|
823
|
+
function restOf(data: SkeletonData, skin: string | undefined, shown: readonly AttachmentPose[][]): AttachmentRest[] {
|
|
824
|
+
const skeleton = setupPosed(data, skin);
|
|
825
|
+
// Slot, then attachment: two maps rather than one joined key, so no pair of
|
|
826
|
+
// names can fold into another's entry whatever characters they carry.
|
|
827
|
+
const seen = new Map<string, Set<string>>();
|
|
828
|
+
const out: AttachmentRest[] = [];
|
|
829
|
+
for (const entries of shown) {
|
|
830
|
+
for (const entry of entries) {
|
|
831
|
+
const names = seen.get(entry.slot) ?? new Set<string>();
|
|
832
|
+
if (names.has(entry.attachment)) continue;
|
|
833
|
+
names.add(entry.attachment);
|
|
834
|
+
seen.set(entry.slot, names);
|
|
835
|
+
const slotIndex = data.findSlot(entry.slot)?.index ?? -1;
|
|
836
|
+
const slot = skeleton.slots[slotIndex];
|
|
837
|
+
const attachment = slot === undefined ? null : skeleton.getAttachment(slotIndex, entry.attachment);
|
|
838
|
+
if (slot === undefined || !(attachment instanceof MeshAttachment || attachment instanceof RegionAttachment)) {
|
|
839
|
+
throw new Error(
|
|
840
|
+
`slot ${JSON.stringify(entry.slot)} showed attachment ${JSON.stringify(entry.attachment)} in a frame, and ` +
|
|
841
|
+
'the setup skeleton resolves no region or mesh of that name there',
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
// A mesh reads the slot's deform array; the setup pose leaves it empty,
|
|
845
|
+
// and emptying it here says so rather than trusting that it is.
|
|
846
|
+
slot.appliedPose.deform.length = 0;
|
|
847
|
+
const vertices = worldVerticesOf(skeleton, slot, attachment);
|
|
848
|
+
out.push(
|
|
849
|
+
attachment instanceof MeshAttachment
|
|
850
|
+
? {
|
|
851
|
+
slot: entry.slot,
|
|
852
|
+
attachment: entry.attachment,
|
|
853
|
+
kind: 'mesh',
|
|
854
|
+
vertices,
|
|
855
|
+
triangles: Array.from(attachment.triangles),
|
|
856
|
+
hull: attachment.hullLength / 2,
|
|
857
|
+
uvs: Array.from(attachment.regionUVs),
|
|
858
|
+
}
|
|
859
|
+
: { slot: entry.slot, attachment: entry.attachment, kind: 'region', vertices, triangles: [...QUAD_TRIANGLES] },
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return out;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* One skeleton as it stands posed now, as the numbers `firstNonFinite`
|
|
868
|
+
* (`./nonfinite.ts`) reads: every bone's six world-transform terms, in skeleton
|
|
869
|
+
* order, then the world vertices of every region and mesh its slots show, in
|
|
870
|
+
* draw order — the runtime's reading of each (`worldVerticesOf`).
|
|
871
|
+
*
|
|
872
|
+
* ⭐ This is how `A10_NO_NAN_AFTER_STEPPING` reads each pose spine-core steps
|
|
873
|
+
* (issue #882; its runtime supplier in `validate.ts` since issue #1025), so the
|
|
874
|
+
* gate and the renderer hold one definition of "not finite": the six terms
|
|
875
|
+
* `firstNonFinite` reads off a bone, and the vertices the runtime computes from
|
|
876
|
+
* them. Before #882 A10 read the world POSITION alone, and a bone at `rotation:
|
|
877
|
+
* 1e309` — finite position, NaN `a`, `b`, `c`, `d` — was gated green and then
|
|
878
|
+
* refused here. The vertices are read too because a bone can be finite and
|
|
879
|
+
* still carry one that is not: a two-bone `scaleX` chain of 1e154 × 1e154
|
|
880
|
+
* leaves the child's `a` at 1e308, finite, and every vertex of its attachment
|
|
881
|
+
* past the largest double. `firstNonFinite` reads the bones first, so a broken
|
|
882
|
+
* bone is named as the cause rather than through its vertices.
|
|
883
|
+
*/
|
|
884
|
+
export function posedNumbersOf(skeleton: Skeleton): { bones: WorldTransform[]; drawn: PosedVertices[] } {
|
|
885
|
+
const bones: WorldTransform[] = skeleton.bones.map((bone) => {
|
|
886
|
+
const { a, b, c, d, worldX, worldY } = bone.appliedPose;
|
|
887
|
+
return { name: bone.data.name, a, b, c, d, worldX, worldY };
|
|
888
|
+
});
|
|
889
|
+
const drawn: PosedVertices[] = [];
|
|
890
|
+
for (const slot of skeleton.drawOrder.appliedPose) {
|
|
891
|
+
const attachment = slot.appliedPose.attachment;
|
|
892
|
+
if (!(attachment instanceof MeshAttachment) && !(attachment instanceof RegionAttachment)) continue;
|
|
893
|
+
drawn.push({ slot: slot.data.name, attachment: attachment.name, vertices: worldVerticesOf(skeleton, slot, attachment) });
|
|
894
|
+
}
|
|
895
|
+
return { bones, drawn };
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// ---------------------------------------------------------------------------
|
|
899
|
+
// texture-only substitution — see `substituteTexture`
|
|
900
|
+
// ---------------------------------------------------------------------------
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* One piece's UVs in the drawing's own space, for the region it resolved to.
|
|
904
|
+
*
|
|
905
|
+
* ## The two shapes, and why only one needs arithmetic
|
|
906
|
+
*
|
|
907
|
+
* A **mesh** already carries them. `MeshAttachment.regionUVs` are read by
|
|
908
|
+
* `spine-core` as coordinates over the *untrimmed* drawing — that is what its own
|
|
909
|
+
* `u -= region.offsetX / textureWidth` and `width = region.originalWidth /
|
|
910
|
+
* textureWidth` mean — so a mesh's authored UVs are atlas-independent by
|
|
911
|
+
* construction, and so is its geometry: `MeshAttachment.computeWorldVertices`
|
|
912
|
+
* reads `vertices` and bones and never touches the region at all. A mesh
|
|
913
|
+
* therefore has nothing an atlas swap could move except its texels.
|
|
914
|
+
*
|
|
915
|
+
* A **region** is the case issue #199 is about. Its quad is derived from the
|
|
916
|
+
* region rectangle — `RegionAttachment.computeUVs` insets it by `offsetX/offsetY`
|
|
917
|
+
* and sizes it by `region.width/height` over `originalWidth/originalHeight` — so
|
|
918
|
+
* swapping the atlas re-seats the quad as well as the texels. Its four corners
|
|
919
|
+
* span the sub-rectangle of the drawing its own atlas kept, in `spine-core`'s
|
|
920
|
+
* corner order (left-bottom, left-top, right-top, right-bottom, read straight off
|
|
921
|
+
* that function's `uvs` assignments).
|
|
922
|
+
*
|
|
923
|
+
* ⚠️ `null` for a region whose own atlas packs it **rotated**: the corner order
|
|
924
|
+
* above is the unrotated one, and `RegionAttachment.computeUVs` assigns a
|
|
925
|
+
* different one at 90°. rigc emits one unrotated part per page and never packs, so
|
|
926
|
+
* no candidate this ships for reaches that branch; a refusal that names itself is
|
|
927
|
+
* better than a fourth opinion about a mapping only three callers have.
|
|
928
|
+
*/
|
|
929
|
+
function artUvsOf(attachment: MeshAttachment | RegionAttachment, region: TextureAtlasRegion): PieceTexture | undefined {
|
|
930
|
+
if (attachment instanceof MeshAttachment) {
|
|
931
|
+
return { region: regionKey(region), artUvs: Array.from(attachment.regionUVs) };
|
|
932
|
+
}
|
|
933
|
+
if (region.degrees !== 0) return undefined;
|
|
934
|
+
const ow = region.originalWidth;
|
|
935
|
+
const oh = region.originalHeight;
|
|
936
|
+
if (!(ow > 0) || !(oh > 0)) return undefined;
|
|
937
|
+
const s0 = region.offsetX / ow;
|
|
938
|
+
const s1 = (region.offsetX + region.width) / ow;
|
|
939
|
+
// `offsetY` is the trim measured from the drawing's BOTTOM and art space runs
|
|
940
|
+
// downwards, so the region's bottom edge is the larger of the two.
|
|
941
|
+
const tBottom = 1 - region.offsetY / oh;
|
|
942
|
+
const tTop = 1 - (region.offsetY + region.height) / oh;
|
|
943
|
+
return { region: regionKey(region), artUvs: [s0, tBottom, s0, tTop, s1, tTop, s1, tBottom] };
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* The skin roster of a parsed Spine file, as the runtime flags it —
|
|
948
|
+
* `unposedBones` over a fresh skeleton's `active` under each skin, so Spine's
|
|
949
|
+
* activation rule is the runtime's and is not restated here. Nothing is posed
|
|
950
|
+
* until a question is asked: the framing asks one (the skin it frames under,
|
|
951
|
+
* `slotsOnUnposedBones`) and the refusal asks one per skin.
|
|
952
|
+
*/
|
|
953
|
+
export function skinRosterOf(data: SkeletonData): SkinRoster {
|
|
954
|
+
return {
|
|
955
|
+
skins: data.skins.map((k) => k.name),
|
|
956
|
+
unposedUnder: (skin) => {
|
|
957
|
+
const skeleton = skeletonUnderSkin(data, skin);
|
|
958
|
+
return unposedBones(skeleton.bones.map((bone) => ({ name: bone.data.name, parent: bone.parent?.data.name ?? null, active: bone.active })));
|
|
959
|
+
},
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* An atlas's pages and regions as spine-core's `TextureAtlas` reads them, each
|
|
965
|
+
* region mapping the drawing's own coordinates onto its page through the
|
|
966
|
+
* runtime's `MeshAttachment.computeUVs` — the `spine` reader of
|
|
967
|
+
* `textureSubstitutionFromText` (`SubstitutionReader`), which reaches it
|
|
968
|
+
* through the seam. Moved here unchanged from that function's branch (issue
|
|
969
|
+
* #1052): it names the runtime, and that function does not.
|
|
970
|
+
*/
|
|
971
|
+
function spineSubstitution(atlasText: string): { pages: string[]; regions: Map<string, SubstituteRegion> } {
|
|
972
|
+
const regions = new Map<string, SubstituteRegion>();
|
|
973
|
+
const names: string[] = [];
|
|
974
|
+
const atlas = new TextureAtlas(atlasText);
|
|
975
|
+
for (const page of atlas.pages) names.push(page.name);
|
|
976
|
+
for (const region of atlas.regions) {
|
|
977
|
+
const page = { name: region.page.name, width: region.page.width, height: region.page.height };
|
|
978
|
+
regions.set(regionKey(region), {
|
|
979
|
+
page,
|
|
980
|
+
x: region.x,
|
|
981
|
+
y: region.y,
|
|
982
|
+
width: region.width,
|
|
983
|
+
height: region.height,
|
|
984
|
+
degrees: region.degrees,
|
|
985
|
+
pageUvs: (art) => {
|
|
986
|
+
const uvs = new Array<number>(art.length).fill(0);
|
|
987
|
+
MeshAttachment.computeUVs(region, [...art], uvs);
|
|
988
|
+
return uvs;
|
|
989
|
+
},
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
return { pages: names, regions };
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// ---------------------------------------------------------------------------
|
|
996
|
+
// the Spine side, registered (issue #1052)
|
|
997
|
+
// ---------------------------------------------------------------------------
|
|
998
|
+
//
|
|
999
|
+
// ⭐ Loading this file is what fills the seam `./render_shared.ts` reads: every
|
|
1000
|
+
// program that imports it — `cli.ts`, the tools, the selftest — poses an export,
|
|
1001
|
+
// `--poser spine` and a fallback through spine-core exactly as before, and one
|
|
1002
|
+
// that never loads it links nothing of the runtime. Functions only: nothing of
|
|
1003
|
+
// the runtime is touched here, so a runtime that cannot be used is still met
|
|
1004
|
+
// where a run first needs it (`requireSpineRuntime`).
|
|
1005
|
+
registerSpinePosing({
|
|
1006
|
+
requireRuntime: requireSpineRuntime,
|
|
1007
|
+
skeletonData: (skeletonText, atlasText, refuse) => spineSkeletonData(skeletonText, atlasText, refuse),
|
|
1008
|
+
facts: (data, atlasText) => spineFacts(data as SkeletonData, atlasText),
|
|
1009
|
+
poser: (data) => spinePoser(data as SkeletonData),
|
|
1010
|
+
skinRoster: (data) => skinRosterOf(data as SkeletonData),
|
|
1011
|
+
atlasPageNames,
|
|
1012
|
+
substitution: spineSubstitution,
|
|
1013
|
+
});
|