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/core/raw.ts
ADDED
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The core's raw entry (issue #966, step 3b of issue #380): one pose, or one
|
|
3
|
+
* animation walked at the caller's own steps, returned as the doubles the
|
|
4
|
+
* core computed — no grid — with everything a renderer or a deform measurer
|
|
5
|
+
* reads past the oracle's rows: each bone's world rotation and scale
|
|
6
|
+
* readings, each drawn attachment's local UVs, triangles, hull, sequence
|
|
7
|
+
* frame and colour, the clip polygons, the triangles drawn under a clip, and
|
|
8
|
+
* the events fired.
|
|
9
|
+
*
|
|
10
|
+
* The oracle's document (`tools/pose_oracle.ts`) samples an animation on a
|
|
11
|
+
* phase grid, re-applying it from the setup pose at every sample; the two
|
|
12
|
+
* consumers step instead. `src/render.ts` resets a track at 0 and then takes
|
|
13
|
+
* exactly one step of `1/fps` per frame; `src/deformmeasure.ts` resets and
|
|
14
|
+
* takes one step of `time`. So the entry takes a list of steps rather than a
|
|
15
|
+
* list of times, and this header states what one step is, as measured.
|
|
16
|
+
*
|
|
17
|
+
* ## One step — the walk, measured
|
|
18
|
+
*
|
|
19
|
+
* Measured against spine-core 4.3.13 running `src/render.ts`'s recipe
|
|
20
|
+
* (`AnimationState` with one non-looping track: frame 0 `apply`,
|
|
21
|
+
* `Skeleton.update(0)`, `updateWorldTransform(Physics.reset)`; every later
|
|
22
|
+
* frame `AnimationState.update(dt)`, `apply`, `Skeleton.update(dt)`,
|
|
23
|
+
* `updateWorldTransform(Physics.update)`), read off `TrackEntry.trackTime`,
|
|
24
|
+
* `TrackEntry.getAnimationTime()` and every bone's `appliedPose` in full
|
|
25
|
+
* doubles (the PR of issue #966 carries the table):
|
|
26
|
+
*
|
|
27
|
+
* - **The track time is the running sum of the steps**, `T += dt` in double,
|
|
28
|
+
* not `i·dt`, and the animation is applied at the sum. Over every
|
|
29
|
+
* animation of the nineteen tree rows `TrackEntry.trackTime` equalled the
|
|
30
|
+
* running sum on 2,612 of 2,612 frames at 12 fps (12,850 of 12,850 at 60,
|
|
31
|
+
* 5,173 of 5,173 at 24) and `i·dt` on only 662 (1,168; 810): at 12 fps
|
|
32
|
+
* frame 6 reads `0.49999999999999994`, not `0.5`, and frame 10
|
|
33
|
+
* `0.8333333333333334`, not `0.8333333333333333`. `src/render.ts` files a
|
|
34
|
+
* frame under `i·dt`, which is its label and not the time posed.
|
|
35
|
+
* - **A non-looping track holds at its duration**: the animation time is
|
|
36
|
+
* `min(T, duration)`, where `duration` is the runtime's (the last key of
|
|
37
|
+
* every timeline, as float32 — `CoreAnimationTimelines.duration`). With
|
|
38
|
+
* `round(duration·fps)` frames the last one overshoots on 20 animation
|
|
39
|
+
* frames of the 2,612 at 12 fps (19 at 60, 25 at 24), and
|
|
40
|
+
* `getAnimationTime()` read the duration on every one of them.
|
|
41
|
+
* - **The physics clock moves by the step itself**, `time += dt` (the
|
|
42
|
+
* skeleton's `update`), and a physics constraint integrates the time it
|
|
43
|
+
* moved (`./constraints_physics.ts`). Pose 0 is the reset, every physics
|
|
44
|
+
* state reset there (where, below).
|
|
45
|
+
* - **Events fire over `(previous animation time, animation time]`**, the
|
|
46
|
+
* first step's interval opening at −1 — so held past the duration, a step
|
|
47
|
+
* fires nothing. Measured against `AnimationState`'s event listener on
|
|
48
|
+
* every frame above.
|
|
49
|
+
* - **Where the reset is taken is the caller's** (`RawReset`): render's
|
|
50
|
+
* frame 0 applies the animation at 0 and resets there (`animation`);
|
|
51
|
+
* deformmeasure's `poseAt` resets at the setup pose and then applies the
|
|
52
|
+
* animation in its one step (`setup`). On a physics rig the two differ by
|
|
53
|
+
* whole units, not bits: of the 162 one-jump poses (three per animation),
|
|
54
|
+
* 12 read off under `animation` — six on `7-anticipation`'s physics rig (a
|
|
55
|
+
* cape bone 1.7° off at 0.617 s) and six on `gallery/look`'s — and none
|
|
56
|
+
* under `setup`.
|
|
57
|
+
*
|
|
58
|
+
* Every step poses the animation from the setup pose at its animation time
|
|
59
|
+
* (the setup blend at alpha 1), as the oracle's stepped dump does, applied
|
|
60
|
+
* from the animation time of the step before (−1 before the first): a
|
|
61
|
+
* physics `reset` key fires on the step that crosses it, once. Until issue
|
|
62
|
+
* #960 this walk fired such a key at every step from it on (the oracle's
|
|
63
|
+
* reading then), and against the recipe above a probe with a key at 0.5
|
|
64
|
+
* read 32 of 61 frames bit-exact at 60 fps, the rest off by up to 53 units;
|
|
65
|
+
* with the key crossed once, 61 of 61 at 60 fps and 13 of 13 at 12. None of
|
|
66
|
+
* the nineteen tree rows keys a `reset`, which is why the raw gate never
|
|
67
|
+
* saw it. The one difference from `--physics step` is where the steps fall,
|
|
68
|
+
* which is the caller's.
|
|
69
|
+
*
|
|
70
|
+
* ## What a pose carries
|
|
71
|
+
*
|
|
72
|
+
* - `bones` — the world matrix and origin, and the four readings the
|
|
73
|
+
* runtime's getters give: `rotationX = atan2(c, a)·DEG`, `rotationY =
|
|
74
|
+
* atan2(d, b)·DEG`, `scaleX = √(a² + c²)`, `scaleY = √(b² + d²)`, with
|
|
75
|
+
* `DEG = 180 / π` at the runtime's π (`RUNTIME_DEG` in `./world.ts`) —
|
|
76
|
+
* measured bit-exact against `BonePose.getWorldRotationX/Y` and
|
|
77
|
+
* `getWorldScaleX/Y` on every bone of 2,793 poses of the nineteen tree
|
|
78
|
+
* rows (the setup, every 12 fps frame, three jumps per animation).
|
|
79
|
+
* - `slots` — the oracle's slot row as doubles (`posedSlots`).
|
|
80
|
+
* - `drawn` — every region and mesh a slot shows, in draw order: its world
|
|
81
|
+
* vertices (the oracle's `attachments` row as doubles), its LOCAL UVs as the
|
|
82
|
+
* runtime holds them (a region's unit corners `0 1, 0 0, 1 0, 1 1` in the
|
|
83
|
+
* corner order; a mesh's `uvs` — a linked mesh's source's — as the
|
|
84
|
+
* document spells them: `MeshAttachment.regionUVs` read the doubles, not
|
|
85
|
+
* their float32, on every mesh of the nineteen rows — `6-arcs`'
|
|
86
|
+
* `2.554152e-7` where `Math.fround` gives `2.5541518766658555e-7`), its
|
|
87
|
+
* triangles (a region's `0 1 2 2
|
|
88
|
+
* 3 0`), its hull (a mesh's; `null` for a region), its sequence frame (the
|
|
89
|
+
* frame a sequence timeline set, else the series' setup frame, else 0 —
|
|
90
|
+
* the runtime gives every region and mesh a series, one frame long when
|
|
91
|
+
* the record states none, and `Sequence.resolveIndex` read 0 on every such
|
|
92
|
+
* attachment of the nineteen rows) and its own colour (the record's `color`, white
|
|
93
|
+
* unstated) — so the caller forms the tint, slot colour times attachment
|
|
94
|
+
* colour, itself. Where a region sits on a page is not the model's
|
|
95
|
+
* (`ModelAtlasRect`'s 🔸); the page UVs are issue #967's.
|
|
96
|
+
* - `clips`, `clipped` — the oracle's rows as doubles; `clipped` is what the
|
|
97
|
+
* core DRAWS, which is the oracle's block wherever the core poses it and,
|
|
98
|
+
* under a clip that is not strictly convex or is inverse (a block the core
|
|
99
|
+
* leaves out), the core's own convex decomposition (`clipThrough` in
|
|
100
|
+
* `./clipping.ts`, issue #964), held to spine-core by the render's pixels.
|
|
101
|
+
* - `events` — the oracle's event rows as doubles.
|
|
102
|
+
*
|
|
103
|
+
* 🔁 **A second walk shares this one** (issue #1025, cut 4c-5a): A10's
|
|
104
|
+
* LOOPING walk, which keeps a non-finite value rather than refusing it, is
|
|
105
|
+
* `./walk.ts`; both are `walkIn` below in a `WalkMode`, and every difference
|
|
106
|
+
* is a branch on the mode — so this entry's walk is the one it was, and the
|
|
107
|
+
* raw gate's lines did not move.
|
|
108
|
+
*
|
|
109
|
+
* ⛔ **Nothing is posed that the core would leave out.** Where the oracle's
|
|
110
|
+
* core dump names a block absent — a constraint the core cannot pose
|
|
111
|
+
* exactly, skins disagreeing over a placeholder under the merged view, a
|
|
112
|
+
* region with no atlas rectangle — the raw entry refuses the whole call,
|
|
113
|
+
* naming why (`CoreInputError`): a renderer handed a pose with a block
|
|
114
|
+
* missing would draw a different picture in silence.
|
|
115
|
+
*/
|
|
116
|
+
import { activeBones, CoreInputError, drawWalkOf, poseSetup, rawNumber, readColour, shownAttachment, sourceOfDoc, underNoSkin, type CompiledDocument, type CorePlant, type CoreSlotRow } from './index.ts';
|
|
117
|
+
import { posedBoneWorld, posedBoneWorldAlone, posedSlots, type TimelinePlant } from './animation.ts';
|
|
118
|
+
import { constraintsAbsentWhy, pathAnimationsWhy } from './constraints.ts';
|
|
119
|
+
import { freshStepContext, steppedPreviousPassWhy } from './constraints_physics.ts';
|
|
120
|
+
import { attachmentStates } from './deform.ts';
|
|
121
|
+
import { drawOrderAt } from './draw_order.ts';
|
|
122
|
+
import { eventsFired } from './events.ts';
|
|
123
|
+
import { REGION_TRIANGLES, REGION_UVS, type CoreClippedRow, type ShapeClipper } from './clipping.ts';
|
|
124
|
+
import { poseGeometry, type CoreAttachmentRow, type CoreClipRow, type ShownGeometry } from './vertices.ts';
|
|
125
|
+
import { RUNTIME_DEG, type CoreWorld } from './world.ts';
|
|
126
|
+
import type { SliderApplication } from './constraints_slider.ts';
|
|
127
|
+
|
|
128
|
+
/** One bone, posed: the world transform and the runtime getters' four readings (the header's `bones`). */
|
|
129
|
+
export interface RawBone {
|
|
130
|
+
name: string;
|
|
131
|
+
parent: string | null;
|
|
132
|
+
active: boolean;
|
|
133
|
+
a: number;
|
|
134
|
+
b: number;
|
|
135
|
+
c: number;
|
|
136
|
+
d: number;
|
|
137
|
+
worldX: number;
|
|
138
|
+
worldY: number;
|
|
139
|
+
rotationX: number;
|
|
140
|
+
rotationY: number;
|
|
141
|
+
scaleX: number;
|
|
142
|
+
scaleY: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** One region or mesh drawn, in draw order (the header's `drawn`). */
|
|
146
|
+
export interface RawDrawn {
|
|
147
|
+
slot: string;
|
|
148
|
+
attachment: string;
|
|
149
|
+
kind: 'region' | 'mesh';
|
|
150
|
+
vertices: number[];
|
|
151
|
+
uvs: number[];
|
|
152
|
+
triangles: number[];
|
|
153
|
+
hull: number | null;
|
|
154
|
+
sequenceIndex: number;
|
|
155
|
+
colour: [number, number, number, number];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** One clip polygon: `[slot, attachment, end, polygon]` as doubles. */
|
|
159
|
+
export type RawClip = [string, string, string | null, number[]];
|
|
160
|
+
/** One attachment drawn under a clip: `[slot, attachment, clipped, vertices, uvs, triangles]` as the clipper returns them. */
|
|
161
|
+
export type RawClipped = [string, string, 0 | 1, number[], number[], number[]];
|
|
162
|
+
/** One event fired: `[name, time, int, float, string]`. */
|
|
163
|
+
export type RawEvent = [string, number, number, number, string | null];
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Where a walk's physics is reset (the header's *One step*): `animation` —
|
|
167
|
+
* the animation applied at 0 and the reset taken there (`src/render.ts`'s
|
|
168
|
+
* frame 0); `setup` — the reset taken at the setup pose before the animation
|
|
169
|
+
* is applied (`src/deformmeasure.ts`'s `poseAt`), pose 0 then the setup pose.
|
|
170
|
+
*/
|
|
171
|
+
export type RawReset = 'animation' | 'setup';
|
|
172
|
+
|
|
173
|
+
/** One pose — the setup's, or one step of an animation's walk. */
|
|
174
|
+
export interface RawPose {
|
|
175
|
+
/** The track time (the running sum of the steps), 0 for the setup pose. */
|
|
176
|
+
trackTime: number;
|
|
177
|
+
/** The time the animation was applied at: the track time held at the duration. */
|
|
178
|
+
animationTime: number;
|
|
179
|
+
bones: RawBone[];
|
|
180
|
+
slots: CoreSlotRow[];
|
|
181
|
+
drawOrder: string[];
|
|
182
|
+
drawn: RawDrawn[];
|
|
183
|
+
clips: RawClip[];
|
|
184
|
+
clipped: RawClipped[];
|
|
185
|
+
events: RawEvent[];
|
|
186
|
+
// --- #968 render: begin ---
|
|
187
|
+
/**
|
|
188
|
+
* What each slot shows, in draw order — the records `drawn` was posed from
|
|
189
|
+
* (skin, placeholder, series frame, deform). `src/render_core.ts` resolves
|
|
190
|
+
* each drawn attachment's atlas region and page UVs through it, with
|
|
191
|
+
* `./uvs.ts`'s `drawnRegions` (issue #967's gated rule), rather than walking
|
|
192
|
+
* which record a slot shows a second time.
|
|
193
|
+
*/
|
|
194
|
+
shown: ShownGeometry[];
|
|
195
|
+
// --- #968 render: end ---
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* One bone as the scan reads it (issue #1179, the second part): the world
|
|
200
|
+
* matrix and origin `RawBone` carries, and none of the readings it forms from
|
|
201
|
+
* them. `A10_NO_NAN_AFTER_STEPPING` reads these six numbers and the name off a
|
|
202
|
+
* bone, and nothing else (`firstNonFinite` in `src/nonfinite.ts`).
|
|
203
|
+
*/
|
|
204
|
+
export interface ScanBone {
|
|
205
|
+
name: string;
|
|
206
|
+
a: number;
|
|
207
|
+
b: number;
|
|
208
|
+
c: number;
|
|
209
|
+
d: number;
|
|
210
|
+
worldX: number;
|
|
211
|
+
worldY: number;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* One pose as the scan reads it (issue #1179, the second part) — the scan's
|
|
216
|
+
* walk (`scanWalkIn`, `scanSetupIn`): the time the animation was applied at,
|
|
217
|
+
* and the bones, slot rows and drawn attachments computed exactly as a
|
|
218
|
+
* `RawPose`'s are. Internal to A10's walk (`./walk.ts`); no exported pose type
|
|
219
|
+
* changed for it.
|
|
220
|
+
*/
|
|
221
|
+
export interface ScanPose {
|
|
222
|
+
animationTime: number;
|
|
223
|
+
bones: ScanBone[];
|
|
224
|
+
slots: CoreSlotRow[];
|
|
225
|
+
drawn: ScanDrawn[];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** One region or mesh drawn, as the scan reads it: its slot, its name and its world vertices, as `RawDrawn` carries them. */
|
|
229
|
+
export interface ScanDrawn {
|
|
230
|
+
slot: string;
|
|
231
|
+
attachment: string;
|
|
232
|
+
vertices: number[];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// --- #968 render: begin ---
|
|
236
|
+
/** The shown records in `order`, the pose's draw order (a pose's `shown`). */
|
|
237
|
+
function drawOrderOf(shown: readonly ShownGeometry[], order: readonly string[]): ShownGeometry[] {
|
|
238
|
+
const rank = new Map(order.map((n, r) => [n, r]));
|
|
239
|
+
return [...shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
|
|
240
|
+
}
|
|
241
|
+
// --- #968 render: end ---
|
|
242
|
+
|
|
243
|
+
/** A double the entry computed: never `null`, since a non-finite pose is refused rather than passed on. */
|
|
244
|
+
function finite(v: number | null, what: string): number {
|
|
245
|
+
if (v === null) throw new CoreInputError(`the raw pose computed a non-finite ${what}`);
|
|
246
|
+
return v;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* How a walk treats the two things the raw entry and the looping walk
|
|
251
|
+
* (`./walk.ts`, issue #1025) read differently: whether the track loops, and
|
|
252
|
+
* whether a value that is not finite is kept in the pose or refuses the call.
|
|
253
|
+
* The raw entry is `{ loop: false, keep: false }`, and that is its contract.
|
|
254
|
+
*/
|
|
255
|
+
export interface WalkMode {
|
|
256
|
+
loop: boolean;
|
|
257
|
+
keep: boolean;
|
|
258
|
+
/** A plant only (the core suite's `CO26`): the animation time a looping step applies at, from its track time, the duration and the track time before it — `loopedTime` unless given. */
|
|
259
|
+
time?: (trackTime: number, duration: number, before: number) => number;
|
|
260
|
+
/** A plant only (`CO26`): what the physics clock moves by, from the step and how far the animation time moved — the step itself unless given. */
|
|
261
|
+
clock?: (dt: number, moved: number) => number;
|
|
262
|
+
/** A plant only (`CO26`): `false` fires no `reset` key across the wrap — the reading the walk was measured against and rejected. */
|
|
263
|
+
wrapResets?: boolean;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The raw entry's mode: a track that holds at its duration, and a non-finite value refused. */
|
|
267
|
+
const RAW_MODE: WalkMode = { loop: false, keep: false };
|
|
268
|
+
|
|
269
|
+
/** What a kept walk writes a number as: the double itself, finite or not — so a NaN and an Infinity stay what they are. */
|
|
270
|
+
const keptNumber = (v: number): number => v;
|
|
271
|
+
|
|
272
|
+
/** The plant a walk in `mode` poses with: the raw entry's unrounded double, or under `keep` the number whatever it is. */
|
|
273
|
+
const roundPlant = (mode: WalkMode): CorePlant => ({ round: mode.keep ? keptNumber : rawNumber });
|
|
274
|
+
|
|
275
|
+
/** A double a kept walk computed, as it is; a raw walk's, refused when it is not finite (`finite`). */
|
|
276
|
+
function numberOf(v: number | null, what: string, mode: WalkMode): number {
|
|
277
|
+
return mode.keep ? (v === null ? Number.NaN : v) : finite(v, what);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* What the looping walk cuts a drawn attachment through under a clip: nothing
|
|
282
|
+
* (issue #1179). Its one reader, A10, reads no clipped row — `./walk.ts`
|
|
283
|
+
* narrows them away with the events — so the walk does not cut them; what it
|
|
284
|
+
* still does is plan every clip it starts (`poseClipped`'s `clipShapeOf`), so
|
|
285
|
+
* a clip the core does not draw refuses the walk by name as before. Every
|
|
286
|
+
* number a looping pose carries is computed as it was.
|
|
287
|
+
*/
|
|
288
|
+
const NO_CLIPPED_ROWS: ShapeClipper = () => null;
|
|
289
|
+
|
|
290
|
+
/** The bones of a pose from the world transforms (the header's `bones`). */
|
|
291
|
+
function rawBones(doc: CompiledDocument, world: ReadonlyMap<string, CoreWorld>): RawBone[] {
|
|
292
|
+
const active = activeBones(doc);
|
|
293
|
+
return doc.bones.map((b): RawBone => {
|
|
294
|
+
const w = world.get(b.name);
|
|
295
|
+
if (w === undefined) throw new CoreInputError(`bone "${b.name}" has no world transform`);
|
|
296
|
+
return {
|
|
297
|
+
name: b.name,
|
|
298
|
+
parent: b.parent ?? null,
|
|
299
|
+
active: active.has(b.name),
|
|
300
|
+
a: w.a,
|
|
301
|
+
b: w.b,
|
|
302
|
+
c: w.c,
|
|
303
|
+
d: w.d,
|
|
304
|
+
worldX: w.worldX,
|
|
305
|
+
worldY: w.worldY,
|
|
306
|
+
rotationX: Math.atan2(w.c, w.a) * RUNTIME_DEG,
|
|
307
|
+
rotationY: Math.atan2(w.d, w.b) * RUNTIME_DEG,
|
|
308
|
+
scaleX: Math.sqrt(w.a * w.a + w.c * w.c),
|
|
309
|
+
scaleY: Math.sqrt(w.b * w.b + w.d * w.d),
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The bones of a scan pose (`ScanBone`): `rawBones` less the parent, the
|
|
316
|
+
* active flag and the four getter readings, which nothing the scan reads is
|
|
317
|
+
* computed from — each reading is formed from the world after it is posed,
|
|
318
|
+
* and the matrix and origin are copied as they are.
|
|
319
|
+
*/
|
|
320
|
+
function scanBones(doc: CompiledDocument, world: ReadonlyMap<string, CoreWorld>): ScanBone[] {
|
|
321
|
+
return doc.bones.map((b): ScanBone => {
|
|
322
|
+
const w = world.get(b.name);
|
|
323
|
+
if (w === undefined) throw new CoreInputError(`bone "${b.name}" has no world transform`);
|
|
324
|
+
return { name: b.name, a: w.a, b: w.b, c: w.c, d: w.d, worldX: w.worldX, worldY: w.worldY };
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* What a pose draws, posed and refused where the core leaves it out: the
|
|
330
|
+
* attachment rows, the clip rows and the drawn clipped rows — what `rawDrawn`
|
|
331
|
+
* and `scanDrawn` both assemble from.
|
|
332
|
+
*/
|
|
333
|
+
function drawnGeometry(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant, mode: WalkMode): { rows: CoreAttachmentRow[]; clips: CoreClipRow[]; drawnClipped: CoreClippedRow[] } {
|
|
334
|
+
const draw = drawWalkOf(doc, order, plant);
|
|
335
|
+
const geometry = poseGeometry(shown, world, sourceOfDoc(doc), mode.keep ? keptNumber : rawNumber, { region: plant.region, vertices: plant.vertices }, mode.loop && plant.through === undefined ? { ...draw, through: NO_CLIPPED_ROWS } : draw);
|
|
336
|
+
if (geometry.attachments === null) throw new CoreInputError(`the raw pose leaves the attachments out: ${geometry.attachmentsWhy}`);
|
|
337
|
+
// The drawn rows (issue #964): the oracle's `clipped` rows where it is posed, and a concave or inverse clip cut through the core's own decomposition.
|
|
338
|
+
if (geometry.drawnClipped === null) throw new CoreInputError(`the raw pose leaves the clipped triangles out: ${geometry.drawnClippedWhy}`);
|
|
339
|
+
return { rows: geometry.attachments, clips: geometry.clips, drawnClipped: geometry.drawnClipped };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* The drawn attachments of a scan pose (`ScanDrawn`): `rawDrawn`'s rows, each
|
|
344
|
+
* its slot, its name and its world vertices through the same `numberOf`, and
|
|
345
|
+
* none of what `rawDrawn` reads past them for a renderer — the UVs and
|
|
346
|
+
* triangles it copies, the record it looks up for the colour and the sequence
|
|
347
|
+
* frame, the hull, the clip rows' numbers. No vertex is computed from any of
|
|
348
|
+
* those. The one refusal `rawDrawn` makes past the rows, a linked mesh whose
|
|
349
|
+
* source carries no mesh, is not reachable here: `poseGeometry` resolved the
|
|
350
|
+
* same source by the same lookup (`sourceOfDoc`) and throws before it returns.
|
|
351
|
+
*
|
|
352
|
+
* 🔁 **No copy of the vertices** (issue #1179, the second part): `rawDrawn`
|
|
353
|
+
* maps each row through `numberOf` into a new array; under `keep` that map is
|
|
354
|
+
* the identity on every number and turns only a `null` into NaN. The scan
|
|
355
|
+
* hands the row's own array on wherever it holds no `null` (`allNumbers`) and
|
|
356
|
+
* maps it as before where it does, so A10 reads the same values either way.
|
|
357
|
+
* What makes the row's array safe to keep past the step — A10 collects an
|
|
358
|
+
* animation's 120 frames before it scans one — is that it is the pose's own:
|
|
359
|
+
* `poseGeometry` (`./vertices.ts`) builds every row's vertices with
|
|
360
|
+
* `.map(round)` over the corners or the skinned vertices it just computed, a
|
|
361
|
+
* new array per attachment per pose, held by nothing but the row. The core
|
|
362
|
+
* suite's `CO45` plants a poser that hands every pose the same array and reads
|
|
363
|
+
* the retained frames red.
|
|
364
|
+
*/
|
|
365
|
+
function scanDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant, mode: WalkMode, scanPlant: ScanPlant = {}): ScanDrawn[] {
|
|
366
|
+
const { rows } = drawnGeometry(doc, shown, world, order, plant, mode);
|
|
367
|
+
const drawn: ScanDrawn[] = [];
|
|
368
|
+
let k = 0;
|
|
369
|
+
for (const s of shown) {
|
|
370
|
+
const g = s.geometry;
|
|
371
|
+
if (g.kind !== 'region' && g.kind !== 'mesh' && g.kind !== 'linkedmesh') continue;
|
|
372
|
+
const row = rows[k++];
|
|
373
|
+
if (row === undefined || row[0] !== s.slot) throw new CoreInputError(`slot "${s.slot}": the attachment rows are not in the shown records' order`);
|
|
374
|
+
const vertices = row[3];
|
|
375
|
+
const kept = mode.keep && allNumbers(vertices) ? vertices : vertices.map((v) => numberOf(v, `vertex of slot "${s.slot}"`, mode));
|
|
376
|
+
drawn.push({ slot: s.slot, attachment: row[1], vertices: scanPlant.vertices === undefined ? kept : scanPlant.vertices(kept, s.slot) });
|
|
377
|
+
}
|
|
378
|
+
return drawn;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Whether a row's vertices hold no `null` — what `numberOf` would map — so the row's own array can be handed on as numbers. */
|
|
382
|
+
function allNumbers(values: Array<number | null>): values is number[] {
|
|
383
|
+
for (const v of values) if (v === null) return false;
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The scan's one plant (the core suite's `CO45`): what a drawn row's vertices
|
|
389
|
+
* are handed on as, given the array the scan would hand on. A poser that
|
|
390
|
+
* reuses one buffer per slot across steps is the plant `CO45` passes.
|
|
391
|
+
* Nothing but a control passes one.
|
|
392
|
+
*/
|
|
393
|
+
export interface ScanPlant {
|
|
394
|
+
vertices?: (vertices: number[], slot: string) => number[];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Every region and mesh the shown records draw, with what the renderer reads past the vertices (the header's `drawn`). */
|
|
398
|
+
function rawDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant, mode: WalkMode = RAW_MODE): { drawn: RawDrawn[]; clips: RawClip[]; clipped: RawClipped[] } {
|
|
399
|
+
const geometry = drawnGeometry(doc, shown, world, order, plant, mode);
|
|
400
|
+
// Under a concave or inverse clip the oracle's `clipped` block is absent (the runtime's own triangle list); what the core draws is `drawnClipped`,
|
|
401
|
+
// its own decomposition, and the render samples each drawn triangle at its source triangle's affine UV, so the pixels are the decomposition's
|
|
402
|
+
// coverage alone (issue #964, `Mesh.source` in src/render.ts).
|
|
403
|
+
const rows = geometry.rows;
|
|
404
|
+
const drawn: RawDrawn[] = [];
|
|
405
|
+
let k = 0;
|
|
406
|
+
for (const s of shown) {
|
|
407
|
+
const g = s.geometry;
|
|
408
|
+
if (g.kind !== 'region' && g.kind !== 'mesh' && g.kind !== 'linkedmesh') continue;
|
|
409
|
+
const row = rows[k++];
|
|
410
|
+
if (row === undefined || row[0] !== s.slot) throw new CoreInputError(`slot "${s.slot}": the attachment rows are not in the shown records' order`);
|
|
411
|
+
const record = doc.skins.find((x) => x.name === s.skin)?.attachments[s.slot]?.[s.placeholder];
|
|
412
|
+
const colour = record?.color === undefined ? ([1, 1, 1, 1] as [number, number, number, number]) : readColour(record.color);
|
|
413
|
+
let uvs: number[];
|
|
414
|
+
let triangles: number[];
|
|
415
|
+
let hull: number | null = null;
|
|
416
|
+
if (g.kind === 'region') {
|
|
417
|
+
uvs = [...REGION_UVS];
|
|
418
|
+
triangles = [...REGION_TRIANGLES];
|
|
419
|
+
} else {
|
|
420
|
+
const source = g.kind === 'mesh' ? g : doc.skins.find((x) => x.name === g.skin)?.attachments[g.slot]?.[g.source]?.geometry;
|
|
421
|
+
if (source === undefined || source.kind !== 'mesh') throw new CoreInputError(`slot "${s.slot}": the linked mesh's source carries no mesh`);
|
|
422
|
+
uvs = [...source.uvs];
|
|
423
|
+
triangles = [...source.triangles];
|
|
424
|
+
hull = source.hull ?? null;
|
|
425
|
+
}
|
|
426
|
+
const sequenceIndex = s.frame ?? record?.sequenceSetup ?? 0;
|
|
427
|
+
drawn.push({ slot: s.slot, attachment: row[1], kind: row[2], vertices: row[3].map((v) => numberOf(v, `vertex of slot "${s.slot}"`, mode)), uvs, triangles, hull, sequenceIndex, colour });
|
|
428
|
+
}
|
|
429
|
+
const clips = geometry.clips.map((c): RawClip => [c[0], c[1], c[2], c[3].map((v) => numberOf(v, `clip vertex of slot "${c[0]}"`, mode))]);
|
|
430
|
+
const clipped = geometry.drawnClipped.map((c): RawClipped => [c[0], c[1], c[2], c[3].map((v) => numberOf(v, `clipped vertex of slot "${c[0]}"`, mode)), c[4].map((v) => numberOf(v, `clipped uv of slot "${c[0]}"`, mode)), c[5]]);
|
|
431
|
+
return { drawn, clips, clipped };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* The setup pose as the renderer poses a skeleton with no animation — the
|
|
436
|
+
* setup pose, every physics state reset — in full doubles. Refused by name
|
|
437
|
+
* where the core leaves a block of it out.
|
|
438
|
+
*/
|
|
439
|
+
export function poseRawSetup(doc: CompiledDocument, plant: CorePlant = {}): RawPose {
|
|
440
|
+
return setupPoseIn(doc, plant, RAW_MODE);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** `poseRawSetup` in `mode` — under `keep`, a value that is not finite stays in the pose (`./walk.ts`). */
|
|
444
|
+
export function setupPoseIn(doc: CompiledDocument, plant: CorePlant, mode: WalkMode): RawPose {
|
|
445
|
+
const { world, shown, slots, drawOrder } = setupPosed(doc, plant, mode);
|
|
446
|
+
const drawn = rawDrawn(doc, shown, world, drawOrder, plant, mode);
|
|
447
|
+
return { trackTime: 0, animationTime: 0, bones: rawBones(doc, world), slots, drawOrder, ...drawn, events: [], shown: drawOrderOf(shown, drawOrder) };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** `setupPoseIn` as the scan reads it (`ScanPose`): the same setup pose, the same refusals in the same order, assembled into the scan's pose. */
|
|
451
|
+
export function scanSetupIn(doc: CompiledDocument, plant: CorePlant, mode: WalkMode, scanPlant: ScanPlant = {}): ScanPose {
|
|
452
|
+
const { world, shown, slots, drawOrder } = setupPosed(doc, plant, mode);
|
|
453
|
+
const drawn = scanDrawn(doc, shown, world, drawOrder, plant, mode, scanPlant);
|
|
454
|
+
return { animationTime: 0, bones: scanBones(doc, world), slots, drawn };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** The setup pose `setupPoseIn` and `scanSetupIn` assemble, refused by name where the core leaves a block of it out. */
|
|
458
|
+
function setupPosed(doc: CompiledDocument, plant: CorePlant, mode: WalkMode): { world: Map<string, CoreWorld>; shown: ShownGeometry[]; slots: CoreSlotRow[]; drawOrder: string[] } {
|
|
459
|
+
const posed = poseSetup(doc, { ...plant, ...roundPlant(mode) }, freshStepContext(plant.physicsStep));
|
|
460
|
+
// `setup.clipped` is the oracle's block, left out under a concave or inverse clip whose triangle list is the runtime's own; what the core draws there is `rawDrawn`'s to refuse or pose (issue #964).
|
|
461
|
+
const absent = posed.absent.filter(([block]) => block !== 'setup.clipped');
|
|
462
|
+
if (absent.length > 0) throw new CoreInputError(`the raw setup pose leaves ${absent.map(([b, why]) => `${b} out (${why})`).join('; ')}`);
|
|
463
|
+
const { setup, world, shown } = posed;
|
|
464
|
+
if (world === null || shown === null || setup.slots === null || setup.drawOrder === null) throw new CoreInputError('the raw setup pose was not posed');
|
|
465
|
+
return { world, shown, slots: setup.slots, drawOrder: setup.drawOrder };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* One animation walked at the caller's steps (the header's *One step*): the
|
|
470
|
+
* reset pose at 0, then one pose per step of `steps` — `steps.length + 1`
|
|
471
|
+
* poses in all. `src/render.ts`'s walk is `count` steps of `1/fps`;
|
|
472
|
+
* `src/deformmeasure.ts`'s is one step of `time`. A step must be a finite
|
|
473
|
+
* time at or above 0; the animation must be the document's; a construct the
|
|
474
|
+
* core would leave out refuses the call by name. `reset` is where the
|
|
475
|
+
* physics is reset (`RawReset`): `animation` for render's walk, `setup` for
|
|
476
|
+
* deformmeasure's one jump.
|
|
477
|
+
*/
|
|
478
|
+
export function poseRawAnimation(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant = {}, reset: RawReset = 'animation'): RawPose[] {
|
|
479
|
+
return walkIn(doc, animation, steps, plant, reset, RAW_MODE);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* `poseRawAnimation`, each pose handed to `visit` as it is posed rather than
|
|
484
|
+
* collected (issue #1180): the same walk, the same poses in the same order,
|
|
485
|
+
* and the caller holds as many of them as it keeps. `src/render_core.ts`
|
|
486
|
+
* draws each and lets it go, so a render holds one pose and not an
|
|
487
|
+
* animation's series — on the production rig the core poser's render peaked
|
|
488
|
+
* highest on, the largest animation's series was 153 MiB of retained heap.
|
|
489
|
+
* A refusal is thrown from the pose it is met at, after `visit` has seen the
|
|
490
|
+
* poses before it.
|
|
491
|
+
*/
|
|
492
|
+
export function poseRawAnimationEach(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, visit: (pose: RawPose, index: number) => void): void {
|
|
493
|
+
walkEach(doc, animation, steps, plant, reset, RAW_MODE, RAW_SHAPE, visit);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* The animation time a looping track applies its animation at (`./walk.ts`,
|
|
498
|
+
* *The time*): the track time wrapped by the duration, and 0 over a duration
|
|
499
|
+
* of 0.
|
|
500
|
+
*/
|
|
501
|
+
export function loopedTime(trackTime: number, duration: number): number {
|
|
502
|
+
return duration === 0 ? 0 : trackTime % duration;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* `poseRawAnimation` in `mode`: the raw entry's walk, or the looping walk
|
|
507
|
+
* `./walk.ts` documents — the animation time the track time wrapped by the
|
|
508
|
+
* duration, every value kept whatever it is. Each difference is a branch on
|
|
509
|
+
* `mode` here, so the raw entry's walk is the one it was.
|
|
510
|
+
*/
|
|
511
|
+
export function walkIn(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, mode: WalkMode): RawPose[] {
|
|
512
|
+
const poses: RawPose[] = [];
|
|
513
|
+
walkEach(doc, animation, steps, plant, reset, mode, RAW_SHAPE, (pose) => poses.push(pose));
|
|
514
|
+
return poses;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* `walkIn` as the scan reads it (`ScanPose`, issue #1179, the second part):
|
|
519
|
+
* the same walk — every pose posed by the same operations in the same order,
|
|
520
|
+
* every refusal at the same place — each pose assembled into what A10 reads.
|
|
521
|
+
* `./walk.ts`'s `scanLoopingWalk` is its one caller.
|
|
522
|
+
*/
|
|
523
|
+
export function scanWalkIn(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, mode: WalkMode, scanPlant: ScanPlant = {}): ScanPose[] {
|
|
524
|
+
const poses: ScanPose[] = [];
|
|
525
|
+
walkEach(doc, animation, steps, plant, reset, mode, scanPlant.vertices === undefined ? SCAN_SHAPE : scanShape(scanPlant), (pose) => poses.push(pose));
|
|
526
|
+
return poses;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** What one step of a walk posed, before a pose is assembled from it (`PoseShape`). */
|
|
530
|
+
interface WalkStep {
|
|
531
|
+
trackTime: number;
|
|
532
|
+
animationTime: number;
|
|
533
|
+
world: Map<string, CoreWorld>;
|
|
534
|
+
slots: CoreSlotRow[];
|
|
535
|
+
drawOrder: string[];
|
|
536
|
+
shown: ShownGeometry[];
|
|
537
|
+
/** The events the step fired, computed when called — after the drawn rows, as the raw entry has always ordered its refusals. */
|
|
538
|
+
events: () => RawEvent[];
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* What a walk assembles each pose into (issue #1179, the second part): the raw
|
|
543
|
+
* entry's whole pose (`RAW_SHAPE`), or the scan's (`SCAN_SHAPE`). The walk
|
|
544
|
+
* (`walkEach`) is one body: every number a pose carries is computed there and
|
|
545
|
+
* in the two assemblers below, by the same calls, whatever the shape. A shape
|
|
546
|
+
* chooses only what is assembled from what was posed — the scan drops the
|
|
547
|
+
* readings, rows and copies nothing it reads is computed from — and keeps the
|
|
548
|
+
* order of every call that can refuse.
|
|
549
|
+
*/
|
|
550
|
+
interface PoseShape<P> {
|
|
551
|
+
/** Whether a step forms the oracle's bone rows beside its world (`posedBoneWorld`), which no walk pose carries; `false` poses the world alone (`posedBoneWorldAlone`). */
|
|
552
|
+
rows: boolean;
|
|
553
|
+
/** The reset pose at the setup (`reset: 'setup'`). */
|
|
554
|
+
setup: (doc: CompiledDocument, world: Map<string, CoreWorld>, slots: CoreSlotRow[], drawOrder: string[], shown: ShownGeometry[], plant: CorePlant, mode: WalkMode) => P;
|
|
555
|
+
/** One step's pose. */
|
|
556
|
+
step: (doc: CompiledDocument, posed: WalkStep, plant: CorePlant, mode: WalkMode) => P;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** The raw entry's pose, assembled as it always was: at the reset the bones, then the drawn rows; at a step the drawn rows, the events, then the bones. */
|
|
560
|
+
const RAW_SHAPE: PoseShape<RawPose> = {
|
|
561
|
+
rows: true,
|
|
562
|
+
setup: (doc, world, slots, drawOrder, shown, plant, mode) => ({ trackTime: 0, animationTime: 0, bones: rawBones(doc, world), slots, drawOrder, ...rawDrawn(doc, shown, world, drawOrder, plant, mode), events: [], shown: drawOrderOf(shown, drawOrder) }),
|
|
563
|
+
step: (doc, p, plant, mode) => {
|
|
564
|
+
const drawn = rawDrawn(doc, p.shown, p.world, p.drawOrder, plant, mode);
|
|
565
|
+
const events = p.events();
|
|
566
|
+
return { trackTime: p.trackTime, animationTime: p.animationTime, bones: rawBones(doc, p.world), slots: p.slots, drawOrder: p.drawOrder, ...drawn, events, shown: p.shown };
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
/** The scan's pose (`ScanPose`), in the raw shape's order: the scan's walk is a looping one, which fires no event. */
|
|
571
|
+
function scanShape(scanPlant: ScanPlant): PoseShape<ScanPose> {
|
|
572
|
+
return {
|
|
573
|
+
rows: false,
|
|
574
|
+
setup: (doc, world, slots, drawOrder, shown, plant, mode) => {
|
|
575
|
+
const bones = scanBones(doc, world);
|
|
576
|
+
return { animationTime: 0, bones, slots, drawn: scanDrawn(doc, shown, world, drawOrder, plant, mode, scanPlant) };
|
|
577
|
+
},
|
|
578
|
+
step: (doc, p, plant, mode) => {
|
|
579
|
+
const drawn = scanDrawn(doc, p.shown, p.world, p.drawOrder, plant, mode, scanPlant);
|
|
580
|
+
return { animationTime: p.animationTime, bones: scanBones(doc, p.world), slots: p.slots, drawn };
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
const SCAN_SHAPE: PoseShape<ScanPose> = scanShape({});
|
|
585
|
+
|
|
586
|
+
/** `walkIn`'s walk, each pose assembled in `shape` and handed to `visit` in order as it is posed (`poseRawAnimationEach`). */
|
|
587
|
+
function walkEach<P>(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, mode: WalkMode, shape: PoseShape<P>, visit: (pose: P, index: number) => void): void {
|
|
588
|
+
const anim = doc.animations.find((a) => a.name === animation);
|
|
589
|
+
if (anim === undefined) throw new CoreInputError(`animation "${animation}" is not one of this document's [${doc.animations.map((a) => a.name).join(', ')}]`);
|
|
590
|
+
const bad = steps.findIndex((s) => !Number.isFinite(s) || s < 0);
|
|
591
|
+
if (bad >= 0) throw new CoreInputError(`step ${bad} is ${steps[bad]}, not a finite time at or above 0`);
|
|
592
|
+
const why = constraintsAbsentWhy(doc) ?? pathAnimationsWhy(doc) ?? steppedPreviousPassWhy(doc);
|
|
593
|
+
if (why !== null) throw new CoreInputError(`the raw walk leaves the bones out: ${why}`);
|
|
594
|
+
const raw: TimelinePlant = { ...plant, ...roundPlant(mode) };
|
|
595
|
+
const resolve = plant.shown ?? shownAttachment;
|
|
596
|
+
const duration = anim.timelines.duration;
|
|
597
|
+
const ctx = freshStepContext(plant.physicsStep);
|
|
598
|
+
if (mode.loop && mode.wrapResets !== false) ctx.loop = true;
|
|
599
|
+
let visited = 0;
|
|
600
|
+
let trackTime = 0;
|
|
601
|
+
let last = -1;
|
|
602
|
+
if (reset === 'setup') {
|
|
603
|
+
// The reset taken at the setup pose, before the animation is applied (`src/deformmeasure.ts`'s `poseAt`): pose 0 is the setup pose.
|
|
604
|
+
const setup = poseSetup(doc, { ...plant, ...roundPlant(mode) }, ctx);
|
|
605
|
+
const absent = setup.absent.filter(([block]) => block !== 'setup.clipped');
|
|
606
|
+
if (absent.length > 0) throw new CoreInputError(`the raw walk's reset pose leaves ${absent.map(([b, w]) => `${b} out (${w})`).join('; ')}`);
|
|
607
|
+
if (setup.world === null || setup.shown === null || setup.setup.slots === null || setup.setup.drawOrder === null) throw new CoreInputError('the raw walk\'s reset pose was not posed');
|
|
608
|
+
ctx.phase = 'update';
|
|
609
|
+
visit(shape.setup(doc, setup.world, setup.setup.slots, setup.setup.drawOrder, setup.shown, plant, mode), visited++);
|
|
610
|
+
}
|
|
611
|
+
for (let i = reset === 'setup' ? 1 : 0; i <= steps.length; i++) {
|
|
612
|
+
const dt = i === 0 ? 0 : steps[i - 1];
|
|
613
|
+
trackTime += dt;
|
|
614
|
+
const t = mode.loop ? (mode.time ?? loopedTime)(trackTime, duration, trackTime - dt) : trackTime < duration ? trackTime : duration;
|
|
615
|
+
const before = ctx.time;
|
|
616
|
+
ctx.time += mode.clock === undefined ? dt : mode.clock(dt, t - (i === 0 ? 0 : Math.max(last, 0)));
|
|
617
|
+
const sliders: SliderApplication[] = [];
|
|
618
|
+
const world = shape.rows ? posedBoneWorld(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before }).world : posedBoneWorldAlone(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before });
|
|
619
|
+
if (i === 0) ctx.phase = 'update';
|
|
620
|
+
const placeholders = new Map<string, string | null>();
|
|
621
|
+
const slots = posedSlots(doc, anim.timelines, t, raw, sliders, placeholders);
|
|
622
|
+
if (slots.conflicts.length > 0) throw new CoreInputError(`the raw walk leaves the slots out: ${slots.conflicts.join('; ')}`);
|
|
623
|
+
const evalOrder = plant.drawOrder ?? drawOrderAt;
|
|
624
|
+
let order = evalOrder(doc.slots.length, anim.timelines.drawOrder, t) ?? doc.slots.map((_s, k) => k);
|
|
625
|
+
for (const app of sliders) order = evalOrder(doc.slots.length, app.timelines.drawOrder, app.at) ?? order;
|
|
626
|
+
const drawOrder = order.map((k) => doc.slots[k].name);
|
|
627
|
+
const states = attachmentStates(doc, resolve, placeholders, { timelines: anim.timelines, t }, sliders, plant);
|
|
628
|
+
if (states.why.length > 0) throw new CoreInputError(`the raw walk leaves the attachments out: ${states.why.join('; ')}`);
|
|
629
|
+
const rank = new Map(drawOrder.map((n, r) => [n, r]));
|
|
630
|
+
const shown = [...states.shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
|
|
631
|
+
// A looping walk fires no event here: what fires across the wrap was not measured, and the walk's one reader (A10) reads none. Nor does it cut its
|
|
632
|
+
// drawn attachments through a clip (`NO_CLIPPED_ROWS`), so its `clipped` rows are empty.
|
|
633
|
+
const from = last;
|
|
634
|
+
const events = (): RawEvent[] => (mode.loop ? [] : (plant.events ?? eventsFired)(anim.timelines.events, from, t, rawNumber).map((e): RawEvent => [e[0], finite(e[1], 'event time'), e[2], finite(e[3], 'event float'), e[4]]));
|
|
635
|
+
const pose = shape.step(doc, { trackTime, animationTime: t, world, slots: slots.rows, drawOrder, shown, events }, plant, mode);
|
|
636
|
+
last = t;
|
|
637
|
+
visit(pose, visited++);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// --- #907 the setup-pose bounding box: begin ---
|
|
642
|
+
/**
|
|
643
|
+
* The setup-pose bounding box (issue #907) — `setupBounds` below: the axis-aligned box around every
|
|
644
|
+
* region and mesh a skeleton draws at its setup pose, as spine-core's
|
|
645
|
+
* `Skeleton.getBounds(offset, size, temp)` returns it on a fresh skeleton —
|
|
646
|
+
* no skin set, `updateWorldTransform(Physics.none)` — which is what the Spine
|
|
647
|
+
* format says the header's `x`, `y`, `width` and `height` are.
|
|
648
|
+
*
|
|
649
|
+
* Every rule below was measured by running spine-core 4.3.13 through the
|
|
650
|
+
* tree's own loader, not read off its source:
|
|
651
|
+
*
|
|
652
|
+
* - **The view is "no skin set"** (`underNoSkin`, issue #1051): a skeleton
|
|
653
|
+
* `build` writes is loaded and bounded before anyone calls `setSkin`, so
|
|
654
|
+
* the slots resolve through the default skin alone and no skin's bones or
|
|
655
|
+
* constraint lists apply.
|
|
656
|
+
* - **Constraints applied.** The box is of the pose `updateWorldTransform`
|
|
657
|
+
* leaves, every ik, transform, path and slider constraint applied in the
|
|
658
|
+
* document's order — the pose `poseSetup` computes. The editor's own
|
|
659
|
+
* header is of that pose too: on `examples/spineboy/export/spineboy-pro.json`
|
|
660
|
+
* (seven ik and seven transform constraints) `getBounds` with the
|
|
661
|
+
* constraints applied is within 0.0035 of the header the editor wrote, and
|
|
662
|
+
* with them stripped it is 0.31 off in `y` and `height`.
|
|
663
|
+
* - **Physics.none.** A physics constraint applies nothing at the setup pose
|
|
664
|
+
* (the oracle's setup reading). Under `Physics.reset` spine-core's box was
|
|
665
|
+
* the same, to the bit, on all nineteen tree rows (two declare a physics constraint).
|
|
666
|
+
* - **What is counted.** A region's four corners and a mesh's (a linked
|
|
667
|
+
* mesh's) world vertices — the rows of the oracle's `setup.attachments`
|
|
668
|
+
* block, in full doubles. A clipping polygon, a bounding box, a path and a
|
|
669
|
+
* point are not counted (measured: each placed 5000 units out moved
|
|
670
|
+
* `getBounds` by nothing), and the box is not clipped: `getBounds` called
|
|
671
|
+
* without its optional clipper.
|
|
672
|
+
* - **A slot on an inactive bone is not counted** — measured: a skin-required
|
|
673
|
+
* bone 1000 units out, which only a named skin activates, carrying a region
|
|
674
|
+
* left `getBounds` at the other region's 20 units with no skin set, and 1020
|
|
675
|
+
* with the bone not skin-required. A bone that is not skin-required under an
|
|
676
|
+
* inactive parent is active and unposed, and its slot is counted at the
|
|
677
|
+
* vertices that pose gives; the core poses the same vertices (the oracle's
|
|
678
|
+
* `setup.attachments`, held at tolerance 0), and a probe of that shape read
|
|
679
|
+
* the same box both ways.
|
|
680
|
+
* - **The box.** `x`, `y` are the least `x` and `y` over every counted
|
|
681
|
+
* vertex and `width`, `height` the greatest less the least — the
|
|
682
|
+
* bottom-left corner in Spine's y-up world, not the top-left. In full
|
|
683
|
+
* doubles; `build` writes each on the model's 1e-6 grid at float32
|
|
684
|
+
* (`headerBoxNumber` in `src/compile.ts`, whose comment says why).
|
|
685
|
+
* - **Nothing drawn is no box.** `getBounds` over no vertex returns an offset
|
|
686
|
+
* of `+Infinity` and a size of `-Infinity`, which is not a box; this
|
|
687
|
+
* returns `null` for it, and the emitter writes no box rather than a number
|
|
688
|
+
* nobody measured.
|
|
689
|
+
*
|
|
690
|
+
* Pure, like the rest of `src/core/`: it links nothing from the runtime, and
|
|
691
|
+
* `tools/core_gate.ts` holds it to `getBounds` at tolerance 0 over the corpus
|
|
692
|
+
* (its `BOUNDS` rows).
|
|
693
|
+
*/
|
|
694
|
+
|
|
695
|
+
/** The setup-pose box: its bottom-left corner and its extent, in Spine world units, in full doubles. */
|
|
696
|
+
export interface CoreBounds {
|
|
697
|
+
x: number;
|
|
698
|
+
y: number;
|
|
699
|
+
width: number;
|
|
700
|
+
height: number;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* The setup-pose bounding box of a model document (the header's rules), or
|
|
705
|
+
* `null` where nothing is drawn. Refused by name — `CoreInputError` — where
|
|
706
|
+
* the core leaves the setup attachments out, since a box over the rest would
|
|
707
|
+
* be a box of a different pose.
|
|
708
|
+
*/
|
|
709
|
+
export function setupBounds(doc: CompiledDocument, plant: CorePlant = {}): CoreBounds | null {
|
|
710
|
+
const view = underNoSkin(doc);
|
|
711
|
+
const posed = poseSetup(view, { ...plant, round: rawNumber });
|
|
712
|
+
const rows = posed.setup.attachments;
|
|
713
|
+
if (rows === null) {
|
|
714
|
+
const why = posed.absent.find(([block]) => block === 'setup.attachments')?.[1] ?? 'the setup attachments were not posed';
|
|
715
|
+
throw new CoreInputError(`the setup-pose bounding box cannot be computed: ${why}`);
|
|
716
|
+
}
|
|
717
|
+
const active = activeBones(view);
|
|
718
|
+
const boneOf = new Map(view.slots.map((s) => [s.name, s.bone]));
|
|
719
|
+
let minX = Infinity;
|
|
720
|
+
let minY = Infinity;
|
|
721
|
+
let maxX = -Infinity;
|
|
722
|
+
let maxY = -Infinity;
|
|
723
|
+
for (const [slot, attachment, , vertices] of rows) {
|
|
724
|
+
const bone = boneOf.get(slot);
|
|
725
|
+
if (bone === undefined || !active.has(bone)) continue;
|
|
726
|
+
for (let i = 0; i + 1 < vertices.length; i += 2) {
|
|
727
|
+
const x = vertices[i];
|
|
728
|
+
const y = vertices[i + 1];
|
|
729
|
+
if (x === null || y === null) throw new CoreInputError(`the setup-pose bounding box cannot be computed: slot "${slot}" attachment "${attachment}" vertex ${i / 2} is not finite`);
|
|
730
|
+
minX = Math.min(minX, x);
|
|
731
|
+
minY = Math.min(minY, y);
|
|
732
|
+
maxX = Math.max(maxX, x);
|
|
733
|
+
maxY = Math.max(maxY, y);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (minX === Infinity) return null;
|
|
737
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
738
|
+
}
|
|
739
|
+
// --- #907 the setup-pose bounding box: end ---
|