rig-c 0.0.0-stage → 2.20.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +13 -0
- package/LICENSE +30 -0
- package/NOTICE.md +145 -0
- package/README.md +817 -3
- package/bin/rigc.cjs +83 -0
- package/cli.ts +61 -0
- package/cli_core.ts +46 -0
- package/docs/AUTHORING.md +9923 -0
- package/docs/FACE.md +1948 -0
- package/docs/INGEST.md +1488 -0
- package/docs/MOTION.md +1241 -0
- package/docs/PROMPTING.md +109 -0
- package/docs/RIGGING.md +1441 -0
- package/docs/SPEC_COVERAGE.md +357 -0
- package/package.json +108 -4
- package/skills/rigc/SKILL.md +133 -0
- package/skills/rigc-face/SKILL.md +60 -0
- package/skills/rigc-ingest/SKILL.md +78 -0
- package/skills/rigc-motion/SKILL.md +51 -0
- package/skills/rigc-rigging/SKILL.md +49 -0
- package/src/areaband.ts +159 -0
- package/src/assertions/bodies/a01.ts +23 -0
- package/src/assertions/bodies/a02.ts +21 -0
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a04.ts +40 -0
- package/src/assertions/bodies/a05.ts +56 -0
- package/src/assertions/bodies/a06.ts +245 -0
- package/src/assertions/bodies/a07.ts +68 -0
- package/src/assertions/bodies/a08.ts +76 -0
- package/src/assertions/bodies/a09.ts +82 -0
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a12.ts +30 -0
- package/src/assertions/bodies/a13.ts +51 -0
- package/src/assertions/bodies/a14.ts +35 -0
- package/src/assertions/bodies/a15.ts +97 -0
- package/src/assertions/bodies/a16.ts +24 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a18.ts +62 -0
- package/src/assertions/bodies/a19.ts +404 -0
- package/src/assertions/bodies/a20.ts +122 -0
- package/src/assertions/bodies/a21.ts +190 -0
- package/src/assertions/bodies/a22.ts +39 -0
- package/src/assertions/bodies/a23.ts +305 -0
- package/src/assertions/bodies/a24.ts +68 -0
- package/src/assertions/bodies/a25.ts +39 -0
- package/src/assertions/bodies/a26.ts +61 -0
- package/src/assertions/bodies/a27.ts +33 -0
- package/src/assertions/bodies/a28.ts +70 -0
- package/src/assertions/bodies/a29.ts +34 -0
- package/src/assertions/bodies/a30.ts +50 -0
- package/src/assertions/bodies/a31.ts +61 -0
- package/src/assertions/bodies/a32.ts +44 -0
- package/src/assertions/bodies/a33.ts +110 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a35.ts +160 -0
- package/src/assertions/bodies/a36.ts +81 -0
- package/src/assertions/bodies/a37.ts +77 -0
- package/src/assertions/bodies/a38.ts +73 -0
- package/src/assertions/bodies/a39.ts +303 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/bodies/a42.ts +97 -0
- package/src/assertions/bodies/a43.ts +181 -0
- package/src/assertions/bodies/a44.ts +23 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/bodies/a46.ts +224 -0
- package/src/assertions/bodies/a47.ts +126 -0
- package/src/assertions/bodies/a48.ts +83 -0
- package/src/assertions/bodies/a49.ts +81 -0
- package/src/assertions/bodies/a50.ts +97 -0
- package/src/assertions/constraint_words.ts +169 -0
- package/src/assertions/emitted/index.ts +148 -0
- package/src/assertions/facts/animated_bones.ts +30 -0
- package/src/assertions/facts/animation_durations.ts +37 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/atlas_regions.ts +52 -0
- package/src/assertions/facts/bone_timelines.ts +37 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/constraints.ts +155 -0
- package/src/assertions/facts/deform_survey.ts +27 -0
- package/src/assertions/facts/event_keys.ts +55 -0
- package/src/assertions/facts/linked_meshes.ts +38 -0
- package/src/assertions/facts/mesh_attachments.ts +100 -0
- package/src/assertions/facts/region_joins.ts +34 -0
- package/src/assertions/facts/sequences.ts +85 -0
- package/src/assertions/facts/skeleton_roster.ts +45 -0
- package/src/assertions/facts/skin_entries.ts +37 -0
- package/src/assertions/facts/skin_members.ts +53 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/facts/stage.ts +27 -0
- package/src/assertions/facts/stage_box.ts +65 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/facts/two_colour.ts +52 -0
- package/src/assertions/facts/vertex_polygons.ts +53 -0
- package/src/assertions/footprints.ts +367 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/inward_advance.ts +58 -0
- package/src/assertions/kinds.ts +105 -0
- package/src/assertions/mesh_kinds.ts +56 -0
- package/src/assertions/model/animated_bones.ts +38 -0
- package/src/assertions/model/animation_durations.ts +57 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/atlas_regions.ts +76 -0
- package/src/assertions/model/bone_timelines.ts +58 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/constraints.ts +233 -0
- package/src/assertions/model/declared.ts +125 -0
- package/src/assertions/model/deform_survey.ts +24 -0
- package/src/assertions/model/event_keys.ts +45 -0
- package/src/assertions/model/given.ts +45 -0
- package/src/assertions/model/index.ts +398 -0
- package/src/assertions/model/linked_meshes.ts +24 -0
- package/src/assertions/model/mesh_attachments.ts +119 -0
- package/src/assertions/model/parse.ts +146 -0
- package/src/assertions/model/region_joins.ts +67 -0
- package/src/assertions/model/runtime_timelines.ts +78 -0
- package/src/assertions/model/sequences.ts +157 -0
- package/src/assertions/model/skeleton_roster.ts +23 -0
- package/src/assertions/model/skin_entries.ts +69 -0
- package/src/assertions/model/skin_members.ts +64 -0
- package/src/assertions/model/slider_composition.ts +193 -0
- package/src/assertions/model/slot_colour.ts +81 -0
- package/src/assertions/model/stage.ts +28 -0
- package/src/assertions/model/stage_box.ts +51 -0
- package/src/assertions/model/stepped_poses.ts +105 -0
- package/src/assertions/model/two_colour.ts +61 -0
- package/src/assertions/model/vertex_polygons.ts +72 -0
- package/src/assertions/reasons.ts +129 -0
- package/src/assertions/region_lookups.ts +61 -0
- package/src/assertions/report.ts +189 -0
- package/src/assertions/values.ts +39 -0
- package/src/atlas.ts +2870 -0
- package/src/ballot.ts +866 -0
- package/src/bonedist.ts +643 -0
- package/src/chainfit.ts +2752 -0
- package/src/chains.ts +170 -0
- package/src/check.ts +4303 -0
- package/src/checkpics.ts +295 -0
- package/src/cli/core_commands.ts +1627 -0
- package/src/cli/repack.ts +414 -0
- package/src/cli/shared.ts +2776 -0
- package/src/cli/spine_commands.ts +820 -0
- package/src/compile.ts +9414 -0
- package/src/core/additive.ts +458 -0
- package/src/core/animation.ts +1050 -0
- package/src/core/clipping.ts +696 -0
- package/src/core/constraints.ts +1876 -0
- package/src/core/constraints_path.ts +964 -0
- package/src/core/constraints_physics.ts +881 -0
- package/src/core/constraints_slider.ts +635 -0
- package/src/core/deform.ts +613 -0
- package/src/core/draw_order.ts +125 -0
- package/src/core/events.ts +135 -0
- package/src/core/hooks.ts +249 -0
- package/src/core/index.ts +1400 -0
- package/src/core/raw.ts +739 -0
- package/src/core/skins.ts +129 -0
- package/src/core/uvs.ts +469 -0
- package/src/core/vertices.ts +490 -0
- package/src/core/walk.ts +197 -0
- package/src/core/world.ts +289 -0
- package/src/correspondence.ts +15 -0
- package/src/deformbuild.ts +60 -0
- package/src/deformgen.ts +630 -0
- package/src/deformmeasure.ts +732 -0
- package/src/deformreport.ts +373 -0
- package/src/deformstructure.ts +386 -0
- package/src/deformsurvey.ts +2162 -0
- package/src/depth.ts +784 -0
- package/src/diff.ts +2252 -0
- package/src/emit.ts +134 -0
- package/src/emit_spine.ts +854 -0
- package/src/errors.ts +53 -0
- package/src/framing.ts +819 -0
- package/src/generation.ts +139 -0
- package/src/ingest.ts +2293 -0
- package/src/json-position.ts +253 -0
- package/src/keyorder.ts +587 -0
- package/src/keys.ts +486 -0
- package/src/ladder.ts +121 -0
- package/src/mesh.ts +2382 -0
- package/src/meshcompare.ts +1188 -0
- package/src/meshquality.ts +2042 -0
- package/src/meshrasters.ts +944 -0
- package/src/meshreduce.ts +1425 -0
- package/src/model.ts +1245 -0
- package/src/motion.ts +809 -0
- package/src/nonfinite.ts +54 -0
- package/src/package_meta.ts +48 -0
- package/src/png.ts +297 -0
- package/src/pose.ts +2324 -0
- package/src/preview.ts +434 -0
- package/src/region_joins.ts +54 -0
- package/src/render.ts +1013 -0
- package/src/render_core.ts +871 -0
- package/src/render_shared.ts +2958 -0
- package/src/repack.ts +495 -0
- package/src/rig.ts +2941 -0
- package/src/slots.ts +892 -0
- package/src/spine_side.ts +138 -0
- package/src/timelines.ts +837 -0
- package/src/trackgen.ts +364 -0
- package/src/transform.ts +310 -0
- package/src/types.ts +1797 -0
- package/src/validate.ts +3875 -0
- package/tools/contact.ts +126 -0
- package/tools/editor_roundtrip.ts +1641 -0
- package/tools/font5x7.ts +101 -0
- package/tools/measure_contact_depth.ts +105 -0
- package/tools/plate.ts +508 -0
- package/tools/png_probe.mjs +72 -0
|
@@ -0,0 +1,2162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deform survey, with nothing of the runtime in it (issue #1025, cut 4c-3
|
|
3
|
+
* of step 4c of #380): every measurement `src/deformmeasure.ts`'s header
|
|
4
|
+
* describes, the interface its two posers stand behind, the core's poser, and
|
|
5
|
+
* the survey off a model document alone (`surveyOfModel`).
|
|
6
|
+
*
|
|
7
|
+
* ⭐ **Why the survey is its own module.** `A39_DEFORM_KEEPS_TRIANGLE_WINDING`
|
|
8
|
+
* is written once, against the survey as a fact (`src/assertions/bodies/a39.ts`),
|
|
9
|
+
* and its model-side supplier has to reach the survey without reaching
|
|
10
|
+
* spine-core — the condition under which the model side can load at all, held
|
|
11
|
+
* by the selftest's `VF01` over every module `src/assertions/` reaches by value.
|
|
12
|
+
* The survey was always two halves behind one seam (#969): the arithmetic and
|
|
13
|
+
* the interface, which name no runtime class, and spine-core's reader and
|
|
14
|
+
* poser, which are nothing else. This file is the first half, moved out of
|
|
15
|
+
* `src/deformmeasure.ts` unchanged — every statement byte for byte, the ones a
|
|
16
|
+
* caller there reads now exported, and the two comment lines that said where
|
|
17
|
+
* spine-core's half is now saying it — and that file keeps the second half and
|
|
18
|
+
* re-exports every name it exported before, so no caller changed an import.
|
|
19
|
+
*
|
|
20
|
+
* What a survey measures and why is `src/deformmeasure.ts`'s header; it is not
|
|
21
|
+
* restated here.
|
|
22
|
+
*
|
|
23
|
+
* Links nothing from the runtime, directly or through what it imports.
|
|
24
|
+
*/
|
|
25
|
+
import { CoreInputError, underSkin, type CompiledDocument } from './core/index.ts';
|
|
26
|
+
import { float32Rows, meshWorld, poseDial as corePoseDial, poseJump, recordIdentity, sliderRecordOf, slotDraw, type CoreSurveyPose } from './core/hooks.ts';
|
|
27
|
+
import { modelStructure, type SurveyAnimation, type SurveyDeformTimeline, type SurveyMesh, type SurveySkin, type SurveySlider, type SurveyStructure } from './deformstructure.ts';
|
|
28
|
+
import { areaBand, DEFORM_AREA_EPSILON, float32AreaNoise, stretchSingularValues, triangleAreas } from './areaband.ts';
|
|
29
|
+
|
|
30
|
+
// The area band lives in `src/areaband.ts` (moved unchanged, issue #1224) so the
|
|
31
|
+
// geometry entry can read it without reaching the compiler, and
|
|
32
|
+
// `stretchSingularValues` joined it (moved unchanged, issue #1230) so the motion
|
|
33
|
+
// comparison can too; every name this module exported before is exported from
|
|
34
|
+
// here still.
|
|
35
|
+
export { DEFORM_AREA_EPSILON, float32AreaNoise, stretchSingularValues, triangleAreas };
|
|
36
|
+
|
|
37
|
+
/** A quantity's worst triangle on one key, and which triangle it was. */
|
|
38
|
+
export interface DeformExtreme {
|
|
39
|
+
triangle: number;
|
|
40
|
+
value: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One reversed triangle, with everything A39's message names about it. */
|
|
44
|
+
export interface DeformReversal {
|
|
45
|
+
triangle: number;
|
|
46
|
+
ids: [number, number, number];
|
|
47
|
+
before: number;
|
|
48
|
+
after: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What the slot is doing with this mesh at one key's own time (issue #401).
|
|
53
|
+
*
|
|
54
|
+
* ⭐ Read off the SAME posed skeleton the geometry came from, one key at a time,
|
|
55
|
+
* so the two halves cannot disagree about which frame they describe.
|
|
56
|
+
*/
|
|
57
|
+
export interface DeformKeyDraw {
|
|
58
|
+
/** The attachment the slot shows at that time, or `null` when it shows none. */
|
|
59
|
+
shown: string | null;
|
|
60
|
+
/**
|
|
61
|
+
* Whether `shown` IS this timeline's mesh — compared the way spine-core
|
|
62
|
+
* compares it (`Attachment.timelineAttachment`), so a linked mesh counts as
|
|
63
|
+
* the mesh it links to. When this is false the runtime applies no deform to
|
|
64
|
+
* this slot at all (`DeformTimeline.applyToSlot` returns early), which is why
|
|
65
|
+
* the geometry below is the cleared pose and says nothing about the key.
|
|
66
|
+
*/
|
|
67
|
+
showsThisMesh: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* `slot.color.a × attachment.color.a` at that time — the product
|
|
70
|
+
* [`src/render.ts`](src/render.ts) tints a piece with, which is what decides
|
|
71
|
+
* whether a texel lands. 0 when the slot shows something else, because then
|
|
72
|
+
* none of this mesh is drawn.
|
|
73
|
+
*/
|
|
74
|
+
alpha: number;
|
|
75
|
+
/**
|
|
76
|
+
* The skin the skeleton was **wearing** when all of the above was read, off
|
|
77
|
+
* `Skeleton.skin` itself — the skin that holds this timeline's attachment
|
|
78
|
+
* (issue #583) — or `null` when it wore none.
|
|
79
|
+
*
|
|
80
|
+
* ⭐ Read off the posed skeleton rather than passed in beside it, so it cannot
|
|
81
|
+
* disagree with what was actually set. It is what makes `blank` a statement
|
|
82
|
+
* about a pose instead of a verdict on the rig: "the slot shows nothing" and
|
|
83
|
+
* "the slot shows nothing in the dress this mesh lives in" are different
|
|
84
|
+
* claims, and only the second is measurable.
|
|
85
|
+
*/
|
|
86
|
+
under: string | null;
|
|
87
|
+
/**
|
|
88
|
+
* Why this mesh puts no pixels on the screen at that time, in the words the
|
|
89
|
+
* `DEFORM` block and A39's stats line both print — or `null` when it puts some
|
|
90
|
+
* there and every figure below is gated normally.
|
|
91
|
+
*
|
|
92
|
+
* ⚠️ The bar is **exactly** 0 and nothing above it. At alpha 0.5 a reversed
|
|
93
|
+
* triangle is plainly visible at half strength, and a floor above 0 would be
|
|
94
|
+
* this repository picking a visibility policy, which is the thing an archetype
|
|
95
|
+
* rule exists not to do.
|
|
96
|
+
*/
|
|
97
|
+
blank: string | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* How an animation is reached, which is the frame its keys are posed in (#407).
|
|
102
|
+
*
|
|
103
|
+
* ⭐ One per way in. An animation nothing applies has exactly one — the track —
|
|
104
|
+
* and an animation two sliders apply has two, both measured, because a fold only
|
|
105
|
+
* one dial can reach is still a fold.
|
|
106
|
+
*/
|
|
107
|
+
export interface DeformReach {
|
|
108
|
+
/** `track` — played on track 0; `slider` — applied by the named constraint. */
|
|
109
|
+
kind: 'track' | 'slider';
|
|
110
|
+
/** The slider that applies it, or `null` on the track. */
|
|
111
|
+
slider: string | null;
|
|
112
|
+
/** The slider's driving bone. `null` on the track, and on a bone-less slider. */
|
|
113
|
+
bone: string | null;
|
|
114
|
+
/**
|
|
115
|
+
* The transform property the slider reads off that bone, as a rig spec spells
|
|
116
|
+
* it (`rotate`, `x`, `y`, `scaleX`, …), or `null` when there is no bone.
|
|
117
|
+
*
|
|
118
|
+
* 🚨 Off the **artifact** since issue #419 — spine-core's own `FromProperty`
|
|
119
|
+
* subclass, which is what the parser built out of the rig spec's `property`
|
|
120
|
+
* field. It used to be whichever local field the probe below found first, and
|
|
121
|
+
* float noise makes `rotation` move a world `sqrt(a² + c²)` reading, so every
|
|
122
|
+
* world `scaleX` / `scaleY` / `shearY` slider was reported as `rotate`.
|
|
123
|
+
*/
|
|
124
|
+
property: string | null;
|
|
125
|
+
/**
|
|
126
|
+
* The bone field the solve actually drives, when that is **not** the one
|
|
127
|
+
* `property` names; `null` when they are the same, which is every rig that has
|
|
128
|
+
* a plain parent.
|
|
129
|
+
*
|
|
130
|
+
* They come apart under `local: false`, where the reader goes through the world
|
|
131
|
+
* transform: a `FromX` slider on a bone whose parent is at 90° reads a world x
|
|
132
|
+
* that local `x` does not move at all and local `y` moves entirely.
|
|
133
|
+
*/
|
|
134
|
+
drive: string | null;
|
|
135
|
+
/** `SliderData.local` — whether the property is read local or world. */
|
|
136
|
+
local: boolean;
|
|
137
|
+
/** The clause the `DEFORM` block and A39's stats line print. */
|
|
138
|
+
label: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The span of one animation's own `0..duration` a dial can select, in seconds. */
|
|
142
|
+
export interface DialSpan {
|
|
143
|
+
lo: number;
|
|
144
|
+
hi: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A dial whose probe **tied** and whose artifact broke the tie (issue #419).
|
|
149
|
+
*
|
|
150
|
+
* 🔒 **Not a disagreement, and it must never be reported as one.** There is one
|
|
151
|
+
* belief here, not two: the probe measured two fields moving the reading by the
|
|
152
|
+
* same amount, named neither, and the skeleton's own reader said which of them
|
|
153
|
+
* the author wrote. A `FromX` slider under `local: false` on a bone whose parent
|
|
154
|
+
* is at 45° is exactly that, and it is legitimate geometry. So there is no second
|
|
155
|
+
* answer, no second reach and nothing to compare — which is why this is a type of
|
|
156
|
+
* its own and not a `verdict` field on the one below.
|
|
157
|
+
*/
|
|
158
|
+
export interface DeformDialTie {
|
|
159
|
+
/** The slider whose dial this is. */
|
|
160
|
+
slider: string;
|
|
161
|
+
/** Its driving bone. */
|
|
162
|
+
bone: string;
|
|
163
|
+
/** The field the artifact named, which is therefore the one the survey drives. */
|
|
164
|
+
drive: string;
|
|
165
|
+
/** What one step of it moved the reading by. */
|
|
166
|
+
driveResponse: number;
|
|
167
|
+
/** The other fields inside `DIAL_PROBE_MARGIN` of it, and what each moved it by. */
|
|
168
|
+
rivals: Array<{ field: string; response: number }>;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A dial the artifact and the probe name **differently**, with the span each of
|
|
173
|
+
* their answers can select (issues #419, #427).
|
|
174
|
+
*
|
|
175
|
+
* ⭐ Structured, and on the survey rather than only inside `DeformReach.label`,
|
|
176
|
+
* because that label is prose only `explain` prints. A `build`-only run is the
|
|
177
|
+
* loop an agent that cannot see the rig actually runs, and until this existed the
|
|
178
|
+
* fact that rigc's two halves disagreed about which dial was turned reached that
|
|
179
|
+
* run not at all.
|
|
180
|
+
*
|
|
181
|
+
* 🚨 **What the two reaches settle.** The property's name is not what makes this
|
|
182
|
+
* survey trustworthy — the set of frames it poses is. So both answers are turned
|
|
183
|
+
* into the span of the animation each can select, and `outside` is the part of
|
|
184
|
+
* what was posed that the artifact's answer could not have reached. Measured
|
|
185
|
+
* (issue #427): under a disagreement `driveReach` **always contains**
|
|
186
|
+
* `statedReach`, because the driven field is the largest response the probe found
|
|
187
|
+
* and a disagreement needs it to clear the artifact's field by
|
|
188
|
+
* `DIAL_PROBE_MARGIN`. So the survey never poses fewer frames than the artifact's
|
|
189
|
+
* answer would have, and `outside` is what the artifact's answer would have
|
|
190
|
+
* MISSED — never what this one invented.
|
|
191
|
+
*/
|
|
192
|
+
export interface DeformDialDispute {
|
|
193
|
+
/** The slider whose dial this is. */
|
|
194
|
+
slider: string;
|
|
195
|
+
/** Its driving bone. */
|
|
196
|
+
bone: string;
|
|
197
|
+
/**
|
|
198
|
+
* The field the artifact names, rig-spec spelled — or the reader's class name
|
|
199
|
+
* when it is one this file does not know.
|
|
200
|
+
*/
|
|
201
|
+
stated: string;
|
|
202
|
+
/** What one step of the artifact's field moves the reading by. */
|
|
203
|
+
statedResponse: number | null;
|
|
204
|
+
/**
|
|
205
|
+
* The part of the animation's own `0..duration` the artifact's field can
|
|
206
|
+
* select, or `null` when it can select none of it — and on the one reader
|
|
207
|
+
* this file cannot name, where there is no field to probe.
|
|
208
|
+
*/
|
|
209
|
+
statedReach: DialSpan | null;
|
|
210
|
+
/** The field the survey drives, which is the one that measurably moves the reading. */
|
|
211
|
+
drive: string;
|
|
212
|
+
/** What one step of THAT moves the reading by. */
|
|
213
|
+
driveResponse: number;
|
|
214
|
+
/** The part of `0..duration` the drive can select, or `null` when it can select none. */
|
|
215
|
+
driveReach: DialSpan | null;
|
|
216
|
+
/**
|
|
217
|
+
* The deform key times this survey posed through `drive` that no settable value
|
|
218
|
+
* of the artifact's field reaches, in ascending order.
|
|
219
|
+
*
|
|
220
|
+
* ⭐ Empty is a **reading**, not an absence: it says both answers pose the same
|
|
221
|
+
* frames, so the disagreement changed nothing about what was measured. A39
|
|
222
|
+
* prints `outside:none` for it rather than omitting the field, because a
|
|
223
|
+
* comparison that was made and came out equal must not look like one nobody
|
|
224
|
+
* made.
|
|
225
|
+
*/
|
|
226
|
+
outside: number[];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The reach every animation has when no slider applies it. */
|
|
230
|
+
const TRACK_REACH: DeformReach = {
|
|
231
|
+
kind: 'track',
|
|
232
|
+
slider: null,
|
|
233
|
+
bone: null,
|
|
234
|
+
property: null,
|
|
235
|
+
drive: null,
|
|
236
|
+
local: false,
|
|
237
|
+
label: 'played on a track',
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* What the slider's dial had to be set to for this key's time to be the one the
|
|
242
|
+
* runtime applies — and whether it worked (issue #407).
|
|
243
|
+
*
|
|
244
|
+
* `null` on a track frame, where there is no dial and the time is the time.
|
|
245
|
+
*/
|
|
246
|
+
export interface DeformDial {
|
|
247
|
+
/**
|
|
248
|
+
* The property value the mapping inversion asks for.
|
|
249
|
+
*
|
|
250
|
+
* `Slider.update` computes `time = offset + (value − property.offset) · scale`
|
|
251
|
+
* — `to`, `from` and `scale` in a rig spec — so this is that line solved for
|
|
252
|
+
* `value`: `property.offset + (time − offset) / scale`. On a bone-less slider
|
|
253
|
+
* the "value" IS the time and this is the time.
|
|
254
|
+
*/
|
|
255
|
+
value: number;
|
|
256
|
+
/**
|
|
257
|
+
* What the driving bone's own LOCAL field was set to so that spine-core's
|
|
258
|
+
* reader returns `value`. The same number under `local: true`, where the
|
|
259
|
+
* reader is `source.rotation` and nothing intervenes; a different one under
|
|
260
|
+
* `local: false`, where the reader goes through the world transform.
|
|
261
|
+
*/
|
|
262
|
+
driven: number;
|
|
263
|
+
/**
|
|
264
|
+
* The drive the mapping asked for when it was past `DIAL_DRIVE_LIMIT` and the
|
|
265
|
+
* bone was therefore stopped at the bound instead — `null` on every dial that
|
|
266
|
+
* stayed inside it, which is every correct rig (issue #419).
|
|
267
|
+
*
|
|
268
|
+
* 🚨 The runaway made visible. Before #419 a world scale slider was solved
|
|
269
|
+
* through `rotation`, whose response to it is float noise, and the drive ran to
|
|
270
|
+
* 4.9e9 with nothing saying so. Now the ask is carried here, the bone is not
|
|
271
|
+
* posed there, and the key is reported unreachable naming both numbers.
|
|
272
|
+
*/
|
|
273
|
+
beyondLimit: number | null;
|
|
274
|
+
/** `SliderPose.time` the runtime then computed, read off the posed skeleton. */
|
|
275
|
+
applied: number;
|
|
276
|
+
/** What `Slider.update` would have stored for this key's own time. */
|
|
277
|
+
wanted: number;
|
|
278
|
+
/**
|
|
279
|
+
* `applied` is not `wanted`: **no dial value selects this key's time.**
|
|
280
|
+
*
|
|
281
|
+
* The reachable one, and it is not hypothetical: `FromRotate.value` ends
|
|
282
|
+
* `if (value < 0) value += 360` over an `atan2`, so `[0, 360)` is the whole of
|
|
283
|
+
* what a `rotate` slider reading a WORLD rotation can be driven to, and
|
|
284
|
+
* everything the inversion asks for outside it arrives 360° away — below 0°
|
|
285
|
+
* (issue #405) and at or above 360° (issue #417) alike. `Math.max(0, time)` is
|
|
286
|
+
* the other.
|
|
287
|
+
*
|
|
288
|
+
* ⚠️ A key like that is measured — at the frame the runtime does land on, which
|
|
289
|
+
* the report names — and then left OUT of the gate's counts, because the
|
|
290
|
+
* geometry there belongs to some other time. Never silent: A39 puts it on the
|
|
291
|
+
* stats line and the `DEFORM` block gives it a line of its own.
|
|
292
|
+
*/
|
|
293
|
+
unreachable: boolean;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* What one deform timeline is doing to one attachment's geometry at **one posed
|
|
298
|
+
* time**, whatever that time is.
|
|
299
|
+
*
|
|
300
|
+
* ⭐ A key and a between-keys probe (issue #403) are the same measurement taken
|
|
301
|
+
* at two kinds of time, so they are one interface and one function
|
|
302
|
+
* (`measurePosed`). Giving the span scan its own arithmetic would be the second
|
|
303
|
+
* derivation this file's opening paragraph exists to forbid — the count A39
|
|
304
|
+
* refuses on between two keys has to be the count it refuses on at one.
|
|
305
|
+
*/
|
|
306
|
+
export interface DeformFrameMeasure {
|
|
307
|
+
/** Vertices in the attachment. */
|
|
308
|
+
vertices: number;
|
|
309
|
+
/**
|
|
310
|
+
* How many of them this key moves at all, and the largest world displacement.
|
|
311
|
+
*
|
|
312
|
+
* `moved` counts an exact difference rather than one past an epsilon, and can:
|
|
313
|
+
* both sides come off the same `computeWorldVertices` call with the same bones,
|
|
314
|
+
* so a vertex the key does not touch is bit-identical on the two.
|
|
315
|
+
*/
|
|
316
|
+
moved: number;
|
|
317
|
+
maxDisplacement: number;
|
|
318
|
+
maxDisplacementVertex: number;
|
|
319
|
+
/** Triangles compared. `reversed` and `collapsed` are A39's own two counts. */
|
|
320
|
+
triangles: number;
|
|
321
|
+
reversed: DeformReversal[];
|
|
322
|
+
collapsed: number;
|
|
323
|
+
/** Triangles with no area at the cleared pose: no winding, no ratio, no stretch. */
|
|
324
|
+
degenerate: number;
|
|
325
|
+
/**
|
|
326
|
+
* `after / before`, signed. **A negative ratio IS a reversal** — the sign is
|
|
327
|
+
* the same fact `reversed` counts, which is why the two can never disagree.
|
|
328
|
+
*/
|
|
329
|
+
areaRatioMin: DeformExtreme | null;
|
|
330
|
+
areaRatioMax: DeformExtreme | null;
|
|
331
|
+
/** Worst stretch and worst squash, over the triangles that have a map. */
|
|
332
|
+
stretchMax: DeformExtreme | null;
|
|
333
|
+
stretchMin: DeformExtreme | null;
|
|
334
|
+
/** The dead band every count above was taken against, in px². */
|
|
335
|
+
band: number;
|
|
336
|
+
/** What the slot draws of this mesh at this time (issue #401). */
|
|
337
|
+
draw: DeformKeyDraw;
|
|
338
|
+
/** How the animation was reached, which is the frame this was posed in (#407). */
|
|
339
|
+
reach: DeformReach;
|
|
340
|
+
/** The dial that selected this time, or `null` on a track frame (#407). */
|
|
341
|
+
dial: DeformDial | null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** What one deform key does to one attachment's geometry. */
|
|
345
|
+
export interface DeformKeyMeasure extends DeformFrameMeasure {
|
|
346
|
+
animation: string;
|
|
347
|
+
skin: string;
|
|
348
|
+
slot: string;
|
|
349
|
+
/** The attachment the timeline resolved to — the name A39's message carries. */
|
|
350
|
+
attachment: string;
|
|
351
|
+
/** The placeholder it sits behind in `skin`, which is what the spec wrote. */
|
|
352
|
+
placeholder: string;
|
|
353
|
+
/** Index into the timeline's own frames, which is the index A39's message names. */
|
|
354
|
+
key: number;
|
|
355
|
+
time: number;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* How the runtime gets from one deform key's geometry to the next one's.
|
|
360
|
+
*
|
|
361
|
+
* The three the format has, and what each does to the span scan below:
|
|
362
|
+
*
|
|
363
|
+
* - `linear` — the interpolation fraction sweeps `[0, 1]` affinely in time, so a
|
|
364
|
+
* fraction the closed form names converts to a time exactly;
|
|
365
|
+
* - `stepped` — the fraction is **0 for the whole span**: the runtime does not
|
|
366
|
+
* interpolate, it holds the earlier key's geometry. So the span introduces no
|
|
367
|
+
* geometry the key survey has not already measured — but it holds that
|
|
368
|
+
* geometry across times whose *alpha* differs from the key's, which is the
|
|
369
|
+
* same hole in another coat and is checked (`DW15`);
|
|
370
|
+
* - `bezier` — the fraction is the stored curve, which spine-core evaluates as a
|
|
371
|
+
* **polyline** of ten points (`DeformTimeline.getCurvePercent`). The scan reads
|
|
372
|
+
* that polyline rather than the cubic, so it is exact about the thing the
|
|
373
|
+
* runtime actually does, overshoot past 0 or 1 included.
|
|
374
|
+
*/
|
|
375
|
+
export type DeformSpanCurve = 'linear' | 'stepped' | 'bezier';
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The interval between two consecutive deform keys, scanned (issue #403).
|
|
379
|
+
*
|
|
380
|
+
* ⚠️ A span is recorded whether or not anything was found, because "the scan ran
|
|
381
|
+
* and said nothing" and "the scan did not run" are the two things a gate must
|
|
382
|
+
* never print the same way.
|
|
383
|
+
*/
|
|
384
|
+
export interface DeformSpan {
|
|
385
|
+
animation: string;
|
|
386
|
+
skin: string;
|
|
387
|
+
slot: string;
|
|
388
|
+
attachment: string;
|
|
389
|
+
placeholder: string;
|
|
390
|
+
/** The frame its two keys were posed in, which is the frame it probes (#407). */
|
|
391
|
+
reach: DeformReach;
|
|
392
|
+
/** The two keys it lies between, by the index A39's own message uses. */
|
|
393
|
+
fromKey: number;
|
|
394
|
+
toKey: number;
|
|
395
|
+
fromTime: number;
|
|
396
|
+
toTime: number;
|
|
397
|
+
curve: DeformSpanCurve;
|
|
398
|
+
/** Triangles the closed form says reverse somewhere strictly inside the span. */
|
|
399
|
+
predicted: number;
|
|
400
|
+
/**
|
|
401
|
+
* Times the closed form named and the scan then measured at, in probe order.
|
|
402
|
+
* **Empty is the ordinary case** — a span nothing is predicted to fold in
|
|
403
|
+
* costs no posed measurement at all.
|
|
404
|
+
*/
|
|
405
|
+
probed: number[];
|
|
406
|
+
/** The probe that found a reversal on a frame the mesh DRAWS, or `null`. */
|
|
407
|
+
fold: { time: number; percent: number; measure: DeformFrameMeasure } | null;
|
|
408
|
+
/**
|
|
409
|
+
* Probes that found the predicted reversal at a time the mesh draws no pixels
|
|
410
|
+
* — issue #401's exemption, applied at a time no key lands on, which is what
|
|
411
|
+
* keeps this from refusing a rig that fades out *before* the fold.
|
|
412
|
+
*/
|
|
413
|
+
notDrawn: number;
|
|
414
|
+
/**
|
|
415
|
+
* The closed form predicted a fold and no probe reproduced one.
|
|
416
|
+
*
|
|
417
|
+
* Not a failure — refusing on a prediction nothing measured would be the false
|
|
418
|
+
* red this repository has paid for twice (issues #44, #262) — and not a
|
|
419
|
+
* silence either: it is on A39's stats line, because the one case that reaches
|
|
420
|
+
* it is a weighted mesh whose BONES move across the span, and that is a limit
|
|
421
|
+
* a reader has to be able to see.
|
|
422
|
+
*/
|
|
423
|
+
unconfirmed: boolean;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Every deform key in a skeleton, measured — and what was passed over. */
|
|
427
|
+
export interface DeformSurvey {
|
|
428
|
+
keys: DeformKeyMeasure[];
|
|
429
|
+
/** Deform timelines seen, whether or not any of them could be measured. */
|
|
430
|
+
timelines: number;
|
|
431
|
+
/** Slots the caller's `exempt` set held, quoted. */
|
|
432
|
+
exempted: string[];
|
|
433
|
+
/** Slots whose attachment has a vertex array and no triangles, quoted. */
|
|
434
|
+
notAMesh: string[];
|
|
435
|
+
/**
|
|
436
|
+
* Triangle samples across every key that draws — one mesh's triangles counted
|
|
437
|
+
* once per key. ⚠️ A key whose `draw.blank` is set is NOT in this figure: it
|
|
438
|
+
* is the sample the gate ran, and the gate does not run on those.
|
|
439
|
+
*/
|
|
440
|
+
trianglesMeasured: number;
|
|
441
|
+
/** Triangles pinched onto zero, summed over the same keys. A real idiom, never a bar. */
|
|
442
|
+
collapsed: number;
|
|
443
|
+
/**
|
|
444
|
+
* Keys in `keys` whose `draw.blank` is set — measured, then passed over
|
|
445
|
+
* because the mesh draws no pixels at that time (issue #401).
|
|
446
|
+
*
|
|
447
|
+
* ⚠️ Carried rather than left implicit because an exemption nobody can see is
|
|
448
|
+
* how a gate comes to look kept while checking nothing. Both consumers print
|
|
449
|
+
* it: A39 on its stats line, the `DEFORM` block per key and in its rollup.
|
|
450
|
+
*/
|
|
451
|
+
notDrawn: number;
|
|
452
|
+
/** Reversed triangles found on those keys, which nothing gates. */
|
|
453
|
+
notDrawnReversed: number;
|
|
454
|
+
/**
|
|
455
|
+
* Keys whose `dial.unreachable` is set — no value of the slider's driving bone
|
|
456
|
+
* selects that key's time, so the frame posed is not the key's (issue #407).
|
|
457
|
+
*
|
|
458
|
+
* ⚠️ Out of `trianglesMeasured` for the same reason `notDrawn` is: those totals
|
|
459
|
+
* are what the gate ran on, and the gate does not run on these.
|
|
460
|
+
*/
|
|
461
|
+
notReachable: number;
|
|
462
|
+
/** Reversed triangles found on those keys, which nothing gates. */
|
|
463
|
+
notReachableReversed: number;
|
|
464
|
+
/**
|
|
465
|
+
* Spans not scanned because one of the two keys bounding them is unreachable.
|
|
466
|
+
*
|
|
467
|
+
* A scan that did not run and a scan that found nothing must never print the
|
|
468
|
+
* same way, and the span list only holds the ones that ran.
|
|
469
|
+
*/
|
|
470
|
+
spansNotScanned: number;
|
|
471
|
+
/**
|
|
472
|
+
* Every interval between two consecutive keys, scanned (issue #403).
|
|
473
|
+
*
|
|
474
|
+
* One entry per consecutive pair per timeline, including the pairs where
|
|
475
|
+
* nothing was predicted — a scan that ran and found nothing has to be
|
|
476
|
+
* distinguishable from a scan that never ran.
|
|
477
|
+
*/
|
|
478
|
+
spans: DeformSpan[];
|
|
479
|
+
/** Spans whose predicted fold was measured at a time the mesh draws. */
|
|
480
|
+
spanFolds: number;
|
|
481
|
+
/** Spans whose predicted fold landed only where the mesh draws nothing. */
|
|
482
|
+
spansNotDrawn: number;
|
|
483
|
+
/** Spans that predicted a fold no probe reproduced. See `DeformSpan.unconfirmed`. */
|
|
484
|
+
spansUnconfirmed: number;
|
|
485
|
+
/**
|
|
486
|
+
* Extra posed measurements the span scan took — **the cost figure**, and 0 on
|
|
487
|
+
* a rig the closed form flags nothing in, which is every green rig. `CUR`-style
|
|
488
|
+
* accounting rather than a timing: a wall clock in a gate is not reproducible
|
|
489
|
+
* and this is (`DW16`).
|
|
490
|
+
*/
|
|
491
|
+
spanProbes: number;
|
|
492
|
+
/**
|
|
493
|
+
* Dials whose probe tied and whose artifact broke the tie, in the skeleton's
|
|
494
|
+
* own constraint order (issue #419). Empty on every rig where it did not.
|
|
495
|
+
*/
|
|
496
|
+
dialTies: DeformDialTie[];
|
|
497
|
+
/**
|
|
498
|
+
* Dials the artifact and the probe name differently, in the same order, each
|
|
499
|
+
* carrying both answers, both reaches and the frames the artifact's answer
|
|
500
|
+
* could not have posed (issues #419, #427).
|
|
501
|
+
*/
|
|
502
|
+
dialDisputes: DeformDialDispute[];
|
|
503
|
+
/**
|
|
504
|
+
* Which poser the survey posed through, and why when it is not the one
|
|
505
|
+
* asked for (issue #969): `model` — rigc's own core over the model document;
|
|
506
|
+
* `spine-core` — the runtime over the Spine skeleton. Every other field is
|
|
507
|
+
* the same survey whichever it was; `tools/survey_hashes.ts` holds that.
|
|
508
|
+
*/
|
|
509
|
+
source: DeformSurveyRecord;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** The survey's record of its poser (`DeformSurvey.source`). */
|
|
513
|
+
export interface DeformSurveyRecord {
|
|
514
|
+
used: DeformSurveySource;
|
|
515
|
+
why: string | null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Why no dial value selects this key's time, in the one sentence A39's stats
|
|
520
|
+
* line, its SKIP reason and the `DEFORM` block all print (issue #407).
|
|
521
|
+
*
|
|
522
|
+
* ⭐ One sentence, three readers, for the reason the whole of this file is one
|
|
523
|
+
* survey: a report and a gate that describe the same key differently are two
|
|
524
|
+
* derivations, and the one that drifts is the one nobody exits non-zero on.
|
|
525
|
+
* Empty on a key that IS reachable, so a caller cannot print it by accident.
|
|
526
|
+
*/
|
|
527
|
+
export function unreachableWhy(key: DeformKeyMeasure): string {
|
|
528
|
+
const dial = key.dial;
|
|
529
|
+
if (dial === null || !dial.unreachable) return '';
|
|
530
|
+
const driven =
|
|
531
|
+
key.reach.bone === null
|
|
532
|
+
? `slider "${key.reach.slider}"'s own time`
|
|
533
|
+
: `${key.reach.bone}.${key.reach.property} (${key.reach.local ? 'local' : 'world'})`;
|
|
534
|
+
// 🚨 The runaway, named with both numbers instead of printed as a dial (issue
|
|
535
|
+
// #419). This comes first because it is a different fact from the one below: the
|
|
536
|
+
// bone was never put where the mapping asked, so what the runtime applied is the
|
|
537
|
+
// clamp's time and says nothing about whether the key is otherwise reachable.
|
|
538
|
+
if (dial.beyondLimit !== null) {
|
|
539
|
+
const field = key.reach.drive ?? key.reach.property;
|
|
540
|
+
return (
|
|
541
|
+
`the dial for t=${key.time}s is out of bounds: slider "${key.reach.slider}" maps that time back to ` +
|
|
542
|
+
`${dial.value.toFixed(6)}, which needs ${key.reach.bone}.${field} at ${dial.beyondLimit.toExponential(4)} — ` +
|
|
543
|
+
`past the ±${DIAL_DRIVE_LIMIT} this solve will drive a bone field to. Past 2^24 a float32 no longer holds ` +
|
|
544
|
+
'two consecutive integers apart, so a figure up there is not a value anybody can set and read back. The ' +
|
|
545
|
+
'bone was left at its setup value rather than driven there, so what was posed is the setup frame and not ' +
|
|
546
|
+
`the one that mapping names — the runtime applied ${dial.applied.toFixed(6)}s against the ` +
|
|
547
|
+
`${dial.wanted.toFixed(6)}s wanted`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
// The one reader that cannot produce a value it is asked for, named where an
|
|
551
|
+
// author will meet it: a WORLD rotation is an `atan2` ending in
|
|
552
|
+
// `if (value < 0) value += 360`, so **[0, 360) is the whole of its range** and
|
|
553
|
+
// `"local": true` is the fix (issue #405).
|
|
554
|
+
//
|
|
555
|
+
// ⚠️ Both ends, not just the low one — a range running past 360° dies in
|
|
556
|
+
// exactly the way one dipping below 0° does. The compiler refuses both since
|
|
557
|
+
// issue #417 (it refused only the low end when #405 landed), so a rig spec in
|
|
558
|
+
// this repository cannot ask for either. This clause is still not redundant:
|
|
559
|
+
// an artifact reaching the gate from the editor, a hand edit or an older rigc
|
|
560
|
+
// can carry a dead range, and the artifact is what this file reads.
|
|
561
|
+
const wrap =
|
|
562
|
+
key.reach.property === 'rotate' && !key.reach.local && (dial.value < 0 || dial.value >= 360)
|
|
563
|
+
? '. A world rotation is read through `FromRotate.value`, an `atan2` ending `if (value < 0) value += 360`, ' +
|
|
564
|
+
'so its whole range is [0, 360) and a driving value outside that is one the runtime never produces — ' +
|
|
565
|
+
'`"local": true` on the slider reads the bone\'s own rotation signed and unwrapped'
|
|
566
|
+
: '';
|
|
567
|
+
return (
|
|
568
|
+
`no value of ${driven} selects t=${key.time}s: slider "${key.reach.slider}" maps that time back to ` +
|
|
569
|
+
`${dial.value.toFixed(6)}, and driven there the runtime applies the animation at ${dial.applied.toFixed(6)}s ` +
|
|
570
|
+
`rather than ${dial.wanted.toFixed(6)}s${wrap}`
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* The survey through one reader and one poser (issues #969, #1019):
|
|
576
|
+
* `surveyDeformKeys`' whole body, with the skeleton's structure — its
|
|
577
|
+
* timelines, keys, curves, sliders and skins — read through `structure`
|
|
578
|
+
* (`./deformstructure.ts`), and every pose taken through `poser`. The two come
|
|
579
|
+
* in pairs: spine-core's reading of the Spine skeleton with spine-core's
|
|
580
|
+
* poses, or the model document's with the core's.
|
|
581
|
+
*/
|
|
582
|
+
export function surveyWith(structure: SurveyStructure, exempt: ReadonlySet<string>, poser: SurveyPoser): Omit<DeformSurvey, 'source'> {
|
|
583
|
+
const keys: DeformKeyMeasure[] = [];
|
|
584
|
+
const spans: DeformSpan[] = [];
|
|
585
|
+
const exempted = new Set<string>();
|
|
586
|
+
const notAMesh = new Set<string>();
|
|
587
|
+
let timelines = 0;
|
|
588
|
+
let trianglesMeasured = 0;
|
|
589
|
+
let collapsedTotal = 0;
|
|
590
|
+
let notDrawn = 0;
|
|
591
|
+
let notDrawnReversed = 0;
|
|
592
|
+
let notReachable = 0;
|
|
593
|
+
let notReachableReversed = 0;
|
|
594
|
+
let spansNotScanned = 0;
|
|
595
|
+
const dialTies: DeformDialTie[] = [];
|
|
596
|
+
const dialDisputes: DeformDialDispute[] = [];
|
|
597
|
+
/**
|
|
598
|
+
* `reachesOf` under one skin, computed once per skin the survey reaches.
|
|
599
|
+
*
|
|
600
|
+
* ⚠️ Keyed on the skin's HANDLE, which is what `placementOf` recovered — one
|
|
601
|
+
* per skin, whichever reader made it. A map keyed on the name would fold two
|
|
602
|
+
* same-named skins into one entry and hand the second one the first's plans
|
|
603
|
+
* (#583).
|
|
604
|
+
*/
|
|
605
|
+
const reachCache = new Map<SurveySkin | null, Map<string, Array<DialPlan | null>>>();
|
|
606
|
+
const reachesUnder = (skin: SurveySkin | null): Map<string, Array<DialPlan | null>> => {
|
|
607
|
+
const already = reachCache.get(skin);
|
|
608
|
+
if (already !== undefined) return already;
|
|
609
|
+
const built = reachesOf(structure, skin, poser);
|
|
610
|
+
reachCache.set(skin, built);
|
|
611
|
+
return built;
|
|
612
|
+
};
|
|
613
|
+
/**
|
|
614
|
+
* Sliders whose tie or dispute is already in the rollup, by name.
|
|
615
|
+
*
|
|
616
|
+
* A slider used to be planned once per animation and therefore reported once.
|
|
617
|
+
* It is now planned once per (animation, skin) — so the guard keeps the rollup
|
|
618
|
+
* at one line per slider, and it loses nothing: a slider the skins do not
|
|
619
|
+
* touch (`skinRequired` false, and a driving bone that is active under every
|
|
620
|
+
* skin) probes identically under all of them, and one they do touch is
|
|
621
|
+
* `active` under exactly the one skin that lists it, because a rig spec is
|
|
622
|
+
* refused for putting a constraint or a bone in two skins.
|
|
623
|
+
*/
|
|
624
|
+
const dialsReported = new Set<string>();
|
|
625
|
+
for (const anim of structure.animations) {
|
|
626
|
+
/**
|
|
627
|
+
* What each of this animation's deform timelines is, and which skin holds
|
|
628
|
+
* its attachment — settled in one pass, BEFORE the frames loop (issue #583).
|
|
629
|
+
*
|
|
630
|
+
* 🚨 Once per timeline, not once per (timeline, skin, way-in). `timelines`,
|
|
631
|
+
* `notAMesh` and `exempted` are counts of the animation's own contents, and
|
|
632
|
+
* a skeleton with meshes in two skins would report each of them twice if the
|
|
633
|
+
* tally sat inside the loops below.
|
|
634
|
+
*/
|
|
635
|
+
const placed: Array<{
|
|
636
|
+
timeline: SurveyDeformTimeline;
|
|
637
|
+
attachment: SurveyMesh;
|
|
638
|
+
triangles: ArrayLike<number>;
|
|
639
|
+
slotName: string;
|
|
640
|
+
placement: ReturnType<SurveyDeformTimeline['placement']>;
|
|
641
|
+
}> = [];
|
|
642
|
+
for (const timeline of anim.deforms) {
|
|
643
|
+
timelines++;
|
|
644
|
+
const attachment = timeline.mesh;
|
|
645
|
+
const slotName = structure.slotName(timeline.slotIndex);
|
|
646
|
+
// A bounding box, a clipping polygon and a path all have a vertex array
|
|
647
|
+
// and NO triangles, so they have no winding to keep and no area to take a
|
|
648
|
+
// ratio of. Saying nothing about them beats inventing a measurement.
|
|
649
|
+
if (attachment === null) {
|
|
650
|
+
notAMesh.add(`"${slotName}"`);
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
if (exempt.has(slotName)) {
|
|
654
|
+
exempted.add(`"${slotName}"`);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
const triangles = attachment.triangles;
|
|
658
|
+
if (!triangles || triangles.length < 3) continue; // A04 owns a mesh with no triangles
|
|
659
|
+
placed.push({
|
|
660
|
+
timeline,
|
|
661
|
+
attachment,
|
|
662
|
+
triangles,
|
|
663
|
+
slotName,
|
|
664
|
+
placement: timeline.placement(),
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
if (placed.length === 0) continue;
|
|
668
|
+
// One pass per skin the animation's meshes live in, in the order its
|
|
669
|
+
// timelines name them. Every rig rigc compiles today has exactly one, and
|
|
670
|
+
// then this loop runs once and the keys come out in timeline order as they
|
|
671
|
+
// always have.
|
|
672
|
+
for (const skin of new Set(placed.map((p) => p.placement.holder))) {
|
|
673
|
+
// One pass per way in (issue #407). The animations nothing applies get the
|
|
674
|
+
// single track pass this loop has always been.
|
|
675
|
+
for (const dials of reachesUnder(skin).get(anim.name) ?? [null]) {
|
|
676
|
+
const poseFrame = (time: number): PoseOfFrame =>
|
|
677
|
+
dials === null
|
|
678
|
+
? { posed: poser.track(skin, anim.name, time), dial: null }
|
|
679
|
+
: poseDial(poser, skin, dials, time);
|
|
680
|
+
const reach = dials === null ? TRACK_REACH : dials.reach;
|
|
681
|
+
/**
|
|
682
|
+
* The key times this plan actually **reached**, for the reach comparison
|
|
683
|
+
* below (#427).
|
|
684
|
+
*
|
|
685
|
+
* ⚠️ A key the drive itself could not select is not in here. It is already
|
|
686
|
+
* named on the stats line as `deformKeysUnreachable`, with the ask and the
|
|
687
|
+
* bound, and the artifact's answer cannot reach it either — so listing it
|
|
688
|
+
* as something the artifact's answer missed would count one defect twice
|
|
689
|
+
* and inflate a disagreement with a frame the disagreement did not cost.
|
|
690
|
+
*/
|
|
691
|
+
const posedTimes = new Set<number>();
|
|
692
|
+
for (const { timeline, attachment, triangles, slotName, placement } of placed) {
|
|
693
|
+
// Measured in the dress the format keys this timeline on, and only
|
|
694
|
+
// there: a mesh in another skin is another skin's pose (issue #583).
|
|
695
|
+
if (placement.holder !== skin) continue;
|
|
696
|
+
const named = {
|
|
697
|
+
animation: anim.name,
|
|
698
|
+
skin: placement.skin,
|
|
699
|
+
slot: slotName,
|
|
700
|
+
attachment: attachment.name,
|
|
701
|
+
placeholder: placement.placeholder,
|
|
702
|
+
};
|
|
703
|
+
/** The previous key's posed frame, kept so the span between them can be scanned. */
|
|
704
|
+
let previous: PosedFrame | null = null;
|
|
705
|
+
for (let frame = 0; frame < timeline.frames.length; frame++) {
|
|
706
|
+
const time = timeline.frames[frame];
|
|
707
|
+
const at = poseFrame(time);
|
|
708
|
+
const frameMeasure = measurePosed(at.posed, time, timeline.slotIndex, attachment, triangles, reach, at.dial);
|
|
709
|
+
// A key that draws no pixels — or one at a time no dial can select —
|
|
710
|
+
// is measured and then left out of the totals, because those totals
|
|
711
|
+
// are "what the gate ran on": A39 reads them onto its stats line and
|
|
712
|
+
// the report's rollup has to match them.
|
|
713
|
+
if (at.dial?.unreachable === true) {
|
|
714
|
+
notReachable++;
|
|
715
|
+
notReachableReversed += frameMeasure.measure.reversed.length;
|
|
716
|
+
} else if (frameMeasure.measure.draw.blank === null) {
|
|
717
|
+
trianglesMeasured += frameMeasure.measure.triangles;
|
|
718
|
+
collapsedTotal += frameMeasure.measure.collapsed;
|
|
719
|
+
} else {
|
|
720
|
+
notDrawn++;
|
|
721
|
+
notDrawnReversed += frameMeasure.measure.reversed.length;
|
|
722
|
+
}
|
|
723
|
+
keys.push({ ...named, key: frame, time, ...frameMeasure.measure });
|
|
724
|
+
if (at.dial?.unreachable !== true) posedTimes.add(time);
|
|
725
|
+
if (previous !== null) {
|
|
726
|
+
// ⚠️ A span whose end is a frame the runtime cannot reach has no
|
|
727
|
+
// interpolation to scan: the anchors it would solve the quadratic
|
|
728
|
+
// over are two poses of some other time. Counted, never silent.
|
|
729
|
+
if (at.dial?.unreachable === true || previous.measure.dial?.unreachable === true) {
|
|
730
|
+
spansNotScanned++;
|
|
731
|
+
} else {
|
|
732
|
+
spans.push(
|
|
733
|
+
scanDeformSpan(
|
|
734
|
+
anim,
|
|
735
|
+
timeline,
|
|
736
|
+
attachment,
|
|
737
|
+
triangles,
|
|
738
|
+
named,
|
|
739
|
+
frame - 1,
|
|
740
|
+
previous,
|
|
741
|
+
frameMeasure,
|
|
742
|
+
poseFrame,
|
|
743
|
+
),
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
previous = frameMeasure;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
// --- what the artifact's own answer could NOT have posed (issue #427) ---
|
|
751
|
+
//
|
|
752
|
+
// ⭐ Posed, not predicted. The survey builds a second plan out of the field
|
|
753
|
+
// the SKELETON names and runs it through the same `poseDial` every real
|
|
754
|
+
// frame goes through, so each entry is spine-core saying "no settable value
|
|
755
|
+
// of this field lands me on that time" rather than this file inferring it
|
|
756
|
+
// off an interval. Measured (#427): the list is empty whenever the two
|
|
757
|
+
// answers reach the same span, and empty is the reading that says the
|
|
758
|
+
// disagreement changed nothing about which frames were measured.
|
|
759
|
+
//
|
|
760
|
+
// 🚨 And nothing is reported at all about a dial that posed NOTHING — a
|
|
761
|
+
// slider whose animation carries no deform timeline. `outside` would be
|
|
762
|
+
// empty there for the one reason that must never print as agreement:
|
|
763
|
+
// there were no frames to disagree about. A comparison of nothing and a
|
|
764
|
+
// comparison that came out equal are the vacuous pass this file exists
|
|
765
|
+
// to keep apart.
|
|
766
|
+
if (posedTimes.size === 0) continue;
|
|
767
|
+
// A plan is visited once per (animation, skin) — see `dialsReported` for
|
|
768
|
+
// why one line per slider is the whole of it — so the two lists come out
|
|
769
|
+
// in the skeleton's own constraint order.
|
|
770
|
+
const alreadyReported = dials !== null && dialsReported.has(dials.slider.name);
|
|
771
|
+
if (dials !== null) dialsReported.add(dials.slider.name);
|
|
772
|
+
if (dials?.tie && !alreadyReported) dialTies.push(dials.tie);
|
|
773
|
+
if (dials?.dispute && !alreadyReported) dialDisputes.push(dials.dispute);
|
|
774
|
+
if (dials?.dispute && dials.statedMap !== null) {
|
|
775
|
+
const shadow: DialPlan = {
|
|
776
|
+
...dials,
|
|
777
|
+
field: dials.statedMap.field,
|
|
778
|
+
u0: dials.statedMap.u0,
|
|
779
|
+
v0: dials.statedMap.v0,
|
|
780
|
+
u1: dials.statedMap.u1,
|
|
781
|
+
v1: dials.statedMap.v1,
|
|
782
|
+
};
|
|
783
|
+
// ⚠️ The one time posing cannot answer: a field that moves the reading by
|
|
784
|
+
// NOTHING has no map to invert, so `poseDial` divides by zero and calls
|
|
785
|
+
// every time out of bounds — including the setup time, which that field
|
|
786
|
+
// reaches by being left alone. The reach says which one that is, and it
|
|
787
|
+
// is a single point. Nothing in spine-core 4.3 has been measured getting
|
|
788
|
+
// here (a parent at exactly 90° still moves a world x reading by 2.3e-8),
|
|
789
|
+
// and a report that over-stated a disagreement by one key would be the
|
|
790
|
+
// false red this file has paid for twice.
|
|
791
|
+
const flat = dials.statedMap.v1 === dials.statedMap.v0;
|
|
792
|
+
const only = dials.dispute.statedReach;
|
|
793
|
+
const outside = [...posedTimes]
|
|
794
|
+
.filter((time) =>
|
|
795
|
+
flat
|
|
796
|
+
? only === null || Math.abs(time - only.lo) > DIAL_TIME_EPSILON
|
|
797
|
+
: poseDial(poser, skin, shadow, time).dial?.unreachable === true,
|
|
798
|
+
)
|
|
799
|
+
.sort((a, b) => a - b);
|
|
800
|
+
dials.dispute.outside.push(...outside);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
let spanProbes = 0;
|
|
806
|
+
let spanFolds = 0;
|
|
807
|
+
let spansNotDrawn = 0;
|
|
808
|
+
let spansUnconfirmed = 0;
|
|
809
|
+
for (const span of spans) {
|
|
810
|
+
spanProbes += span.probed.length;
|
|
811
|
+
if (span.fold !== null) spanFolds++;
|
|
812
|
+
if (span.notDrawn > 0) spansNotDrawn++;
|
|
813
|
+
if (span.unconfirmed) spansUnconfirmed++;
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
keys,
|
|
817
|
+
timelines,
|
|
818
|
+
exempted: [...exempted],
|
|
819
|
+
notAMesh: [...notAMesh],
|
|
820
|
+
trianglesMeasured,
|
|
821
|
+
collapsed: collapsedTotal,
|
|
822
|
+
notDrawn,
|
|
823
|
+
notDrawnReversed,
|
|
824
|
+
notReachable,
|
|
825
|
+
notReachableReversed,
|
|
826
|
+
spans,
|
|
827
|
+
spanFolds,
|
|
828
|
+
spansNotDrawn,
|
|
829
|
+
spansUnconfirmed,
|
|
830
|
+
spansNotScanned,
|
|
831
|
+
spanProbes,
|
|
832
|
+
dialTies,
|
|
833
|
+
dialDisputes,
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// ---------------------------------------------------------------------------
|
|
838
|
+
// Which frame a key is posed in — issue #407
|
|
839
|
+
// ---------------------------------------------------------------------------
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* The six local fields of a `BonePose` a `FromProperty` can be made to read.
|
|
843
|
+
*
|
|
844
|
+
* In `SkeletonJson.fromProperty`'s own order, and the property name a reach
|
|
845
|
+
* reports is this list's entry rather than a second table: `rotation` is spelled
|
|
846
|
+
* `rotate` in a rig spec and the rest are spelled as they are here.
|
|
847
|
+
*/
|
|
848
|
+
const DIAL_FIELDS = ['rotation', 'x', 'y', 'scaleX', 'scaleY', 'shearY'] as const;
|
|
849
|
+
export type DialField = (typeof DIAL_FIELDS)[number];
|
|
850
|
+
|
|
851
|
+
/** The rig-spec spelling of one of those, which is the name a report prints. */
|
|
852
|
+
const DIAL_PROPERTY: Record<DialField, string> = {
|
|
853
|
+
rotation: 'rotate',
|
|
854
|
+
x: 'x',
|
|
855
|
+
y: 'y',
|
|
856
|
+
scaleX: 'scaleX',
|
|
857
|
+
scaleY: 'scaleY',
|
|
858
|
+
shearY: 'shearY',
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* How far a field is nudged to find out whether it moves the slider's time.
|
|
863
|
+
*
|
|
864
|
+
* A scale is a multiplier around 1 and the others are degrees or units around 0,
|
|
865
|
+
* so one step of each is a different size. The number only has to be large enough
|
|
866
|
+
* that the resulting change in the property is not float noise and small enough
|
|
867
|
+
* not to wrap a rotation: any value in between gives the same answer, because
|
|
868
|
+
* every one of these readers is affine in its own field at a fixed parent pose.
|
|
869
|
+
*/
|
|
870
|
+
function dialStep(field: DialField): number {
|
|
871
|
+
return field === 'scaleX' || field === 'scaleY' ? 0.25 : 1;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* How the artifact's answer and the probe's answer settled (issue #419).
|
|
876
|
+
*
|
|
877
|
+
* - `agreed` — one field responds decisively and it is the reader's own. Every
|
|
878
|
+
* rig in the gallery and every `local: true` slider.
|
|
879
|
+
* - `settled` — the probe was **not** decisive (two or more fields within
|
|
880
|
+
* `DIAL_PROBE_MARGIN` of each other) and the reader's own field is one of them,
|
|
881
|
+
* so the artifact broke the tie. Real: a `FromX` slider under `local: false` on
|
|
882
|
+
* a bone whose parent is at 45° is driven exactly equally by local `x` and
|
|
883
|
+
* local `y`, and letting list order decide between them would be the same
|
|
884
|
+
* silent arbitrariness #419 was filed on.
|
|
885
|
+
* - `disagreed` — the field that moves the reading is not the reader's own field.
|
|
886
|
+
* Also real: the same slider with the parent at 90° is moved by local `y` alone
|
|
887
|
+
* and not at all by local `x`. The probe wins the drive, because it is the one
|
|
888
|
+
* that measured something, and the disagreement is printed rather than resolved.
|
|
889
|
+
*/
|
|
890
|
+
type DialVerdict = 'agreed' | 'settled' | 'disagreed';
|
|
891
|
+
|
|
892
|
+
/** The two answers, what they were, and how they settled. */
|
|
893
|
+
interface DialDiscovery {
|
|
894
|
+
/** The reader's own field, off the artifact. `null` on a reader not known here. */
|
|
895
|
+
stated: DialField | null;
|
|
896
|
+
/** What the artifact's reader is called, for a report that has to name it. */
|
|
897
|
+
reader: string;
|
|
898
|
+
/** The field the solve drives. Always one the probe measured a response on. */
|
|
899
|
+
drive: DialField;
|
|
900
|
+
/** What one step of that field moved the reading by. */
|
|
901
|
+
driveResponse: number;
|
|
902
|
+
/**
|
|
903
|
+
* The other fields inside `DIAL_PROBE_MARGIN` of the winner — the ones that
|
|
904
|
+
* stopped the probe being decisive. Empty whenever it was.
|
|
905
|
+
*/
|
|
906
|
+
rivals: Array<{ field: DialField; response: number }>;
|
|
907
|
+
/** What the reader's OWN field responded, when it is not the one driven. */
|
|
908
|
+
statedResponse: number | null;
|
|
909
|
+
verdict: DialVerdict;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* A slider whose animation is being posed, with everything the inversion needs
|
|
914
|
+
* measured off the runtime rather than assumed.
|
|
915
|
+
*/
|
|
916
|
+
interface DialPlan {
|
|
917
|
+
slider: SurveySlider;
|
|
918
|
+
reach: DeformReach;
|
|
919
|
+
/**
|
|
920
|
+
* The probe's tie, when it had one — the survey collects these so a `build`
|
|
921
|
+
* sees them and not only `explain` (issues #419, #427).
|
|
922
|
+
*/
|
|
923
|
+
tie: DeformDialTie | null;
|
|
924
|
+
/** The two answers and their two reaches, when they named different fields. */
|
|
925
|
+
dispute: DeformDialDispute | null;
|
|
926
|
+
/**
|
|
927
|
+
* The bone field that drives it, or `null` on a bone-less slider — whose time
|
|
928
|
+
* IS its pose value and is set directly.
|
|
929
|
+
*/
|
|
930
|
+
field: DialField | null;
|
|
931
|
+
/** Two points of the affine map `local field -> property value`, from probes. */
|
|
932
|
+
u0: number;
|
|
933
|
+
v0: number;
|
|
934
|
+
u1: number;
|
|
935
|
+
v1: number;
|
|
936
|
+
/**
|
|
937
|
+
* The ARTIFACT's own field as a map of its own, kept only on a disputed dial
|
|
938
|
+
* (issue #427).
|
|
939
|
+
*
|
|
940
|
+
* ⭐ It is what lets `DeformDialDispute.outside` be **posed** rather than
|
|
941
|
+
* predicted: the survey builds a second plan out of it and runs the same
|
|
942
|
+
* `poseDial` the real frames go through, so "no settable value of `knob.x`
|
|
943
|
+
* reaches t=0.5s" is a measurement taken against spine-core and not an
|
|
944
|
+
* inference off an interval.
|
|
945
|
+
*/
|
|
946
|
+
statedMap: DialProbe | null;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* One field's probe: how far one step of it moved the reading, and the two points
|
|
951
|
+
* of the affine map that step traced.
|
|
952
|
+
*/
|
|
953
|
+
interface DialProbe {
|
|
954
|
+
field: DialField;
|
|
955
|
+
response: number;
|
|
956
|
+
u0: number;
|
|
957
|
+
v0: number;
|
|
958
|
+
u1: number;
|
|
959
|
+
v1: number;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* How near the wanted time the runtime has to land before the dial is called
|
|
964
|
+
* good, in seconds.
|
|
965
|
+
*
|
|
966
|
+
* Nothing is being tuned here: on an affine reader the residual is float64 noise
|
|
967
|
+
* — 1e-15 on `gallery/look` — and the two failures this separates it from are
|
|
968
|
+
* both *gross*. `FromRotate`'s wrap moves the value by 360, so the time moves by
|
|
969
|
+
* `360 · scale`; `Math.max(0, time)` moves it by the whole of whatever was
|
|
970
|
+
* negative. There is nothing measured between the two.
|
|
971
|
+
*/
|
|
972
|
+
const DIAL_TIME_EPSILON = 1e-6;
|
|
973
|
+
|
|
974
|
+
/** How many secant steps the solve takes before it calls a time unreachable. */
|
|
975
|
+
const DIAL_SOLVE_STEPS = 4;
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* How far the largest probe response has to clear the runner-up before the probe
|
|
979
|
+
* alone is allowed to name the field the solve drives (issue #419).
|
|
980
|
+
*
|
|
981
|
+
* ⭐ **A ratio, so it carries no units and no fixture's scale.** What it separates
|
|
982
|
+
* was measured rather than assumed, on this file's own probe:
|
|
983
|
+
*
|
|
984
|
+
* | reading | field | response to one step | ratio to the winner |
|
|
985
|
+
* | --- | --- | --- | --- |
|
|
986
|
+
* | `FromScaleX`, world, plain parent | `scaleX` | 2.5e-1 | — |
|
|
987
|
+
* | | `rotation` | 4.0e-10 (float noise in `sqrt(a²+c²)`) | 6.2e8 |
|
|
988
|
+
* | `FromX`, world, parent at 90° | `y` | 1.0 | — |
|
|
989
|
+
* | | `x` | 4.6e-8 (float noise) | 2.2e7 |
|
|
990
|
+
* | `FromScaleX`, world, parent scaled 2×1 | `scaleX` | 5.0e-1 | — |
|
|
991
|
+
* | | `rotation` | 2.3e-4 (**real**, not noise) | 2.2e3 |
|
|
992
|
+
* | `FromX`, world, parent at 45° | `x` | 7.07e-1 | — |
|
|
993
|
+
* | | `y` | 7.07e-1 (**real**, and exactly equal) | 1.0 |
|
|
994
|
+
*
|
|
995
|
+
* So 1e3 sits between the smallest genuine secondary dependency measured (2.2e3)
|
|
996
|
+
* and the largest float-noise ratio (2.2e7), with four orders of headroom on the
|
|
997
|
+
* side that matters. ⚠️ It is not a noise threshold and must not be read as one:
|
|
998
|
+
* a rig with a heavier parent scale pushes that 2.2e3 down through it, and what
|
|
999
|
+
* happens then is not a wrong answer but an *ambiguous* one — which this file
|
|
1000
|
+
* reports and the artifact settles, below.
|
|
1001
|
+
*/
|
|
1002
|
+
const DIAL_PROBE_MARGIN = 1e3;
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* The largest magnitude the solve will drive a bone field to (issue #419).
|
|
1006
|
+
*
|
|
1007
|
+
* 🚨 **This is the bound that replaces "gave up at 4.9e9".** A dial figure is an
|
|
1008
|
+
* instruction — *set `knob.scaleX` to this* — and past 2²⁴ a float32 cannot hold
|
|
1009
|
+
* two consecutive integers apart, so a number up there is not one an author can
|
|
1010
|
+
* set and read back; the emitted skeleton, which rounds to six decimals, has lost
|
|
1011
|
+
* the precision long before. A drive the mapping asks for beyond this is
|
|
1012
|
+
* therefore **not posed**: the bone stops at the bound, the runtime lands on some
|
|
1013
|
+
* other time, and the key is reported unreachable with the ask and the bound both
|
|
1014
|
+
* named — never printed as if it were advice.
|
|
1015
|
+
*
|
|
1016
|
+
* The runaway #419 was filed on reached 1.2e9, 2.5e9 and 4.9e9, all two orders
|
|
1017
|
+
* above this, and every dial `gallery/look` and the DW fixtures ask for is under
|
|
1018
|
+
* 200.
|
|
1019
|
+
*/
|
|
1020
|
+
const DIAL_DRIVE_LIMIT = 2 ** 24;
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Every way each animation is reached, keyed by animation name.
|
|
1024
|
+
*
|
|
1025
|
+
* A `null` entry in the returned list is the track — the frame every animation
|
|
1026
|
+
* had before #407 — and it is what an animation NO slider applies gets. An
|
|
1027
|
+
* animation a slider applies gets one plan per slider and no track entry, on
|
|
1028
|
+
* spine-core's own statement that *"slider animations are designed to be applied
|
|
1029
|
+
* by slider constraints rather than on their own"*
|
|
1030
|
+
* (`SkeletonData.findSliderAnimations`).
|
|
1031
|
+
*
|
|
1032
|
+
* ⚠️ A slider **muted at setup** applies nothing: `Slider.update` returns before
|
|
1033
|
+
* it reads the bone when `mix` is 0. So it is not a way in, and its animation
|
|
1034
|
+
* keeps the track frame — which is the older idiom of muting at setup and keying
|
|
1035
|
+
* `slider.<name>.mix` from a playing animation, where the frame the deform keys
|
|
1036
|
+
* actually occur in is that playing animation's, and rigc has no way to know
|
|
1037
|
+
* which one that is.
|
|
1038
|
+
*
|
|
1039
|
+
* ⚠️ **Per skin, because whether a slider runs at all is per skin** (#583). A
|
|
1040
|
+
* `skinRequired` slider is out of the update cache under every skin that does
|
|
1041
|
+
* not list it, and a slider on a `skinRequired` bone reads a world property that
|
|
1042
|
+
* nothing moves there — so the same constraint plans differently depending on
|
|
1043
|
+
* what the skeleton is wearing, and the answer that matters is the one under the
|
|
1044
|
+
* skin holding the mesh being measured.
|
|
1045
|
+
*/
|
|
1046
|
+
function reachesOf(structure: SurveyStructure, skin: SurveySkin | null, poser: SurveyPoser): Map<string, Array<DialPlan | null>> {
|
|
1047
|
+
const out = new Map<string, Array<DialPlan | null>>();
|
|
1048
|
+
for (const anim of structure.animations) out.set(anim.name, []);
|
|
1049
|
+
for (const slider of structure.sliders) {
|
|
1050
|
+
if (slider.mix === 0) continue;
|
|
1051
|
+
const list = out.get(slider.animation);
|
|
1052
|
+
if (list === undefined) continue;
|
|
1053
|
+
const plan = planDial(poser, skin, slider);
|
|
1054
|
+
if (plan !== null) list.push(plan);
|
|
1055
|
+
}
|
|
1056
|
+
for (const list of out.values()) if (list.length === 0) list.push(null);
|
|
1057
|
+
return out;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Find which local field of the driving bone moves a slider's time, and read the
|
|
1062
|
+
* affine map from that field to the property value off two probes.
|
|
1063
|
+
*
|
|
1064
|
+
* ⭐ **Probed rather than tabulated.** How a `FromProperty` turns a bone into a
|
|
1065
|
+
* number is spine-core's business, and under `local: false` it goes through the
|
|
1066
|
+
* world transform — so a table of that here would be a second copy of the
|
|
1067
|
+
* runtime's arithmetic AND a claim about parents this file has no business
|
|
1068
|
+
* making. Calls to `data.property.value` — the same call `Slider.update` makes,
|
|
1069
|
+
* with the same all-zero offsets — say it instead, and every one of the six
|
|
1070
|
+
* readers is affine in its own field at a fixed parent pose, so two points of the
|
|
1071
|
+
* winner are the whole map.
|
|
1072
|
+
*
|
|
1073
|
+
* 🚨 **What the probe is NOT allowed to do is stop at the first field that
|
|
1074
|
+
* moves** (issue #419). A world scale is read as `sqrt(a² + c²)`; perturbing
|
|
1075
|
+
* `rotation` changes `a` and `c` by amounts that do not cancel in floating point,
|
|
1076
|
+
* `rotation` is probed first, and every world `scaleX` / `scaleY` / `shearY`
|
|
1077
|
+
* slider was therefore reported as `rotate` — and then solved through a field
|
|
1078
|
+
* whose response to it is 4e-10, which is how a drive reached 4.9e9. So:
|
|
1079
|
+
*
|
|
1080
|
+
* 1. **every** field is probed and they are ranked by response;
|
|
1081
|
+
* 2. the winner has to clear the runner-up by `DIAL_PROBE_MARGIN` to be called
|
|
1082
|
+
* decisive — a tie is a real thing (a `FromX` slider on a bone whose parent
|
|
1083
|
+
* is at 45° is moved exactly equally by local `x` and local `y`) and letting
|
|
1084
|
+
* `DIAL_FIELDS`'s order settle it silently is the same defect one step over;
|
|
1085
|
+
* 3. and the answer is cross-checked against the **artifact** — spine-core's own
|
|
1086
|
+
* reader class, which is what the parser built out of the rig spec's
|
|
1087
|
+
* `property`. That is what the report names, it settles a tie the probe could
|
|
1088
|
+
* not, and where the two genuinely differ the report says so by name.
|
|
1089
|
+
*
|
|
1090
|
+
* ⛔ Nothing here falls back to `rotation`, or to any field, when the search does
|
|
1091
|
+
* not settle. Two answers that disagree are two answers, printed.
|
|
1092
|
+
*/
|
|
1093
|
+
function planDial(poser: SurveyPoser, skin: SurveySkin | null, slider: SurveySlider): DialPlan | null {
|
|
1094
|
+
const boneName = slider.bone ?? '?';
|
|
1095
|
+
const where = slider.local ? ' (local)' : ' (world)';
|
|
1096
|
+
const reach = (discovery: DialDiscovery | null): DeformReach => {
|
|
1097
|
+
if (discovery === null) {
|
|
1098
|
+
return {
|
|
1099
|
+
kind: 'slider',
|
|
1100
|
+
slider: slider.name,
|
|
1101
|
+
bone: slider.bone,
|
|
1102
|
+
property: null,
|
|
1103
|
+
drive: null,
|
|
1104
|
+
local: slider.local,
|
|
1105
|
+
label: `applied by slider "${slider.name}" at its own time`,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
// The name comes off the artifact whenever the artifact has one. A reader
|
|
1109
|
+
// this file does not know is named by its class rather than by a guess.
|
|
1110
|
+
const property = discovery.stated === null ? discovery.reader : DIAL_PROPERTY[discovery.stated];
|
|
1111
|
+
const drive = discovery.stated === discovery.drive ? null : DIAL_PROPERTY[discovery.drive];
|
|
1112
|
+
return {
|
|
1113
|
+
kind: 'slider',
|
|
1114
|
+
slider: slider.name,
|
|
1115
|
+
bone: slider.bone,
|
|
1116
|
+
property,
|
|
1117
|
+
drive,
|
|
1118
|
+
local: slider.local,
|
|
1119
|
+
label:
|
|
1120
|
+
`applied by slider "${slider.name}" off ${boneName}.${property}${where}` +
|
|
1121
|
+
dialDiscoveryClause(discovery, boneName),
|
|
1122
|
+
};
|
|
1123
|
+
};
|
|
1124
|
+
// The bone-less form: `Slider.update` leaves `p.time` alone, so the dial IS the
|
|
1125
|
+
// pose value and the map is the identity.
|
|
1126
|
+
if (slider.bone === null) {
|
|
1127
|
+
return { slider, reach: reach(null), tie: null, dispute: null, field: null, u0: 0, v0: 0, u1: 1, v1: 1, statedMap: null };
|
|
1128
|
+
}
|
|
1129
|
+
const session = poser.dial(skin, slider);
|
|
1130
|
+
if (session === null || !session.hasBone) return null;
|
|
1131
|
+
// ⚠️ Every field is kept, the dead ones included, because the ARTIFACT may name
|
|
1132
|
+
// one of them: a reach comparison needs the map of the field the skeleton
|
|
1133
|
+
// declares even when that field moves the reading by nothing at all (#427).
|
|
1134
|
+
const all: DialProbe[] = [];
|
|
1135
|
+
for (const field of DIAL_FIELDS) {
|
|
1136
|
+
const step = dialStep(field);
|
|
1137
|
+
const base = session.base(field);
|
|
1138
|
+
const v0 = session.at(field, base).read;
|
|
1139
|
+
const v1 = session.at(field, base + step).read;
|
|
1140
|
+
all.push({ field, response: Math.abs(v1 - v0), u0: base, v0, u1: base + step, v1 });
|
|
1141
|
+
}
|
|
1142
|
+
const probes = all.filter((p) => Number.isFinite(p.response) && p.response !== 0);
|
|
1143
|
+
// Nothing moves it: a bone another constraint pins, or a reader that cannot see
|
|
1144
|
+
// this bone at all. A37 owns the `scale: 0` shape of the same silence.
|
|
1145
|
+
if (probes.length === 0) return null;
|
|
1146
|
+
// ⚠️ A stable sort on a strict comparison, so an exact tie keeps `DIAL_FIELDS`'s
|
|
1147
|
+
// order and the tie is DETECTED below rather than decided here. A comparator
|
|
1148
|
+
// that broke ties would put the arbitrary choice back where nothing sees it.
|
|
1149
|
+
probes.sort((a, b) => b.response - a.response);
|
|
1150
|
+
const winner = probes[0];
|
|
1151
|
+
const stated = slider.stated;
|
|
1152
|
+
const leaders = probes.filter((p) => p.response * DIAL_PROBE_MARGIN >= winner.response);
|
|
1153
|
+
const decisive = leaders.length === 1;
|
|
1154
|
+
const chosen = decisive || stated === null ? winner : (leaders.find((p) => p.field === stated) ?? winner);
|
|
1155
|
+
const verdict: DialVerdict = chosen.field === stated ? (decisive ? 'agreed' : 'settled') : 'disagreed';
|
|
1156
|
+
const discovery: DialDiscovery = {
|
|
1157
|
+
stated,
|
|
1158
|
+
reader: slider.reader,
|
|
1159
|
+
drive: chosen.field,
|
|
1160
|
+
driveResponse: chosen.response,
|
|
1161
|
+
rivals: leaders.filter((p) => p.field !== chosen.field).map((p) => ({ field: p.field, response: p.response })),
|
|
1162
|
+
statedResponse:
|
|
1163
|
+
stated === null || stated === chosen.field ? null : (all.find((p) => p.field === stated)?.response ?? 0),
|
|
1164
|
+
verdict,
|
|
1165
|
+
};
|
|
1166
|
+
// 🔒 A tie and a disagreement are built as two different things, because they
|
|
1167
|
+
// ARE two different things: a tie has one belief the artifact broke, and a
|
|
1168
|
+
// disagreement has two that have to be compared. Folding them into one record
|
|
1169
|
+
// with a `verdict` field is how a report comes to say "disagreed" about
|
|
1170
|
+
// legitimate geometry (issues #419, #427).
|
|
1171
|
+
const statedMap = stated === null ? null : (all.find((p) => p.field === stated) ?? null);
|
|
1172
|
+
const tie: DeformDialTie | null =
|
|
1173
|
+
verdict !== 'settled'
|
|
1174
|
+
? null
|
|
1175
|
+
: {
|
|
1176
|
+
slider: slider.name,
|
|
1177
|
+
bone: boneName,
|
|
1178
|
+
drive: DIAL_PROPERTY[chosen.field],
|
|
1179
|
+
driveResponse: chosen.response,
|
|
1180
|
+
rivals: discovery.rivals.map((r) => ({ field: DIAL_PROPERTY[r.field], response: r.response })),
|
|
1181
|
+
};
|
|
1182
|
+
const dispute: DeformDialDispute | null =
|
|
1183
|
+
verdict !== 'disagreed'
|
|
1184
|
+
? null
|
|
1185
|
+
: {
|
|
1186
|
+
slider: slider.name,
|
|
1187
|
+
bone: boneName,
|
|
1188
|
+
stated: stated === null ? discovery.reader : DIAL_PROPERTY[stated],
|
|
1189
|
+
statedResponse: discovery.statedResponse,
|
|
1190
|
+
statedReach: statedMap === null ? null : dialReachOf(slider, statedMap),
|
|
1191
|
+
drive: DIAL_PROPERTY[chosen.field],
|
|
1192
|
+
driveResponse: chosen.response,
|
|
1193
|
+
driveReach: dialReachOf(slider, chosen),
|
|
1194
|
+
outside: [],
|
|
1195
|
+
};
|
|
1196
|
+
return {
|
|
1197
|
+
slider,
|
|
1198
|
+
reach: reach(discovery),
|
|
1199
|
+
tie,
|
|
1200
|
+
dispute,
|
|
1201
|
+
field: chosen.field,
|
|
1202
|
+
u0: chosen.u0,
|
|
1203
|
+
v0: chosen.v0,
|
|
1204
|
+
u1: chosen.u1,
|
|
1205
|
+
v1: chosen.v1,
|
|
1206
|
+
statedMap: dispute === null ? null : statedMap,
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* The part of an animation's own `0..duration` one field of the driving bone can
|
|
1212
|
+
* select, or `null` when it can select none of it (issue #427).
|
|
1213
|
+
*
|
|
1214
|
+
* ⭐ **Closed form, and exact against the thing that decides.** `poseDial` refuses
|
|
1215
|
+
* outright a drive whose magnitude exceeds `DIAL_DRIVE_LIMIT`, and the two maps
|
|
1216
|
+
* between a field and a time are both affine — the probe's `field -> value`, and
|
|
1217
|
+
* `Slider.update`'s own `value -> time` inverted. So the times a *settable* value
|
|
1218
|
+
* of this field asks for are an interval, and its two ends are `±DIAL_DRIVE_LIMIT`
|
|
1219
|
+
* put through both. A field that moves the reading by nothing gives a single
|
|
1220
|
+
* point, which is the honest answer for it: the setup time and no other.
|
|
1221
|
+
*
|
|
1222
|
+
* ⚠️ It is a statement about what can be **asked for**, not about what the runtime
|
|
1223
|
+
* then does with it — `FromRotate`'s `[0, 360)` wrap can refuse a time this
|
|
1224
|
+
* interval contains. That is why the frames a disputed dial could not have posed
|
|
1225
|
+
* are POSED rather than read off here.
|
|
1226
|
+
*/
|
|
1227
|
+
function dialReachOf(slider: SurveySlider, map: DialProbe): DialSpan | null {
|
|
1228
|
+
const timeAt = (u: number): number => {
|
|
1229
|
+
const value = map.v0 + ((u - map.u0) * (map.v1 - map.v0)) / (map.u1 - map.u0);
|
|
1230
|
+
return slider.to + (value - slider.from) * slider.scale;
|
|
1231
|
+
};
|
|
1232
|
+
const ends = [timeAt(-DIAL_DRIVE_LIMIT), timeAt(DIAL_DRIVE_LIMIT)];
|
|
1233
|
+
if (!ends.every((t) => Number.isFinite(t))) return null;
|
|
1234
|
+
const lo = Math.max(0, Math.min(ends[0], ends[1]));
|
|
1235
|
+
const hi = Math.min(slider.duration, Math.max(ends[0], ends[1]));
|
|
1236
|
+
return lo <= hi ? { lo, hi } : null;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* The clause the frame line carries when the two answers did not simply agree —
|
|
1241
|
+
* empty on every rig where they did, which is every one in the gallery.
|
|
1242
|
+
*
|
|
1243
|
+
* ⭐ It names both answers and both responses, because "the probe and the file
|
|
1244
|
+
* disagree" without the numbers is a sentence an author cannot act on.
|
|
1245
|
+
*/
|
|
1246
|
+
function dialDiscoveryClause(discovery: DialDiscovery, bone: string): string {
|
|
1247
|
+
if (discovery.verdict === 'agreed') return '';
|
|
1248
|
+
const drive = `${bone}.${DIAL_PROPERTY[discovery.drive]} ${discovery.driveResponse.toExponential(3)}`;
|
|
1249
|
+
if (discovery.verdict === 'settled') {
|
|
1250
|
+
const rivals = discovery.rivals
|
|
1251
|
+
.map((r) => `${bone}.${DIAL_PROPERTY[r.field]} ${r.response.toExponential(3)}`)
|
|
1252
|
+
.join(', ');
|
|
1253
|
+
return (
|
|
1254
|
+
`, driven through ${bone}.${DIAL_PROPERTY[discovery.drive]} — the probe did not settle that on its own ` +
|
|
1255
|
+
`(${drive} against ${rivals}, inside the ${DIAL_PROBE_MARGIN}x margin), so the skeleton's own reader broke ` +
|
|
1256
|
+
'the tie'
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
const statedName = discovery.stated === null ? discovery.reader : `${bone}.${DIAL_PROPERTY[discovery.stated]}`;
|
|
1260
|
+
const statedAt =
|
|
1261
|
+
discovery.statedResponse === null ? 'which this file does not know' : `which moves it by ${discovery.statedResponse.toExponential(3)}`;
|
|
1262
|
+
return (
|
|
1263
|
+
`, driven through ${bone}.${DIAL_PROPERTY[discovery.drive]} — the probe moves the reading by ${drive} while ` +
|
|
1264
|
+
`the skeleton says the reader is ${statedName}, ${statedAt}. The two disagree and both are reported: the ` +
|
|
1265
|
+
'drive is the one that measurably moves the reading'
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/** A posed frame and the dial that selected it, or `null` on a track frame. */
|
|
1270
|
+
interface PoseOfFrame {
|
|
1271
|
+
posed: SurveyPose;
|
|
1272
|
+
dial: DeformDial | null;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* What `Slider.update` stores in `SliderPose.time` for a wanted animation time.
|
|
1277
|
+
*
|
|
1278
|
+
* On a slider driven by a bone: the time wrapped into the animation under
|
|
1279
|
+
* `loop`, else held at 0 or above. Not assumed — `poseDial`'s step 3 reads
|
|
1280
|
+
* `SliderPose.time` off the skeleton each time it poses a dial and compares
|
|
1281
|
+
* it with this, and a disagreement is reported, never absorbed. It is stated at all because that
|
|
1282
|
+
* verification compares against what the runtime STORED and not against what
|
|
1283
|
+
* was asked for. ⚠️ `loop` on a zero-length animation gives NaN here exactly
|
|
1284
|
+
* as it does there, which is `A37_SLIDER_CONSTRAINT_EFFECTIVE`'s refusal.
|
|
1285
|
+
*/
|
|
1286
|
+
function sliderTimeFor(slider: SurveySlider, time: number): number {
|
|
1287
|
+
if (slider.bone === null) return time;
|
|
1288
|
+
if (slider.loop) return slider.duration + (time % slider.duration);
|
|
1289
|
+
return Math.max(0, time);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* The skeleton of `data` with `plan`'s slider applying its animation at `time`.
|
|
1294
|
+
*
|
|
1295
|
+
* Three steps, and each is answerable on its own:
|
|
1296
|
+
*
|
|
1297
|
+
* 1. **invert the mapping.** `Slider.update` computes
|
|
1298
|
+
* `time = offset + (value − property.offset) · scale`, so the value that
|
|
1299
|
+
* selects `time` is `property.offset + (time − offset) / scale`. This is the
|
|
1300
|
+
* step the whole change is, and it is load-bearing rather than a hint —
|
|
1301
|
+
* everything below aims at the `value` it names and nothing corrects it.
|
|
1302
|
+
* 2. **drive the bone until spine-core's own reader returns that value.**
|
|
1303
|
+
* Through the affine map `planDial` measured; a secant closes any residual.
|
|
1304
|
+
* ⚠️ It converges on the VALUE and never on the time, which is what keeps
|
|
1305
|
+
* step 1 checkable: a solve aimed at the time would quietly absorb a wrong
|
|
1306
|
+
* sign or a dropped offset in the inversion and pose the right frame off the
|
|
1307
|
+
* wrong arithmetic — measured, it does exactly that — so the dial the report
|
|
1308
|
+
* prints would be a number no author could use. Aimed at the value, a wrong
|
|
1309
|
+
* inversion drives the bone somewhere else and step 3 says so.
|
|
1310
|
+
* 3. **check the runtime agrees.** `SliderPose.time` off the posed skeleton
|
|
1311
|
+
* against `sliderTimeFor` — spine-core's answer, not this function's. A time
|
|
1312
|
+
* no dial value selects is reported and never guessed at.
|
|
1313
|
+
*/
|
|
1314
|
+
function poseDial(poser: SurveyPoser, skin: SurveySkin | null, plan: DialPlan, time: number): PoseOfFrame {
|
|
1315
|
+
const slider = plan.slider;
|
|
1316
|
+
const wanted = sliderTimeFor(slider, time);
|
|
1317
|
+
const value = plan.field === null ? time : slider.from + (time - slider.to) / slider.scale;
|
|
1318
|
+
const session = poser.dial(skin, slider);
|
|
1319
|
+
if (session === null) return { posed: poser.track(skin, slider.animation, time), dial: null };
|
|
1320
|
+
/** Pose with the driving field at `candidate`, and read both sides back. */
|
|
1321
|
+
const field = session.hasBone ? plan.field : null;
|
|
1322
|
+
const at = (candidate: number): { read: number; applied: number } => session.at(field, candidate);
|
|
1323
|
+
/** The affine first guess, and what it is judged against. */
|
|
1324
|
+
const asked = plan.u0 + ((value - plan.v0) * (plan.u1 - plan.u0)) / (plan.v1 - plan.v0);
|
|
1325
|
+
// 🚨 The bound, and it is on the drive the answer is READ off, not on the
|
|
1326
|
+
// arithmetic that got there (issue #419). Two different things wanted bounding
|
|
1327
|
+
// and they want it differently:
|
|
1328
|
+
//
|
|
1329
|
+
// - **the mapping's own ask.** A map measured through a field the reading
|
|
1330
|
+
// barely moves inverts to nonsense — this is #419's own case, where a world
|
|
1331
|
+
// scale solved through `rotation` asked for 4.9e9 — and that is the frame
|
|
1332
|
+
// the report would print as advice. Out of bounds, it is not posed at all:
|
|
1333
|
+
// the bone stays at the setup value of its field, the key is unreachable,
|
|
1334
|
+
// and `unreachableWhy` names the ask and the bound.
|
|
1335
|
+
// - **a secant step.** The loop below is a search and a search may step
|
|
1336
|
+
// anywhere; a step out of bounds is a step this stops taking, not a verdict.
|
|
1337
|
+
// `FromRotate`'s wrap sends one there routinely on a rig whose real defect is
|
|
1338
|
+
// the wrap, and reporting the bound instead of the wrap would swap a
|
|
1339
|
+
// diagnosis for a symptom.
|
|
1340
|
+
const runaway = !Number.isFinite(asked) || Math.abs(asked) > DIAL_DRIVE_LIMIT;
|
|
1341
|
+
const first = runaway ? plan.u0 : asked;
|
|
1342
|
+
const near = 1e-9 * (1 + Math.abs(value));
|
|
1343
|
+
let u = first;
|
|
1344
|
+
let got = at(u);
|
|
1345
|
+
// Zero iterations on every affine reader, which is all six of them at a fixed
|
|
1346
|
+
// parent pose. The loop is here for the one that is not — `FromRotate` under
|
|
1347
|
+
// `local: false`, whose `[0, 360)` wrap is a step in the middle of the range —
|
|
1348
|
+
// and where it fails to close, the naive drive is what gets posed and reported,
|
|
1349
|
+
// because "the bone put where the mapping says" is the frame an author can act
|
|
1350
|
+
// on and a wandered secant point is not.
|
|
1351
|
+
let pu = first === plan.u0 ? plan.u1 : plan.u0;
|
|
1352
|
+
let pv = Number.NaN;
|
|
1353
|
+
for (let step = 0; !runaway && step < DIAL_SOLVE_STEPS && Math.abs(got.read - value) > near; step++) {
|
|
1354
|
+
if (!Number.isFinite(pv)) pv = at(pu).read;
|
|
1355
|
+
if (pv === got.read || !Number.isFinite(pv) || !Number.isFinite(got.read)) break;
|
|
1356
|
+
const next = u + ((value - got.read) * (u - pu)) / (got.read - pv);
|
|
1357
|
+
if (!Number.isFinite(next) || Math.abs(next) > DIAL_DRIVE_LIMIT) break;
|
|
1358
|
+
pu = u;
|
|
1359
|
+
pv = got.read;
|
|
1360
|
+
u = next;
|
|
1361
|
+
got = at(u);
|
|
1362
|
+
}
|
|
1363
|
+
if (Math.abs(got.read - value) > near && u !== first) {
|
|
1364
|
+
u = first;
|
|
1365
|
+
got = at(u);
|
|
1366
|
+
}
|
|
1367
|
+
return {
|
|
1368
|
+
posed: session.pose(),
|
|
1369
|
+
dial: {
|
|
1370
|
+
value,
|
|
1371
|
+
driven: u,
|
|
1372
|
+
beyondLimit: runaway ? asked : null,
|
|
1373
|
+
applied: got.applied,
|
|
1374
|
+
wanted,
|
|
1375
|
+
// ⚠️ A drive the bound refused is unreachable WHATEVER the runtime then
|
|
1376
|
+
// landed on. Nothing was posed where the mapping asked, so a time that
|
|
1377
|
+
// happens to match is a coincidence of the setup pose and not a measurement.
|
|
1378
|
+
unreachable: runaway || !(Math.abs(got.applied - wanted) <= DIAL_TIME_EPSILON),
|
|
1379
|
+
},
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/** One posed time, measured — and the two world arrays it was measured from. */
|
|
1384
|
+
interface PosedFrame {
|
|
1385
|
+
time: number;
|
|
1386
|
+
posed: SurveyPose;
|
|
1387
|
+
/** The mesh as the runtime deformed it there. */
|
|
1388
|
+
deformed: Float32Array;
|
|
1389
|
+
/** The same bones with the deform cleared. The denominator, by construction 1.000. */
|
|
1390
|
+
plain: Float32Array;
|
|
1391
|
+
/** `triangleAreas(plain)`, kept so the span scan does not take it a second time. */
|
|
1392
|
+
plainAreas: number[];
|
|
1393
|
+
measure: DeformFrameMeasure;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Everything this file says about one attachment at whatever time a skeleton is
|
|
1398
|
+
* already posed at.
|
|
1399
|
+
*
|
|
1400
|
+
* ⚠️ It **clears the slot's deform array** to take the plain side, so the
|
|
1401
|
+
* skeleton it is handed is spent for any purpose that wanted the runtime's own
|
|
1402
|
+
* deform back. The span scan below relies on that: it writes its own arrays into
|
|
1403
|
+
* the emptied slot to evaluate the two keys' geometry at one pose.
|
|
1404
|
+
*/
|
|
1405
|
+
function measurePosed(
|
|
1406
|
+
posed: SurveyPose,
|
|
1407
|
+
time: number,
|
|
1408
|
+
slotIndex: number,
|
|
1409
|
+
attachment: SurveyMesh,
|
|
1410
|
+
triangles: ArrayLike<number>,
|
|
1411
|
+
reach: DeformReach,
|
|
1412
|
+
dial: DeformDial | null,
|
|
1413
|
+
): PosedFrame {
|
|
1414
|
+
const count = attachment.worldVerticesLength;
|
|
1415
|
+
// Read BEFORE the deform is cleared below, and off the same posed skeleton:
|
|
1416
|
+
// what the slot shows here and at what alpha is the other half of what this
|
|
1417
|
+
// frame does (issue #401).
|
|
1418
|
+
const draw = drawOfKey(posed, slotIndex, attachment);
|
|
1419
|
+
const deformed = posed.deformed(slotIndex, attachment);
|
|
1420
|
+
// The same bones, with the deform taken away. `computeWorldVertices` reads the
|
|
1421
|
+
// array off the slot, so emptying it is the whole control.
|
|
1422
|
+
const plain = posed.plain(slotIndex, attachment);
|
|
1423
|
+
|
|
1424
|
+
const before = triangleAreas(plain, triangles);
|
|
1425
|
+
const after = triangleAreas(deformed, triangles);
|
|
1426
|
+
const band = areaBand(before, plain, deformed);
|
|
1427
|
+
|
|
1428
|
+
let moved = 0;
|
|
1429
|
+
let maxDisplacement = 0;
|
|
1430
|
+
let maxDisplacementVertex = -1;
|
|
1431
|
+
for (let v = 0; v * 2 + 1 < count; v++) {
|
|
1432
|
+
const dx = deformed[v * 2] - plain[v * 2];
|
|
1433
|
+
const dy = deformed[v * 2 + 1] - plain[v * 2 + 1];
|
|
1434
|
+
if (dx === 0 && dy === 0) continue;
|
|
1435
|
+
moved++;
|
|
1436
|
+
const distance = Math.hypot(dx, dy);
|
|
1437
|
+
if (distance > maxDisplacement) {
|
|
1438
|
+
maxDisplacement = distance;
|
|
1439
|
+
maxDisplacementVertex = v;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
const reversed: DeformReversal[] = [];
|
|
1444
|
+
let collapsed = 0;
|
|
1445
|
+
let degenerate = 0;
|
|
1446
|
+
let areaRatioMin: DeformExtreme | null = null;
|
|
1447
|
+
let areaRatioMax: DeformExtreme | null = null;
|
|
1448
|
+
let stretchMax: DeformExtreme | null = null;
|
|
1449
|
+
let stretchMin: DeformExtreme | null = null;
|
|
1450
|
+
for (let t = 0; t < before.length; t++) {
|
|
1451
|
+
// A triangle with no area at the cleared pose has no winding to keep and no
|
|
1452
|
+
// map to take singular values of.
|
|
1453
|
+
if (Math.abs(before[t]) <= band) {
|
|
1454
|
+
degenerate++;
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
const ratio = after[t] / before[t];
|
|
1458
|
+
if (areaRatioMin === null || ratio < areaRatioMin.value) areaRatioMin = { triangle: t, value: ratio };
|
|
1459
|
+
if (areaRatioMax === null || ratio > areaRatioMax.value) areaRatioMax = { triangle: t, value: ratio };
|
|
1460
|
+
const stretch = stretchSingularValues(plain, deformed, triangles, t);
|
|
1461
|
+
if (stretch !== null) {
|
|
1462
|
+
if (stretchMax === null || stretch.max > stretchMax.value) stretchMax = { triangle: t, value: stretch.max };
|
|
1463
|
+
if (stretchMin === null || stretch.min < stretchMin.value) stretchMin = { triangle: t, value: stretch.min };
|
|
1464
|
+
}
|
|
1465
|
+
// A triangle the key collapses ONTO zero has been pinched rather than turned
|
|
1466
|
+
// over — a real idiom, counted and never a bar. Its ratio and its stretch are
|
|
1467
|
+
// kept above, because a triangle crushed to nothing IS the worst compression
|
|
1468
|
+
// on that key and hiding it would flatter the report.
|
|
1469
|
+
if (Math.abs(after[t]) <= band) {
|
|
1470
|
+
collapsed++;
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
if (Math.sign(before[t]) !== Math.sign(after[t])) {
|
|
1474
|
+
reversed.push({
|
|
1475
|
+
triangle: t,
|
|
1476
|
+
ids: [triangles[t * 3], triangles[t * 3 + 1], triangles[t * 3 + 2]],
|
|
1477
|
+
before: before[t],
|
|
1478
|
+
after: after[t],
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return {
|
|
1483
|
+
time,
|
|
1484
|
+
posed,
|
|
1485
|
+
deformed,
|
|
1486
|
+
plain,
|
|
1487
|
+
plainAreas: before,
|
|
1488
|
+
measure: {
|
|
1489
|
+
vertices: count / 2,
|
|
1490
|
+
moved,
|
|
1491
|
+
maxDisplacement,
|
|
1492
|
+
maxDisplacementVertex,
|
|
1493
|
+
triangles: before.length,
|
|
1494
|
+
reversed,
|
|
1495
|
+
collapsed,
|
|
1496
|
+
degenerate,
|
|
1497
|
+
areaRatioMin,
|
|
1498
|
+
areaRatioMax,
|
|
1499
|
+
stretchMax,
|
|
1500
|
+
stretchMin,
|
|
1501
|
+
band,
|
|
1502
|
+
draw,
|
|
1503
|
+
reach,
|
|
1504
|
+
dial,
|
|
1505
|
+
},
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// ---------------------------------------------------------------------------
|
|
1510
|
+
// Between two keys — issue #403
|
|
1511
|
+
// ---------------------------------------------------------------------------
|
|
1512
|
+
|
|
1513
|
+
/**
|
|
1514
|
+
* A closed interval, in whatever the caller is measuring. Empty when `hi < lo`.
|
|
1515
|
+
*/
|
|
1516
|
+
interface Interval {
|
|
1517
|
+
lo: number;
|
|
1518
|
+
hi: number;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/** One straight piece of the curve the runtime reads a span's fraction off. */
|
|
1522
|
+
interface CurveLeg {
|
|
1523
|
+
t0: number;
|
|
1524
|
+
p0: number;
|
|
1525
|
+
t1: number;
|
|
1526
|
+
p1: number;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* The runtime's own interpolation fraction over one span, as a polyline in
|
|
1531
|
+
* `(time, fraction)`.
|
|
1532
|
+
*
|
|
1533
|
+
* ⭐ **The points the runtime stores, not the cubic.**
|
|
1534
|
+
* `DeformTimeline.getCurvePercent` evaluates a bezier by walking the points the
|
|
1535
|
+
* parser sampled into its curve storage and interpolating *linearly* between
|
|
1536
|
+
* them — so the polyline below is not an approximation of what the runtime does,
|
|
1537
|
+
* it is what the runtime does. A curve that overshoots (a fraction below 0 or
|
|
1538
|
+
* above 1, which the format allows and `back`-style easings produce) is therefore
|
|
1539
|
+
* inside this rather than assumed away. Which reader supplies the points is the
|
|
1540
|
+
* structure's (`SurveyCurve`, issue #1019): spine-core's reads that storage
|
|
1541
|
+
* (`curveStorage`); the model document's computes the same nine from the key's
|
|
1542
|
+
* handles with the core's deform curve (`./deformstructure.ts`), measured equal
|
|
1543
|
+
* point for point on every corpus Bézier.
|
|
1544
|
+
*/
|
|
1545
|
+
function curveLegs(timeline: SurveyDeformTimeline, frame: number): { kind: DeformSpanCurve; legs: CurveLeg[] } {
|
|
1546
|
+
const t0 = timeline.frames[frame];
|
|
1547
|
+
const t1 = timeline.frames[frame + 1];
|
|
1548
|
+
const curve = timeline.curve(frame);
|
|
1549
|
+
// STEPPED: `getCurvePercent` returns a flat 0 across the whole span, so the
|
|
1550
|
+
// runtime holds the earlier key's geometry and interpolates nothing.
|
|
1551
|
+
if (curve.kind === 'stepped') return { kind: 'stepped', legs: [{ t0, p0: 0, t1, p1: 0 }] };
|
|
1552
|
+
if (curve.kind === 'linear') return { kind: 'linear', legs: [{ t0, p0: 0, t1, p1: 1 }] };
|
|
1553
|
+
// BEZIER: nine sampled points are stored; the runtime ramps into the first
|
|
1554
|
+
// from (t0, 0) and out of the last to (t1, 1), which is the two legs added
|
|
1555
|
+
// either side.
|
|
1556
|
+
const points = curve.points;
|
|
1557
|
+
const legs: CurveLeg[] = [];
|
|
1558
|
+
let x = t0;
|
|
1559
|
+
let y = 0;
|
|
1560
|
+
for (let i = 0; i < BEZIER_POINTS * 2; i += 2) {
|
|
1561
|
+
legs.push({ t0: x, p0: y, t1: points[i], p1: points[i + 1] });
|
|
1562
|
+
x = points[i];
|
|
1563
|
+
y = points[i + 1];
|
|
1564
|
+
}
|
|
1565
|
+
legs.push({ t0: x, p0: y, t1, p1: 1 });
|
|
1566
|
+
return { kind: 'bezier', legs };
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/** Points `CurveTimeline.setBezier` stores per curve — `BEZIER_SIZE / 2`. */
|
|
1570
|
+
export const BEZIER_POINTS = 9;
|
|
1571
|
+
|
|
1572
|
+
/** The fractions this span's curve actually reaches, overshoot included. */
|
|
1573
|
+
function reachedFractions(legs: readonly CurveLeg[]): Interval {
|
|
1574
|
+
let lo = Infinity;
|
|
1575
|
+
let hi = -Infinity;
|
|
1576
|
+
for (const leg of legs) {
|
|
1577
|
+
lo = Math.min(lo, leg.p0, leg.p1);
|
|
1578
|
+
hi = Math.max(hi, leg.p0, leg.p1);
|
|
1579
|
+
}
|
|
1580
|
+
return { lo, hi };
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/**
|
|
1584
|
+
* The times at which the curve's fraction is inside `window`, as intervals.
|
|
1585
|
+
*
|
|
1586
|
+
* Every leg is straight, so each contributes one interval and the answer is
|
|
1587
|
+
* exact — a non-monotone curve simply contributes more than one.
|
|
1588
|
+
*/
|
|
1589
|
+
function timesAtFractions(legs: readonly CurveLeg[], window: Interval): Interval[] {
|
|
1590
|
+
const out: Interval[] = [];
|
|
1591
|
+
for (const leg of legs) {
|
|
1592
|
+
if (leg.p0 === leg.p1) {
|
|
1593
|
+
if (leg.p0 >= window.lo && leg.p0 <= window.hi) out.push({ lo: leg.t0, hi: leg.t1 });
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
const at = (p: number): number => leg.t0 + ((leg.t1 - leg.t0) * (p - leg.p0)) / (leg.p1 - leg.p0);
|
|
1597
|
+
const a = at(window.lo);
|
|
1598
|
+
const b = at(window.hi);
|
|
1599
|
+
const lo = Math.max(Math.min(leg.t0, leg.t1), Math.min(a, b));
|
|
1600
|
+
const hi = Math.min(Math.max(leg.t0, leg.t1), Math.max(a, b));
|
|
1601
|
+
if (hi >= lo) out.push({ lo, hi });
|
|
1602
|
+
}
|
|
1603
|
+
return out;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
/** The fraction the curve is at, at one time. */
|
|
1607
|
+
function fractionAt(legs: readonly CurveLeg[], time: number): number {
|
|
1608
|
+
for (const leg of legs) {
|
|
1609
|
+
if (time < Math.min(leg.t0, leg.t1) || time > Math.max(leg.t0, leg.t1)) continue;
|
|
1610
|
+
if (leg.t1 === leg.t0) return leg.p0;
|
|
1611
|
+
return leg.p0 + ((leg.p1 - leg.p0) * (time - leg.t0)) / (leg.t1 - leg.t0);
|
|
1612
|
+
}
|
|
1613
|
+
return legs[legs.length - 1].p1;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
/** Overlapping intervals folded into the maximal ones they cover. */
|
|
1617
|
+
function mergeIntervals(intervals: readonly Interval[]): Interval[] {
|
|
1618
|
+
const sorted = [...intervals].filter((i) => i.hi > i.lo).sort((a, b) => a.lo - b.lo || a.hi - b.hi);
|
|
1619
|
+
const out: Interval[] = [];
|
|
1620
|
+
for (const interval of sorted) {
|
|
1621
|
+
const last = out[out.length - 1];
|
|
1622
|
+
if (last !== undefined && interval.lo <= last.hi) last.hi = Math.max(last.hi, interval.hi);
|
|
1623
|
+
else out.push({ ...interval });
|
|
1624
|
+
}
|
|
1625
|
+
return out;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/**
|
|
1629
|
+
* Where a triangle's area has the **wrong sign** over one span, in the
|
|
1630
|
+
* interpolation fraction — the closed form, and the whole reason this scan needs
|
|
1631
|
+
* no subdivision count.
|
|
1632
|
+
*
|
|
1633
|
+
* ## The arithmetic, in full, because it is four lines
|
|
1634
|
+
*
|
|
1635
|
+
* `DeformTimeline.applyToSlot` writes `v1 + (v2 − v1)·p` into the slot's deform
|
|
1636
|
+
* array, and a world vertex is an **affine** function of that array at a fixed
|
|
1637
|
+
* pose — unweighted, the array *is* the local positions; weighted, each pair is
|
|
1638
|
+
* added in a bone's bind space and summed with fixed weights. So over one span
|
|
1639
|
+
* every vertex travels a straight line, `W(p) = A + p·(B − A)`, and a triangle's
|
|
1640
|
+
* doubled signed area is a cross product of two such lines:
|
|
1641
|
+
*
|
|
1642
|
+
* 2A(p) = (e₁ + p·d₁) × (e₂ + p·d₂)
|
|
1643
|
+
* = e₁×e₂ + p·(e₁×d₂ + d₁×e₂) + p²·(d₁×d₂)
|
|
1644
|
+
*
|
|
1645
|
+
* — a **quadratic in p, exactly**, with `e` the edges at `p = 0` and `d` the
|
|
1646
|
+
* edges of the displacement to `p = 1`. The reversal condition is therefore two
|
|
1647
|
+
* roots of a quadratic, not a search, and there is no sample spacing to defend.
|
|
1648
|
+
*
|
|
1649
|
+
* ⭐ Compare `turnCeiling` in [`src/depth.ts`](src/depth.ts), which is the same
|
|
1650
|
+
* move on the other side of the wall: there a yaw makes the area linear in
|
|
1651
|
+
* `cos t` and `sin t` and the fold angle is `atan(A₀/A_axis)`; here the runtime's
|
|
1652
|
+
* own interpolation makes it quadratic in `p` and the fold fraction is a root.
|
|
1653
|
+
* Both replace "build, read the refusal, guess again" with arithmetic.
|
|
1654
|
+
*
|
|
1655
|
+
* 🚨 **What is NOT closed-form is the pose.** `A(p)` above holds the bones still.
|
|
1656
|
+
* The bones move across a span too, and on a WEIGHTED mesh their motion changes
|
|
1657
|
+
* the map from offsets to world — so this is evaluated at both of the span's own
|
|
1658
|
+
* key poses and the union taken, and whatever it names is then *measured* at the
|
|
1659
|
+
* real posed time before anything is refused. On an unweighted mesh the question
|
|
1660
|
+
* does not arise at all: one bone matrix multiplies every vertex, so its
|
|
1661
|
+
* determinant factors out of both sides of the comparison and the reversal is a
|
|
1662
|
+
* property of the offsets alone.
|
|
1663
|
+
*/
|
|
1664
|
+
function wrongSignFractions(
|
|
1665
|
+
c0: number,
|
|
1666
|
+
c1: number,
|
|
1667
|
+
c2: number,
|
|
1668
|
+
plainArea: number,
|
|
1669
|
+
band: number,
|
|
1670
|
+
reach: Interval,
|
|
1671
|
+
): Interval[] {
|
|
1672
|
+
// g(p) < 0 is "reversed by more than the band", with the sign folded in so the
|
|
1673
|
+
// question is the same one whichever way the mesh is wound.
|
|
1674
|
+
const s = plainArea > 0 ? 1 : -1;
|
|
1675
|
+
const a = s * c2;
|
|
1676
|
+
const b = s * c1;
|
|
1677
|
+
const c = s * c0 + band;
|
|
1678
|
+
const clip = (lo: number, hi: number): Interval[] => {
|
|
1679
|
+
const out = { lo: Math.max(lo, reach.lo), hi: Math.min(hi, reach.hi) };
|
|
1680
|
+
return out.hi >= out.lo ? [out] : [];
|
|
1681
|
+
};
|
|
1682
|
+
if (a === 0) {
|
|
1683
|
+
// Degenerate to a straight line, which is what a deform that moves one axis
|
|
1684
|
+
// only comes to — a `yaw` leaves y alone, so its areas are affine in `p` and
|
|
1685
|
+
// two unfolded keys cannot fold between them at all (`DW14`).
|
|
1686
|
+
if (b === 0) return c < 0 ? [{ ...reach }] : [];
|
|
1687
|
+
const root = -c / b;
|
|
1688
|
+
return b > 0 ? clip(-Infinity, root) : clip(root, Infinity);
|
|
1689
|
+
}
|
|
1690
|
+
const discriminant = b * b - 4 * a * c;
|
|
1691
|
+
// No crossing: the sign of `a` is the sign of `g` everywhere.
|
|
1692
|
+
if (discriminant <= 0) return a < 0 ? [{ ...reach }] : [];
|
|
1693
|
+
const sq = Math.sqrt(discriminant);
|
|
1694
|
+
const left = Math.min((-b - sq) / (2 * a), (-b + sq) / (2 * a));
|
|
1695
|
+
const right = Math.max((-b - sq) / (2 * a), (-b + sq) / (2 * a));
|
|
1696
|
+
// ⚠️ An upward parabola is wrong-signed BETWEEN its roots; a downward one is
|
|
1697
|
+
// wrong-signed OUTSIDE them, and those are two disjoint windows with a
|
|
1698
|
+
// correctly-wound middle. Merging them into one — which this did until it was
|
|
1699
|
+
// read back — would put the probe's midpoint in that middle and report a fold
|
|
1700
|
+
// nothing reproduced.
|
|
1701
|
+
return a > 0 ? clip(left, right) : [...clip(-Infinity, left), ...clip(right, Infinity)];
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/*
|
|
1705
|
+
* The times at which the slot's **visibility** can change across a span.
|
|
1706
|
+
*
|
|
1707
|
+
* ⚠️ This is the half of issue #403 that is about the fade rather than the fold,
|
|
1708
|
+
* and getting it wrong in either direction is a defect: read the alpha only at
|
|
1709
|
+
* the keys and a correct rig that fades out over the fold is refused (undoing
|
|
1710
|
+
* issue #401); probe one arbitrary time inside the fold window and a fold that is
|
|
1711
|
+
* drawn for only part of that window is missed.
|
|
1712
|
+
*
|
|
1713
|
+
* ⭐ The way out needs no threshold. A slot's alpha and its attachment are
|
|
1714
|
+
* piecewise functions of time whose pieces are **the slot timelines' own key
|
|
1715
|
+
* times**, so `alpha == 0` can start or stop only there. Splitting the fold
|
|
1716
|
+
* window at those times leaves pieces on which "does this draw?" has one answer,
|
|
1717
|
+
* and the probe inside a piece is representative of the whole piece.
|
|
1718
|
+
*
|
|
1719
|
+
* Every timeline filed under this slot, or under any other slot the deform
|
|
1720
|
+
* reaches (`timelineSlots`), counts — `SurveyAnimation.slotKeyTimes`, which each
|
|
1721
|
+
* reader answers (`visibilityKeyTimes` below for spine-core's; the model
|
|
1722
|
+
* document's slot and attachment timelines for the core's).
|
|
1723
|
+
*/
|
|
1724
|
+
|
|
1725
|
+
/**
|
|
1726
|
+
* Scan the interval between two consecutive deform keys (issue #403).
|
|
1727
|
+
*
|
|
1728
|
+
* ## What it does, in order
|
|
1729
|
+
*
|
|
1730
|
+
* 1. **Solve.** At each of the span's two key poses, take every triangle's
|
|
1731
|
+
* quadratic (`wrongSignFractions`) and collect the fractions at which it is
|
|
1732
|
+
* reversed. Nothing is measured here and nothing is refused here.
|
|
1733
|
+
* 2. **Convert.** Map those fractions to times through the runtime's own curve
|
|
1734
|
+
* polyline (`timesAtFractions`), keep only what is **strictly inside** the
|
|
1735
|
+
* span — the two ends are the keys, and the key survey owns those — and merge
|
|
1736
|
+
* the overlaps.
|
|
1737
|
+
* 3. **Split.** Cut each merged window at the slot's own visibility key times, so
|
|
1738
|
+
* no piece straddles a change in what is drawn.
|
|
1739
|
+
* 4. **Measure.** Pose the skeleton at one time inside each piece and take this
|
|
1740
|
+
* file's ordinary measurement there, alpha included, in time order. Stop at
|
|
1741
|
+
* the first piece that comes back with a reversal on a frame that draws.
|
|
1742
|
+
*
|
|
1743
|
+
* ⇒ Nothing is refused on a prediction. What A39 reads is step 4's measurement,
|
|
1744
|
+
* at a real time, through the real runtime — the closed form only decides *where
|
|
1745
|
+
* to look*, which is exactly the part a subdivision count would have been
|
|
1746
|
+
* guessing at.
|
|
1747
|
+
*
|
|
1748
|
+
* ## Cost — measured, because the multiplier had to be chosen rather than assumed
|
|
1749
|
+
*
|
|
1750
|
+
* Step 1 is **one** extra `computeWorldVertices` per span per anchor (the other
|
|
1751
|
+
* end of the line is the anchor key's own `deformed` array) plus three cross
|
|
1752
|
+
* products per triangle; steps 2–3 are interval arithmetic, `O(triangles)`.
|
|
1753
|
+
* **Step 4 costs nothing at all on a span nothing is predicted in**, which is
|
|
1754
|
+
* every span of every green rig, and that is what keeps the whole figure a
|
|
1755
|
+
* fraction rather than a multiple. Against the same survey with this function
|
|
1756
|
+
* switched off, same process, alternated, on the `2026-09-05-density` study's
|
|
1757
|
+
* own grid ladder and on the four gallery rigs that carry a deform timeline:
|
|
1758
|
+
*
|
|
1759
|
+
* | fixture | keys | spans | triangle samples | keys only | + spans |
|
|
1760
|
+
* | --- | ---: | ---: | ---: | ---: | ---: |
|
|
1761
|
+
* | `grid-129`, weighted, 32,768 triangles | 4 | 3 | 131,072 | 5.5 ms | **8.1 ms (×1.47)** |
|
|
1762
|
+
* | `grid-97`, weighted, 18,432 triangles | 4 | 3 | 73,728 | 3.4 ms | 4.7 ms (×1.40) |
|
|
1763
|
+
* | `gallery/flex`, weighted, 75 triangles | 8 | 6 | 600 | 0.16 ms | 0.30 ms (×1.9) |
|
|
1764
|
+
* | `gallery/nod` | 36 | 30 | 624 | 0.34 ms | 0.46 ms (×1.37) |
|
|
1765
|
+
* | `gallery/portrait`, unweighted | 8 | 6 | 192 | 0.22 ms | 0.25 ms (×1.12) |
|
|
1766
|
+
*
|
|
1767
|
+
* ⇒ **The survey costs about half as much again**, and where in that range a rig
|
|
1768
|
+
* lands is set by two things and not by the triangle count: how many spans it
|
|
1769
|
+
* has per key (a timeline of two keys has one span; `nod`'s 36 keys have 30),
|
|
1770
|
+
* and whether the mesh is weighted — an unweighted one takes a single anchor for
|
|
1771
|
+
* the reason above and a much cheaper `computeWorldVertices` with it. ⚠️ Read
|
|
1772
|
+
* the ratio at `grid-129` and treat the sub-millisecond rows as noisy: they move
|
|
1773
|
+
* ±15% between runs, which is the caveat the density study's README already
|
|
1774
|
+
* carries about single readings. On a rig that DOES fold, add one posed
|
|
1775
|
+
* measurement — the cost of one key — per predicted window, and the build is
|
|
1776
|
+
* being refused anyway.
|
|
1777
|
+
*/
|
|
1778
|
+
function scanDeformSpan(
|
|
1779
|
+
anim: SurveyAnimation,
|
|
1780
|
+
timeline: SurveyDeformTimeline,
|
|
1781
|
+
attachment: SurveyMesh,
|
|
1782
|
+
triangles: ArrayLike<number>,
|
|
1783
|
+
named: { animation: string; skin: string; slot: string; attachment: string; placeholder: string },
|
|
1784
|
+
/** The index of the key the span starts at — `frame + 1` is the one it ends at. */
|
|
1785
|
+
frame: number,
|
|
1786
|
+
from: PosedFrame,
|
|
1787
|
+
to: PosedFrame,
|
|
1788
|
+
/**
|
|
1789
|
+
* The same frame the two keys were posed in, at a time between them (#407).
|
|
1790
|
+
* A probe taken on a track while a slider applies the animation elsewhere is
|
|
1791
|
+
* the frame that never occurs, one interpolation step further in.
|
|
1792
|
+
*/
|
|
1793
|
+
poseFrame: (time: number) => PoseOfFrame,
|
|
1794
|
+
): DeformSpan {
|
|
1795
|
+
const { kind, legs } = curveLegs(timeline, frame);
|
|
1796
|
+
const span: DeformSpan = {
|
|
1797
|
+
...named,
|
|
1798
|
+
reach: from.measure.reach,
|
|
1799
|
+
fromKey: frame,
|
|
1800
|
+
toKey: frame + 1,
|
|
1801
|
+
fromTime: from.time,
|
|
1802
|
+
toTime: to.time,
|
|
1803
|
+
curve: kind,
|
|
1804
|
+
predicted: 0,
|
|
1805
|
+
probed: [],
|
|
1806
|
+
fold: null,
|
|
1807
|
+
notDrawn: 0,
|
|
1808
|
+
unconfirmed: false,
|
|
1809
|
+
};
|
|
1810
|
+
const reach = reachedFractions(legs);
|
|
1811
|
+
const v1 = timeline.vertices(frame);
|
|
1812
|
+
const v2 = timeline.vertices(frame + 1);
|
|
1813
|
+
if (!v1 || !v2) return span;
|
|
1814
|
+
|
|
1815
|
+
const windows: Interval[] = [];
|
|
1816
|
+
const flagged = new Set<number>();
|
|
1817
|
+
// ⭐ **One anchor on an unweighted attachment, and it costs nothing to be
|
|
1818
|
+
// sure of.** Every vertex of one is transformed by the SAME bone matrix `M`,
|
|
1819
|
+
// so a triangle's world area is `det M` times its local area on both sides of
|
|
1820
|
+
// the comparison and the factor cancels: whether the deform reverses a
|
|
1821
|
+
// winding is then a property of the offsets alone, identical at every pose,
|
|
1822
|
+
// and the second anchor would recompute the same answer. A WEIGHTED
|
|
1823
|
+
// attachment blends a different matrix per vertex, `det` does not factor out,
|
|
1824
|
+
// and the fixed-pose quadratic is only exact where the bones hold still — so
|
|
1825
|
+
// both of the span's own key poses are taken and the union used. `DW17` is
|
|
1826
|
+
// the control for the claim: a bone rotation across the span moves nothing
|
|
1827
|
+
// about where an unweighted mesh is found to fold.
|
|
1828
|
+
for (const anchor of attachment.weighted ? [from, to] : [from]) {
|
|
1829
|
+
// The two ends of the straight line every vertex travels, evaluated at THIS
|
|
1830
|
+
// anchor's bones. `measurePosed` has already emptied the slot's deform array
|
|
1831
|
+
// to take its plain side, so writing into it is how the two are posed.
|
|
1832
|
+
//
|
|
1833
|
+
// ⭐ Half of these four are already in hand. At a key's own time the
|
|
1834
|
+
// runtime's interpolation fraction is 0, so `applyToSlot` copies that key's
|
|
1835
|
+
// array verbatim — which makes the anchor's own `deformed` exactly the end
|
|
1836
|
+
// of the line that belongs to it, provided the slot was showing the mesh
|
|
1837
|
+
// there for the runtime to have applied anything at all.
|
|
1838
|
+
const own = anchor.measure.draw.showsThisMesh;
|
|
1839
|
+
const a =
|
|
1840
|
+
own && anchor === from ? from.deformed : anchor.posed.withDeform(timeline.slotIndex, attachment, v1);
|
|
1841
|
+
const b =
|
|
1842
|
+
own && anchor === to ? to.deformed : anchor.posed.withDeform(timeline.slotIndex, attachment, v2);
|
|
1843
|
+
// The anchor's own plain areas, taken once when it was measured as a key —
|
|
1844
|
+
// the same numbers, so the span cannot disagree with the key about which
|
|
1845
|
+
// triangles have a winding to keep.
|
|
1846
|
+
const plainAreas = anchor.plainAreas;
|
|
1847
|
+
const band = areaBand(plainAreas, anchor.plain, a, b);
|
|
1848
|
+
for (let t = 0; t < plainAreas.length; t++) {
|
|
1849
|
+
if (Math.abs(plainAreas[t]) <= band) continue; // no winding to keep
|
|
1850
|
+
const i0 = triangles[t * 3] * 2;
|
|
1851
|
+
const i1 = triangles[t * 3 + 1] * 2;
|
|
1852
|
+
const i2 = triangles[t * 3 + 2] * 2;
|
|
1853
|
+
const e1x = a[i1] - a[i0];
|
|
1854
|
+
const e1y = a[i1 + 1] - a[i0 + 1];
|
|
1855
|
+
const e2x = a[i2] - a[i0];
|
|
1856
|
+
const e2y = a[i2 + 1] - a[i0 + 1];
|
|
1857
|
+
const d1x = b[i1] - b[i0] - e1x;
|
|
1858
|
+
const d1y = b[i1 + 1] - b[i0 + 1] - e1y;
|
|
1859
|
+
const d2x = b[i2] - b[i0] - e2x;
|
|
1860
|
+
const d2y = b[i2 + 1] - b[i0 + 1] - e2y;
|
|
1861
|
+
const c0 = 0.5 * (e1x * e2y - e2x * e1y);
|
|
1862
|
+
const c1 = 0.5 * (e1x * d2y - d2x * e1y + d1x * e2y - e2x * d1y);
|
|
1863
|
+
const c2 = 0.5 * (d1x * d2y - d2x * d1y);
|
|
1864
|
+
for (const wrong of wrongSignFractions(c0, c1, c2, plainAreas[t], band, reach)) {
|
|
1865
|
+
const inside = timesAtFractions(legs, wrong)
|
|
1866
|
+
.map((i) => ({ lo: Math.max(i.lo, from.time), hi: Math.min(i.hi, to.time) }))
|
|
1867
|
+
.filter((i) => i.hi > i.lo);
|
|
1868
|
+
if (inside.length === 0) continue;
|
|
1869
|
+
flagged.add(t);
|
|
1870
|
+
windows.push(...inside);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
span.predicted = flagged.size;
|
|
1875
|
+
if (windows.length === 0) return span;
|
|
1876
|
+
|
|
1877
|
+
// Split each merged window at the slot's own visibility keys, so no piece
|
|
1878
|
+
// straddles a change in what is drawn. Every point of a merged window is
|
|
1879
|
+
// inside at least one triangle's window, so its middle is a fold and not a gap.
|
|
1880
|
+
const slots = new Set<number>([timeline.slotIndex, ...attachment.timelineSlots]);
|
|
1881
|
+
const cuts = anim.slotKeyTimes(slots);
|
|
1882
|
+
const pieces: Interval[] = [];
|
|
1883
|
+
for (const window of mergeIntervals(windows)) {
|
|
1884
|
+
const inner = [...new Set(cuts.filter((t) => t > window.lo && t < window.hi))].sort((x, y) => x - y);
|
|
1885
|
+
let lo = window.lo;
|
|
1886
|
+
for (const cut of [...inner, window.hi]) {
|
|
1887
|
+
if (cut > lo) pieces.push({ lo, hi: cut });
|
|
1888
|
+
lo = cut;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
for (const piece of pieces) {
|
|
1892
|
+
const time = (piece.lo + piece.hi) / 2;
|
|
1893
|
+
const at = poseFrame(time);
|
|
1894
|
+
// A probe the dial cannot select is no probe: it would be measuring some
|
|
1895
|
+
// other time. Both keys bounding this span were reachable, and the map from
|
|
1896
|
+
// dial to time is affine, so this is a shape nothing in the corpus reaches —
|
|
1897
|
+
// which is exactly why it must not be recorded as a probe that ran.
|
|
1898
|
+
if (at.dial?.unreachable === true) continue;
|
|
1899
|
+
span.probed.push(time);
|
|
1900
|
+
const probe = measurePosed(
|
|
1901
|
+
at.posed,
|
|
1902
|
+
time,
|
|
1903
|
+
timeline.slotIndex,
|
|
1904
|
+
attachment,
|
|
1905
|
+
triangles,
|
|
1906
|
+
from.measure.reach,
|
|
1907
|
+
at.dial,
|
|
1908
|
+
);
|
|
1909
|
+
if (probe.measure.reversed.length === 0) continue;
|
|
1910
|
+
if (probe.measure.draw.blank !== null) {
|
|
1911
|
+
span.notDrawn++;
|
|
1912
|
+
continue;
|
|
1913
|
+
}
|
|
1914
|
+
span.fold = { time, percent: fractionAt(legs, time), measure: probe.measure };
|
|
1915
|
+
return span;
|
|
1916
|
+
}
|
|
1917
|
+
span.unconfirmed = span.fold === null && span.notDrawn === 0;
|
|
1918
|
+
return span;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
/**
|
|
1922
|
+
* Whether this key's mesh draws any pixels at this key's own time, and if not,
|
|
1923
|
+
* the sentence that says why (issue #401).
|
|
1924
|
+
*
|
|
1925
|
+
* ## The two ways a key draws nothing, and the one bar
|
|
1926
|
+
*
|
|
1927
|
+
* - **The slot shows something else** — or nothing. The runtime then applies no
|
|
1928
|
+
* deform to the slot at all, so the mesh is neither on screen nor deformed.
|
|
1929
|
+
* - **The slot's alpha is exactly 0.** Every blend mode the format has multiplies
|
|
1930
|
+
* the source by that alpha, so no channel of the destination moves.
|
|
1931
|
+
*
|
|
1932
|
+
* 🚨 **Exactly 0.** Not "small", not "below a floor". At 0.5 a reversed triangle
|
|
1933
|
+
* is visible at half strength and A39 goes on refusing it, with the alpha in the
|
|
1934
|
+
* message.
|
|
1935
|
+
*
|
|
1936
|
+
* ⚠️ **A deform reaches more than its own slot.** `Attachment.timelineSlots` (4.3)
|
|
1937
|
+
* lists the other slots a deform timeline is applied to, and spine-core applies
|
|
1938
|
+
* it to every one of them. So a mesh the timeline's own slot has swapped away may
|
|
1939
|
+
* still be drawn — and folded — in another slot, and the exemption is refused
|
|
1940
|
+
* unless NONE of the slots the deform reaches draws it. rigc emits no
|
|
1941
|
+
* `timelineSlots` of its own, so on a rigc-compiled skeleton the list is empty
|
|
1942
|
+
* and this loop runs zero times; it is here because a foreign skeleton reaching
|
|
1943
|
+
* `explain` is exactly where a silent false green would be unnoticeable.
|
|
1944
|
+
*
|
|
1945
|
+
* 🚨 **Every sentence here is about ONE skin — the one the skeleton is wearing**
|
|
1946
|
+
* (issue #583), which `under` names off `Skeleton.skin` rather than off whatever
|
|
1947
|
+
* the caller believes it set. That matters most in the loop below: a slot the
|
|
1948
|
+
* deform reaches through `timelineSlots` carries a LINKED mesh, a separate
|
|
1949
|
+
* attachment object that lives in a skin of its own, and `setSkin` dresses the
|
|
1950
|
+
* whole skeleton at once — so a linked copy whose skin is not the one worn here
|
|
1951
|
+
* resolves to nothing and `shownAt` reports alpha 0 for it, exactly as the
|
|
1952
|
+
* runtime would with that same skin on.
|
|
1953
|
+
*
|
|
1954
|
+
* ⛔ Not a reason to pose the key again under each of those other skins. Which
|
|
1955
|
+
* skin is worn is the consumer's, and a measurement taken under a skin the
|
|
1956
|
+
* timeline is not keyed on would be rigc composing a scene to make its own gate
|
|
1957
|
+
* green. What it must not do is leave the reader guessing which dress the
|
|
1958
|
+
* verdict was taken in, so the skin is in the sentence.
|
|
1959
|
+
*/
|
|
1960
|
+
function drawOfKey(posed: SurveyPose, slotIndex: number, attachment: SurveyMesh): DeformKeyDraw {
|
|
1961
|
+
const own = posed.shownAt(slotIndex, attachment);
|
|
1962
|
+
const under = posed.under();
|
|
1963
|
+
const draw = { shown: own.shown, showsThisMesh: own.showsThisMesh, alpha: own.alpha, under };
|
|
1964
|
+
// 🔒 On the "shows something else" branch ONLY, because that is the one branch
|
|
1965
|
+
// the skin decides: what a slot shows is resolved through the worn skin and
|
|
1966
|
+
// then `defaultSkin`, while an alpha is read off the pose and has no skin in
|
|
1967
|
+
// it. Before #583 the pose wore no skin at all, so a mesh in a named skin was
|
|
1968
|
+
// reported as a slot showing nothing — a true sentence about a pose nobody
|
|
1969
|
+
// would ever play, read as a claim about the rig. A clause on every branch
|
|
1970
|
+
// would be noise on the branch it cannot explain.
|
|
1971
|
+
const dress = under === null ? ', with no skin worn' : `, with skin "${under}" worn`;
|
|
1972
|
+
for (const other of attachment.timelineSlots) {
|
|
1973
|
+
if (other === slotIndex) continue;
|
|
1974
|
+
const there = posed.shownAt(other, attachment);
|
|
1975
|
+
if (there.alpha > 0) {
|
|
1976
|
+
// Drawn somewhere the deform reaches, so there is nothing to exempt — and
|
|
1977
|
+
// the geometry above was measured on a slot that is not the one drawing
|
|
1978
|
+
// it, which the report says out loud rather than passing over.
|
|
1979
|
+
return { ...draw, blank: null };
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
if (!own.showsThisMesh) {
|
|
1983
|
+
const instead = own.shown === null ? 'no attachment at all' : `attachment "${own.shown}"`;
|
|
1984
|
+
return {
|
|
1985
|
+
...draw,
|
|
1986
|
+
blank:
|
|
1987
|
+
`the slot shows ${instead} at this time${dress}, not this mesh, so the runtime applies no deform to it ` +
|
|
1988
|
+
'here and draws none of it',
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
if (own.alpha === 0) {
|
|
1992
|
+
return {
|
|
1993
|
+
...draw,
|
|
1994
|
+
blank:
|
|
1995
|
+
`the slot's alpha is exactly 0 at this time (slot ${own.slotAlpha.toFixed(4)} x attachment ` +
|
|
1996
|
+
`${own.attachmentAlpha.toFixed(4)}), so this key draws no pixels`,
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
return { ...draw, blank: null };
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// --- #969 the seam (spine-core's side of it is `./deformmeasure.ts`'s) ---
|
|
2003
|
+
//
|
|
2004
|
+
// The survey poses a skeleton and reads four things off it; everything else it
|
|
2005
|
+
// does is arithmetic over those readings and over the skeleton's structure. So
|
|
2006
|
+
// the seam is those poses and readings, and there are two posers behind it:
|
|
2007
|
+
// spine-core over the Spine skeleton (the survey as it always was, and
|
|
2008
|
+
// `validate.ts`'s A39, which hands it the SkeletonData it round-tripped), and
|
|
2009
|
+
// rigc's own core over the model document (`./core/hooks.ts`, issue #969),
|
|
2010
|
+
// which a build carries and `explain` hands in. Since issue #1019 the
|
|
2011
|
+
// structure comes in the same two kinds, paired with the posers: spine-core's
|
|
2012
|
+
// parse read by `runtimeSide` (`./deformmeasure.ts`), and the model document read by
|
|
2013
|
+
// `modelStructure` (`./deformstructure.ts`) — so a build the core poses is
|
|
2014
|
+
// surveyed without touching the runtime at all. The survey's record names which
|
|
2015
|
+
// pair it used (`DeformSurvey.source`), and `tools/survey_hashes.ts` holds the
|
|
2016
|
+
// two to one survey, byte for byte, on every corpus.
|
|
2017
|
+
|
|
2018
|
+
/** Which poser a survey used (`DeformSurvey.source`). */
|
|
2019
|
+
export type DeformSurveySource = 'spine-core' | 'model';
|
|
2020
|
+
|
|
2021
|
+
/** What one slot shows of a mesh at one pose, and at what alpha — `shownAt`'s reading. */
|
|
2022
|
+
export interface ShownReading {
|
|
2023
|
+
/** The shown attachment's name, or `null` when the slot shows none. */
|
|
2024
|
+
shown: string | null;
|
|
2025
|
+
showsThisMesh: boolean;
|
|
2026
|
+
slotAlpha: number;
|
|
2027
|
+
attachmentAlpha: number;
|
|
2028
|
+
alpha: number;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
/**
|
|
2032
|
+
* One posed skeleton as the survey reads it.
|
|
2033
|
+
*
|
|
2034
|
+
* ⚠️ spine-core's side is a live skeleton: `plain` empties the slot's deform
|
|
2035
|
+
* array for good and `withDeform` writes one and empties it again, exactly as
|
|
2036
|
+
* the survey always did — the survey reads `deformed` before `plain` on a pose
|
|
2037
|
+
* and never after, so the core's side, which holds no state, reads the same.
|
|
2038
|
+
*/
|
|
2039
|
+
export interface SurveyPose {
|
|
2040
|
+
/** The skin the skeleton wears, off the pose itself (`DeformKeyDraw.under`). */
|
|
2041
|
+
under(): string | null;
|
|
2042
|
+
shownAt(slotIndex: number, attachment: SurveyMesh): ShownReading;
|
|
2043
|
+
/** The mesh's world vertices with the slot's deform as posed. */
|
|
2044
|
+
deformed(slotIndex: number, attachment: SurveyMesh): Float32Array;
|
|
2045
|
+
/** The same bones with the slot's deform cleared. */
|
|
2046
|
+
plain(slotIndex: number, attachment: SurveyMesh): Float32Array;
|
|
2047
|
+
/** The same bones with `deform` written into the slot. */
|
|
2048
|
+
withDeform(slotIndex: number, attachment: SurveyMesh, deform: ArrayLike<number>): Float32Array;
|
|
2049
|
+
/**
|
|
2050
|
+
* The doubles those three are stored from — the slot's deform as posed,
|
|
2051
|
+
* cleared, or replaced — leaving the pose as it found it. The survey reads
|
|
2052
|
+
* the float32 rows; this is what measures the rows' source (`tools/survey_hashes.ts hooks`).
|
|
2053
|
+
*/
|
|
2054
|
+
rows(slotIndex: number, attachment: SurveyMesh, deform: 'posed' | 'cleared' | ArrayLike<number>): number[];
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
/** One slider's dial, posed again and again (`planDial`'s probe, `poseDial`'s solve). */
|
|
2058
|
+
export interface DialSession {
|
|
2059
|
+
/** Whether the slider has a driving bone — `false` on the bone-less form, whose dial is its time. */
|
|
2060
|
+
hasBone: boolean;
|
|
2061
|
+
/** The driving bone's setup value of `field`. */
|
|
2062
|
+
base(field: DialField): number;
|
|
2063
|
+
/** Posed with `field` at `candidate` (the slider's time when `field` is `null`): the property read, and `SliderPose.time`. */
|
|
2064
|
+
at(field: DialField | null, candidate: number): { read: number; applied: number };
|
|
2065
|
+
/** The pose the last `at` left. */
|
|
2066
|
+
pose(): SurveyPose;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
/** The two ways the survey poses: a jump on a track, and a slider's dial. */
|
|
2070
|
+
export interface SurveyPoser {
|
|
2071
|
+
track(skin: SurveySkin | null, animation: string, time: number): SurveyPose;
|
|
2072
|
+
/** `null` when the skeleton carries no such slider. */
|
|
2073
|
+
dial(skin: SurveySkin | null, slider: SurveySlider): DialSession | null;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
/** A pose of the core's, read through the structure's slot names and each mesh's model record. */
|
|
2077
|
+
function corePose(pose: CoreSurveyPose, structure: SurveyStructure): SurveyPose {
|
|
2078
|
+
const slotName = (slotIndex: number): string => {
|
|
2079
|
+
const name = structure.slotName(slotIndex);
|
|
2080
|
+
if (name === `#${slotIndex}`) throw new CoreInputError(`slot #${slotIndex} is not a slot of the skeleton`);
|
|
2081
|
+
return name;
|
|
2082
|
+
};
|
|
2083
|
+
const recordOf = (attachment: SurveyMesh): { skin: string; slot: string; placeholder: string } => {
|
|
2084
|
+
const r = attachment.record;
|
|
2085
|
+
if (r === null) throw new CoreInputError(`mesh "${attachment.name}" is in no skin of the skeleton, so it names no model record`);
|
|
2086
|
+
return r;
|
|
2087
|
+
};
|
|
2088
|
+
const world = (slotIndex: number, attachment: SurveyMesh, deform: 'posed' | 'cleared' | ArrayLike<number>): Float32Array =>
|
|
2089
|
+
float32Rows(meshWorld(pose, slotName(slotIndex), recordOf(attachment), deform));
|
|
2090
|
+
return {
|
|
2091
|
+
under: () => pose.doc.skin,
|
|
2092
|
+
shownAt: (slotIndex, attachment) => {
|
|
2093
|
+
const d = slotDraw(pose, slotName(slotIndex));
|
|
2094
|
+
const r = recordOf(attachment);
|
|
2095
|
+
const showsThisMesh = d.identity !== null && d.identity === recordIdentity(r.skin, r.slot, r.placeholder);
|
|
2096
|
+
return { shown: d.shown, showsThisMesh, slotAlpha: d.slotAlpha, attachmentAlpha: d.attachmentAlpha, alpha: showsThisMesh ? d.slotAlpha * d.attachmentAlpha : 0 };
|
|
2097
|
+
},
|
|
2098
|
+
deformed: (slotIndex, attachment) => world(slotIndex, attachment, 'posed'),
|
|
2099
|
+
plain: (slotIndex, attachment) => world(slotIndex, attachment, 'cleared'),
|
|
2100
|
+
withDeform: (slotIndex, attachment, deform) => world(slotIndex, attachment, deform),
|
|
2101
|
+
rows: (slotIndex, attachment, deform) => meshWorld(pose, slotName(slotIndex), recordOf(attachment), deform),
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
/**
|
|
2106
|
+
* The survey's poses through rigc's own core over the model document
|
|
2107
|
+
* (`./core/hooks.ts`). A skin is worn by name — the core's view of one skin
|
|
2108
|
+
* (`underSkin`) — so a skeleton declaring two skins of one name, and a pose
|
|
2109
|
+
* wearing none, are refused by name: neither reaches the survey through
|
|
2110
|
+
* `SkeletonJson` (`placementOf`'s note), and neither has a view to pose.
|
|
2111
|
+
* `structure` is whichever reader's: the model document's on a build the core
|
|
2112
|
+
* poses, the runtime's where the hooks are held to spine-core's.
|
|
2113
|
+
*/
|
|
2114
|
+
export function corePoser(structure: SurveyStructure, doc: CompiledDocument): SurveyPoser {
|
|
2115
|
+
const views = new Map<string, CompiledDocument>();
|
|
2116
|
+
const viewOf = (skin: SurveySkin | null): CompiledDocument => {
|
|
2117
|
+
if (skin === null) throw new CoreInputError('the survey asked for a pose wearing no skin, and the core poses one skin\'s view');
|
|
2118
|
+
if (structure.skinsNamed(skin.name) > 1) throw new CoreInputError(`the skeleton declares two skins named "${skin.name}", and the model names a skin by its name`);
|
|
2119
|
+
const cached = views.get(skin.name);
|
|
2120
|
+
if (cached !== undefined) return cached;
|
|
2121
|
+
const view = underSkin(doc, skin.name);
|
|
2122
|
+
views.set(skin.name, view);
|
|
2123
|
+
return view;
|
|
2124
|
+
};
|
|
2125
|
+
return {
|
|
2126
|
+
track: (skin, animation, time) => corePose(poseJump(viewOf(skin), animation, time), structure),
|
|
2127
|
+
dial: (skin, slider) => {
|
|
2128
|
+
const view = viewOf(skin);
|
|
2129
|
+
const record = sliderRecordOf(view, slider.name);
|
|
2130
|
+
const boneName = record.bone;
|
|
2131
|
+
let last: CoreSurveyPose | null = null;
|
|
2132
|
+
return {
|
|
2133
|
+
hasBone: boneName !== null,
|
|
2134
|
+
base: (field) => {
|
|
2135
|
+
const bone = view.bones.find((b) => b.name === boneName);
|
|
2136
|
+
if (bone === undefined) throw new CoreInputError(`slider "${slider.name}"'s bone is not a bone of the model document`);
|
|
2137
|
+
const v = bone[field];
|
|
2138
|
+
return v ?? (field === 'scaleX' || field === 'scaleY' ? 1 : 0);
|
|
2139
|
+
},
|
|
2140
|
+
at: (field, candidate) => {
|
|
2141
|
+
const posed = corePoseDial(view, field === null || boneName === null ? { slider: slider.name, time: candidate } : { bone: boneName, field, value: candidate }, slider.name);
|
|
2142
|
+
last = posed;
|
|
2143
|
+
return { read: field === null || posed.read === null ? candidate : posed.read, applied: posed.applied };
|
|
2144
|
+
},
|
|
2145
|
+
pose: () => {
|
|
2146
|
+
if (last === null) throw new CoreInputError(`slider "${slider.name}"'s dial was asked for its pose before it was posed`);
|
|
2147
|
+
return corePose(last, structure);
|
|
2148
|
+
},
|
|
2149
|
+
};
|
|
2150
|
+
},
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
/**
|
|
2155
|
+
* The survey off a model document alone: its structure read by `read` —
|
|
2156
|
+
* `modelStructure` unless a control plants a misreading (`DM13`) — and posed
|
|
2157
|
+
* through the core, as `surveyOfBuild`'s model path takes it.
|
|
2158
|
+
*/
|
|
2159
|
+
export function surveyOfModel(doc: CompiledDocument, exempt: ReadonlySet<string>, read: (doc: CompiledDocument) => SurveyStructure = modelStructure): DeformSurvey {
|
|
2160
|
+
const structure = read(doc);
|
|
2161
|
+
return { ...surveyWith(structure, exempt, corePoser(structure, doc)), source: { used: 'model', why: null } };
|
|
2162
|
+
}
|