rig-c 0.0.0-stage → 2.20.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +13 -0
- package/LICENSE +30 -0
- package/NOTICE.md +145 -0
- package/README.md +817 -3
- package/bin/rigc.cjs +83 -0
- package/cli.ts +61 -0
- package/cli_core.ts +46 -0
- package/docs/AUTHORING.md +9923 -0
- package/docs/FACE.md +1948 -0
- package/docs/INGEST.md +1488 -0
- package/docs/MOTION.md +1241 -0
- package/docs/PROMPTING.md +109 -0
- package/docs/RIGGING.md +1441 -0
- package/docs/SPEC_COVERAGE.md +357 -0
- package/package.json +108 -4
- package/skills/rigc/SKILL.md +133 -0
- package/skills/rigc-face/SKILL.md +60 -0
- package/skills/rigc-ingest/SKILL.md +78 -0
- package/skills/rigc-motion/SKILL.md +51 -0
- package/skills/rigc-rigging/SKILL.md +49 -0
- package/src/areaband.ts +159 -0
- package/src/assertions/bodies/a01.ts +23 -0
- package/src/assertions/bodies/a02.ts +21 -0
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a04.ts +40 -0
- package/src/assertions/bodies/a05.ts +56 -0
- package/src/assertions/bodies/a06.ts +245 -0
- package/src/assertions/bodies/a07.ts +68 -0
- package/src/assertions/bodies/a08.ts +76 -0
- package/src/assertions/bodies/a09.ts +82 -0
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a12.ts +30 -0
- package/src/assertions/bodies/a13.ts +51 -0
- package/src/assertions/bodies/a14.ts +35 -0
- package/src/assertions/bodies/a15.ts +97 -0
- package/src/assertions/bodies/a16.ts +24 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a18.ts +62 -0
- package/src/assertions/bodies/a19.ts +404 -0
- package/src/assertions/bodies/a20.ts +122 -0
- package/src/assertions/bodies/a21.ts +190 -0
- package/src/assertions/bodies/a22.ts +39 -0
- package/src/assertions/bodies/a23.ts +305 -0
- package/src/assertions/bodies/a24.ts +68 -0
- package/src/assertions/bodies/a25.ts +39 -0
- package/src/assertions/bodies/a26.ts +61 -0
- package/src/assertions/bodies/a27.ts +33 -0
- package/src/assertions/bodies/a28.ts +70 -0
- package/src/assertions/bodies/a29.ts +34 -0
- package/src/assertions/bodies/a30.ts +50 -0
- package/src/assertions/bodies/a31.ts +61 -0
- package/src/assertions/bodies/a32.ts +44 -0
- package/src/assertions/bodies/a33.ts +110 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a35.ts +160 -0
- package/src/assertions/bodies/a36.ts +81 -0
- package/src/assertions/bodies/a37.ts +77 -0
- package/src/assertions/bodies/a38.ts +73 -0
- package/src/assertions/bodies/a39.ts +303 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/bodies/a42.ts +97 -0
- package/src/assertions/bodies/a43.ts +181 -0
- package/src/assertions/bodies/a44.ts +23 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/bodies/a46.ts +224 -0
- package/src/assertions/bodies/a47.ts +126 -0
- package/src/assertions/bodies/a48.ts +83 -0
- package/src/assertions/bodies/a49.ts +81 -0
- package/src/assertions/bodies/a50.ts +97 -0
- package/src/assertions/constraint_words.ts +169 -0
- package/src/assertions/emitted/index.ts +148 -0
- package/src/assertions/facts/animated_bones.ts +30 -0
- package/src/assertions/facts/animation_durations.ts +37 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/atlas_regions.ts +52 -0
- package/src/assertions/facts/bone_timelines.ts +37 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/constraints.ts +155 -0
- package/src/assertions/facts/deform_survey.ts +27 -0
- package/src/assertions/facts/event_keys.ts +55 -0
- package/src/assertions/facts/linked_meshes.ts +38 -0
- package/src/assertions/facts/mesh_attachments.ts +100 -0
- package/src/assertions/facts/region_joins.ts +34 -0
- package/src/assertions/facts/sequences.ts +85 -0
- package/src/assertions/facts/skeleton_roster.ts +45 -0
- package/src/assertions/facts/skin_entries.ts +37 -0
- package/src/assertions/facts/skin_members.ts +53 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/facts/stage.ts +27 -0
- package/src/assertions/facts/stage_box.ts +65 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/facts/two_colour.ts +52 -0
- package/src/assertions/facts/vertex_polygons.ts +53 -0
- package/src/assertions/footprints.ts +367 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/inward_advance.ts +58 -0
- package/src/assertions/kinds.ts +105 -0
- package/src/assertions/mesh_kinds.ts +56 -0
- package/src/assertions/model/animated_bones.ts +38 -0
- package/src/assertions/model/animation_durations.ts +57 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/atlas_regions.ts +76 -0
- package/src/assertions/model/bone_timelines.ts +58 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/constraints.ts +233 -0
- package/src/assertions/model/declared.ts +125 -0
- package/src/assertions/model/deform_survey.ts +24 -0
- package/src/assertions/model/event_keys.ts +45 -0
- package/src/assertions/model/given.ts +45 -0
- package/src/assertions/model/index.ts +398 -0
- package/src/assertions/model/linked_meshes.ts +24 -0
- package/src/assertions/model/mesh_attachments.ts +119 -0
- package/src/assertions/model/parse.ts +146 -0
- package/src/assertions/model/region_joins.ts +67 -0
- package/src/assertions/model/runtime_timelines.ts +78 -0
- package/src/assertions/model/sequences.ts +157 -0
- package/src/assertions/model/skeleton_roster.ts +23 -0
- package/src/assertions/model/skin_entries.ts +69 -0
- package/src/assertions/model/skin_members.ts +64 -0
- package/src/assertions/model/slider_composition.ts +193 -0
- package/src/assertions/model/slot_colour.ts +81 -0
- package/src/assertions/model/stage.ts +28 -0
- package/src/assertions/model/stage_box.ts +51 -0
- package/src/assertions/model/stepped_poses.ts +105 -0
- package/src/assertions/model/two_colour.ts +61 -0
- package/src/assertions/model/vertex_polygons.ts +72 -0
- package/src/assertions/reasons.ts +129 -0
- package/src/assertions/region_lookups.ts +61 -0
- package/src/assertions/report.ts +189 -0
- package/src/assertions/values.ts +39 -0
- package/src/atlas.ts +2870 -0
- package/src/ballot.ts +866 -0
- package/src/bonedist.ts +643 -0
- package/src/chainfit.ts +2752 -0
- package/src/chains.ts +170 -0
- package/src/check.ts +4303 -0
- package/src/checkpics.ts +295 -0
- package/src/cli/core_commands.ts +1627 -0
- package/src/cli/repack.ts +414 -0
- package/src/cli/shared.ts +2776 -0
- package/src/cli/spine_commands.ts +820 -0
- package/src/compile.ts +9414 -0
- package/src/core/additive.ts +458 -0
- package/src/core/animation.ts +1050 -0
- package/src/core/clipping.ts +696 -0
- package/src/core/constraints.ts +1876 -0
- package/src/core/constraints_path.ts +964 -0
- package/src/core/constraints_physics.ts +881 -0
- package/src/core/constraints_slider.ts +635 -0
- package/src/core/deform.ts +613 -0
- package/src/core/draw_order.ts +125 -0
- package/src/core/events.ts +135 -0
- package/src/core/hooks.ts +249 -0
- package/src/core/index.ts +1400 -0
- package/src/core/raw.ts +739 -0
- package/src/core/skins.ts +129 -0
- package/src/core/uvs.ts +469 -0
- package/src/core/vertices.ts +490 -0
- package/src/core/walk.ts +197 -0
- package/src/core/world.ts +289 -0
- package/src/correspondence.ts +15 -0
- package/src/deformbuild.ts +60 -0
- package/src/deformgen.ts +630 -0
- package/src/deformmeasure.ts +732 -0
- package/src/deformreport.ts +373 -0
- package/src/deformstructure.ts +386 -0
- package/src/deformsurvey.ts +2162 -0
- package/src/depth.ts +784 -0
- package/src/diff.ts +2252 -0
- package/src/emit.ts +134 -0
- package/src/emit_spine.ts +854 -0
- package/src/errors.ts +53 -0
- package/src/framing.ts +819 -0
- package/src/generation.ts +139 -0
- package/src/ingest.ts +2293 -0
- package/src/json-position.ts +253 -0
- package/src/keyorder.ts +587 -0
- package/src/keys.ts +486 -0
- package/src/ladder.ts +121 -0
- package/src/mesh.ts +2382 -0
- package/src/meshcompare.ts +1188 -0
- package/src/meshquality.ts +2042 -0
- package/src/meshrasters.ts +944 -0
- package/src/meshreduce.ts +1425 -0
- package/src/model.ts +1245 -0
- package/src/motion.ts +809 -0
- package/src/nonfinite.ts +54 -0
- package/src/package_meta.ts +48 -0
- package/src/png.ts +297 -0
- package/src/pose.ts +2324 -0
- package/src/preview.ts +434 -0
- package/src/region_joins.ts +54 -0
- package/src/render.ts +1013 -0
- package/src/render_core.ts +871 -0
- package/src/render_shared.ts +2958 -0
- package/src/repack.ts +495 -0
- package/src/rig.ts +2941 -0
- package/src/slots.ts +892 -0
- package/src/spine_side.ts +138 -0
- package/src/timelines.ts +837 -0
- package/src/trackgen.ts +364 -0
- package/src/transform.ts +310 -0
- package/src/types.ts +1797 -0
- package/src/validate.ts +3875 -0
- package/tools/contact.ts +126 -0
- package/tools/editor_roundtrip.ts +1641 -0
- package/tools/font5x7.ts +101 -0
- package/tools/measure_contact_depth.ts +105 -0
- package/tools/plate.ts +508 -0
- package/tools/png_probe.mjs +72 -0
package/src/deformgen.ts
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `deform` key's offsets, evaluated from a transform the spec states.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this module exists
|
|
5
|
+
*
|
|
6
|
+
* A `deform` timeline was the one place in a rig spec where an author was still
|
|
7
|
+
* transcribing arithmetic. `gallery/portrait`'s held 12° head yaw is **160
|
|
8
|
+
* vertex offsets across 8 keys**, and not one of them is a judgement: every one
|
|
9
|
+
* is `x·(cos t − 1) − z·sin t` evaluated at a different column
|
|
10
|
+
* (docs/FACE.md §1). A second angle is a second full table, which is why that
|
|
11
|
+
* example's own angle sweep needed a throwaway script that was never in the
|
|
12
|
+
* repository — *the measurement existed and the reproduction did not*
|
|
13
|
+
* (issue #294).
|
|
14
|
+
*
|
|
15
|
+
* rigc already had this shape of answer for **geometry**:
|
|
16
|
+
* `generator: { kind: "contour" | "ring" | "ribbon" }` exists because "a table
|
|
17
|
+
* of numbers is the wrong way to say a deformation model" (AUTHORING §3.4).
|
|
18
|
+
* This is the same move on the animation half. **The spec states the model and
|
|
19
|
+
* the compiler states the numbers**, so `explain` can print what a key does, an
|
|
20
|
+
* author can sweep a parameter, and a reviewer can check a claim instead of a
|
|
21
|
+
* transcription.
|
|
22
|
+
*
|
|
23
|
+
* ## What this is NOT, and the three rules that keep it that way
|
|
24
|
+
*
|
|
25
|
+
* 1. **It never authors.** Every parameter arrives from the spec — the angle,
|
|
26
|
+
* the radius, the amplitude, the point a scale is about. Nothing here has a
|
|
27
|
+
* default measured off the art, and a missing number is a `CompileError`
|
|
28
|
+
* naming the field, exactly as everywhere else in the compiler.
|
|
29
|
+
* 2. **It generates no in-betweens.** MOTION §7 refuses a `rigc tween`, and
|
|
30
|
+
* this is not one: a transform is evaluated **at one key**, from parameters
|
|
31
|
+
* that key states, and the blend between two keys is still the deform
|
|
32
|
+
* timeline's own single 0..1 channel. Sweeping an angle is editing one number
|
|
33
|
+
* per key, never asking the compiler to interpolate a model.
|
|
34
|
+
* 3. **A parameter that cannot mean what it says is refused by name**, on the
|
|
35
|
+
* principle that a parameter changing nothing is a reader's false lead about
|
|
36
|
+
* which model produced the numbers: `wavelength: 0`, a determinant at or
|
|
37
|
+
* below 0, a radius that falls inside the part. Since issue #350 that extends
|
|
38
|
+
* one step later, to a model whose parameters are each legal and whose
|
|
39
|
+
* *evaluation* is an all-zero run — the refusal at the bottom of
|
|
40
|
+
* `evaluateDeformTransform`, whose whole difficulty is telling that apart
|
|
41
|
+
* from a key that means the identity and says so.
|
|
42
|
+
*
|
|
43
|
+
* ## Determinism
|
|
44
|
+
*
|
|
45
|
+
* Every closed form below is a fixed sequence of float64 operations over
|
|
46
|
+
* numbers read from the spec, in vertex order, with no iteration over an
|
|
47
|
+
* unordered set. The caller quantises with the compiler's `onModelGrid` — a
|
|
48
|
+
* model's own 1e-6 resolution, ending in the float32 name every emitted number
|
|
49
|
+
* takes — so the same spec emits the same bytes, which `A18_DETERMINISTIC_EMIT`
|
|
50
|
+
* proves on a second independent compile. The grid is absolute on purpose: a
|
|
51
|
+
* closed form's identities (a zero crossing, a whole revolution) are exact
|
|
52
|
+
* zeros float64 misses by ~1e-16, and a float32 grid alone has no zero to land
|
|
53
|
+
* on. The runtime then loads the numbers into a `Float32Array`, which is the
|
|
54
|
+
* reason the offsets are reported in the units they are emitted in rather than
|
|
55
|
+
* at full float64 width.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
import { CompileError } from './errors.ts';
|
|
59
|
+
|
|
60
|
+
/** Which coordinate a bend or a wave reads, and which one it displaces. */
|
|
61
|
+
export type DeformAxis = 'x' | 'y';
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A **2.5D turn**: the part is treated as painted on a cylinder standing on
|
|
65
|
+
* `about`, and the key is that rotation projected back onto the screen.
|
|
66
|
+
*
|
|
67
|
+
* `yaw` stands the cylinder vertically and moves vertices horizontally; `pitch`
|
|
68
|
+
* is the same expression with the axes swapped.
|
|
69
|
+
*
|
|
70
|
+
* ## Why there is no separate `parallax` kind
|
|
71
|
+
*
|
|
72
|
+
* A pure depth slide — `d = z · offset`, no angle — was written and then taken
|
|
73
|
+
* out again (2026-09-05), for two reasons and the second is the one that
|
|
74
|
+
* settles it.
|
|
75
|
+
*
|
|
76
|
+
* ⭐ **It is this form with a term dropped.** Subtract them and the whole
|
|
77
|
+
* remainder is `u · (cos t − 1)`, independent of the depth, so the slide is
|
|
78
|
+
* `yaw` at a small angle and nothing else: at 16° the gap is 2.32px, at 1°
|
|
79
|
+
* 0.0091px, quartering with each halving. A second kind for the same model
|
|
80
|
+
* evaluated less accurately is a second answer to one question.
|
|
81
|
+
*
|
|
82
|
+
* 🚨 **And what it was for is not rigc's to state.** A depth slide is a CAMERA
|
|
83
|
+
* move — the viewer shifts and the near parts lag — driven by a pointer, not by
|
|
84
|
+
* a clock. This spec is a timeline. Baking a pointer-driven parameter into time
|
|
85
|
+
* keys is a category error, and it bakes what a runtime should be evaluating.
|
|
86
|
+
*
|
|
87
|
+
* ⇒ What rigc states about a raised surface is the ANGLE it may turn through
|
|
88
|
+
* (here) and, for a soft one, how it answers an impact (a physics constraint on
|
|
89
|
+
* a bone the mesh is bound to). The camera belongs to whatever draws the
|
|
90
|
+
* result. docs/FACE.md §1 is the whole
|
|
91
|
+
* derivation, and §4.2 is the angle past which any given column pair folds —
|
|
92
|
+
* this evaluates the projection and says nothing about whether the angle is
|
|
93
|
+
* sane. `A39_DEFORM_KEEPS_TRIANGLE_WINDING` is what catches a fold.
|
|
94
|
+
*/
|
|
95
|
+
export interface DeformTurn {
|
|
96
|
+
kind: 'yaw' | 'pitch';
|
|
97
|
+
/**
|
|
98
|
+
* The cylinder's radius, in the attachment's own units.
|
|
99
|
+
*
|
|
100
|
+
* ⚠️ **Not the plate's half-width.** For `gallery/portrait`'s head plate the
|
|
101
|
+
* two coincide (340/2 = 170); for its fringe they do not — 372 wide and
|
|
102
|
+
* `radius` 196, because 196 is where the fringe *sits*, 26 in front of the
|
|
103
|
+
* skull. Read it off the depth table, never off the PNG (FACE §4).
|
|
104
|
+
*/
|
|
105
|
+
radius?: number;
|
|
106
|
+
/**
|
|
107
|
+
* Read `z` per vertex from the attachment's depth map instead of deriving it
|
|
108
|
+
* from a cylinder — the same closed form with a measured surface under it.
|
|
109
|
+
*
|
|
110
|
+
* ⭐ It replaces `radius`, and stating both is refused: they are two answers
|
|
111
|
+
* to "how far forward is this vertex", and a spec that carries both leaves a
|
|
112
|
+
* reader unable to tell which one the output came from. The map itself is
|
|
113
|
+
* named on the attachment's `generator` ([`src/rig.ts`](rig.ts)), because it
|
|
114
|
+
* is a property of the art rather than of this key — every key that turns
|
|
115
|
+
* this part reads the same surface.
|
|
116
|
+
*/
|
|
117
|
+
depth?: boolean;
|
|
118
|
+
/** The turn, in degrees. Positive yaws toward −x, which is FACE §1's sign. */
|
|
119
|
+
degrees: number;
|
|
120
|
+
/** Where the axis crosses the driving coordinate. Default 0. */
|
|
121
|
+
about?: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A **scale about a point** — `gallery/squash`'s two shapes, which its README
|
|
126
|
+
* already writes out as `sx`/`sy` about a point.
|
|
127
|
+
*
|
|
128
|
+
* The point is fixed by construction, which is the property that makes the
|
|
129
|
+
* example's claim checkable: the ball's contact vertex sits at the contact
|
|
130
|
+
* point, so its offset is `(0, 0)` and the ball flattens against the ground
|
|
131
|
+
* rather than sinking through it — with no key anywhere that has to be tuned to
|
|
132
|
+
* make that true.
|
|
133
|
+
*
|
|
134
|
+
* ⭐ `det = sx · sy` is refused at or below 0, and above 0 it is a **proof**
|
|
135
|
+
* rather than a report: an affine map with a positive determinant preserves
|
|
136
|
+
* every triangle's winding, so a key of this kind cannot be the fold A39 hunts.
|
|
137
|
+
* A shear belongs to `bend` with `power: 1`.
|
|
138
|
+
*/
|
|
139
|
+
export interface DeformAffine {
|
|
140
|
+
kind: 'affine';
|
|
141
|
+
/** `[sx, sy]`. 1 is unchanged; both are required, because a guessed one is a value the spec did not state. */
|
|
142
|
+
scale: [number, number];
|
|
143
|
+
/** The fixed point, in the attachment's own units. Default `[0, 0]`. */
|
|
144
|
+
about?: [number, number];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A sinusoid of one coordinate, displacing along another.
|
|
149
|
+
*
|
|
150
|
+
* ⚠️ **A wavelength is only as real as the geometry that samples it.** The mesh
|
|
151
|
+
* carries the wave at the coordinates it happens to have, so a period short
|
|
152
|
+
* against the spacing of those coordinates does not make a smaller ripple — it
|
|
153
|
+
* makes a different model. Against a spacing of `s`: `wavelength ≥ 4s` to read
|
|
154
|
+
* as a wave at all and `≥ 8s` to read as a curve; at `2s` every sample lands on
|
|
155
|
+
* the same pair of phases, which is a zigzag, and at that pair's zero crossings
|
|
156
|
+
* it is nothing at all. The last of those is refused (issue #350) because it
|
|
157
|
+
* emits an all-zero run while claiming an amplitude; the zigzag is not, because
|
|
158
|
+
* it is a bad wave rather than an absent one and that is authoring judgement.
|
|
159
|
+
*/
|
|
160
|
+
export interface DeformWave {
|
|
161
|
+
kind: 'wave';
|
|
162
|
+
/** Peak displacement, in the attachment's own units. */
|
|
163
|
+
amplitude: number;
|
|
164
|
+
/** One period, in the same units as the coordinate `along` reads. Sampled by the geometry — see the note above. */
|
|
165
|
+
wavelength: number;
|
|
166
|
+
/** Phase at `along = 0`, in degrees. Default 0. */
|
|
167
|
+
phase?: number;
|
|
168
|
+
/** The coordinate the sinusoid reads. */
|
|
169
|
+
along: DeformAxis;
|
|
170
|
+
/** The coordinate it displaces. */
|
|
171
|
+
axis: DeformAxis;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A **polynomial bend**: displacement rising as a power of how far along the
|
|
176
|
+
* part a vertex is.
|
|
177
|
+
*
|
|
178
|
+
* `power: 1` is an affine shear, and every higher power is a curve no bone
|
|
179
|
+
* transform can produce — which is the whole reason a `deform` timeline is
|
|
180
|
+
* worth its numbers on a mesh whose vertices are pinned to one bone.
|
|
181
|
+
* `power: 2` is a cantilever: zero displacement **and zero slope** at `from`,
|
|
182
|
+
* so a part held at one end bends rather than tilting.
|
|
183
|
+
*/
|
|
184
|
+
export interface DeformBend {
|
|
185
|
+
kind: 'bend';
|
|
186
|
+
/** Displacement at `to`, in the attachment's own units. */
|
|
187
|
+
amount: number;
|
|
188
|
+
/** Where the bend is anchored — displacement is 0 here. */
|
|
189
|
+
from: number;
|
|
190
|
+
/** Where it reaches `amount`. */
|
|
191
|
+
to: number;
|
|
192
|
+
/** The exponent. A whole number ≥ 1; default 2. */
|
|
193
|
+
power?: number;
|
|
194
|
+
/** The coordinate that measures how far along a vertex is. */
|
|
195
|
+
along: DeformAxis;
|
|
196
|
+
/** The coordinate it displaces. */
|
|
197
|
+
axis: DeformAxis;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export type DeformTransform = DeformTurn | DeformAffine | DeformWave | DeformBend;
|
|
201
|
+
|
|
202
|
+
/** The kinds this module evaluates, in the order the docs list them. */
|
|
203
|
+
export const DEFORM_TRANSFORM_KINDS = ['yaw', 'pitch', 'affine', 'wave', 'bend'] as const;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* What one evaluation did, for `explain` to print and a reviewer to check.
|
|
207
|
+
*
|
|
208
|
+
* `stated` is the spec's own parameters and `derived` the scalars the closed
|
|
209
|
+
* form got out of them — the two lines that let somebody re-derive a column by
|
|
210
|
+
* hand. `offsets` is the emitted run, so the report and the artifact cannot
|
|
211
|
+
* disagree (`explain`'s `DEFORM` block, issue #316, quotes this rather than
|
|
212
|
+
* re-evaluating it — see `src/deformmeasure.ts`).
|
|
213
|
+
*/
|
|
214
|
+
export interface DeformTransformReport {
|
|
215
|
+
kind: DeformTransform['kind'];
|
|
216
|
+
/** The transform as the spec states it. */
|
|
217
|
+
stated: string;
|
|
218
|
+
/** The scalars the closed form derived, e.g. `cos t − 1 = −0.021852`. */
|
|
219
|
+
derived: string[];
|
|
220
|
+
/** The closed form, written out. */
|
|
221
|
+
formula: string;
|
|
222
|
+
vertexCount: number;
|
|
223
|
+
/** Largest offset magnitude in the run, and the vertex carrying it. */
|
|
224
|
+
maxOffset: number;
|
|
225
|
+
maxOffsetVertex: number;
|
|
226
|
+
/**
|
|
227
|
+
* The displacement the model states: `x, y` per vertex, already quantised by
|
|
228
|
+
* the caller's rounder.
|
|
229
|
+
*
|
|
230
|
+
* On an attachment whose deform array is one pair per vertex this IS the
|
|
231
|
+
* emitted run, which is the case every report printed before issue #389. On a
|
|
232
|
+
* multi-influence one it is one WORLD displacement per vertex, and the array
|
|
233
|
+
* the caller wrote from it is `expanded` on the compile result — the report
|
|
234
|
+
* still quotes what was emitted rather than re-evaluating anything.
|
|
235
|
+
*/
|
|
236
|
+
offsets: number[];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The compiler's `onModelGrid` (1e-6, then float32), passed in so the quantiser stays in one place. */
|
|
240
|
+
export type Rounder = (n: number) => number;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Evaluate one transform over an attachment's setup geometry.
|
|
244
|
+
*
|
|
245
|
+
* `setup` is `x, y` per vertex, and what comes back is **one displacement per
|
|
246
|
+
* vertex in the same space** — this function neither knows nor needs to know
|
|
247
|
+
* which space that is. The caller owns it, and since issue #389 there are two:
|
|
248
|
+
* the deform array's own space, where every vertex is one pair of one bone's
|
|
249
|
+
* bind space and the result IS the array; and setup **world**, where a vertex
|
|
250
|
+
* has several influences and the caller pushes each displacement into every one
|
|
251
|
+
* of them through that bone's inverse. Either way the array handed in means one
|
|
252
|
+
* thing all the way down it, which is the precondition the closed forms need.
|
|
253
|
+
*
|
|
254
|
+
* Returns a run as long as `setup`, so it always starts at deform index 0 and
|
|
255
|
+
* covers the whole attachment. That is deliberate and it is not a convenience:
|
|
256
|
+
* a transform is a model of the geometry rather than an edit of part of it, and
|
|
257
|
+
* a partially applied model leaves a **step discontinuity at the end of the
|
|
258
|
+
* run** — which is exactly one half of the defect issue #313 records, where a
|
|
259
|
+
* 20-vertex run of a hand-authored ripple began 6.3px away from the unkeyed
|
|
260
|
+
* vertex before it.
|
|
261
|
+
*/
|
|
262
|
+
export function evaluateDeformTransform(
|
|
263
|
+
transform: DeformTransform,
|
|
264
|
+
setup: readonly number[],
|
|
265
|
+
round: Rounder,
|
|
266
|
+
where: string,
|
|
267
|
+
/**
|
|
268
|
+
* Per-vertex `z` for this attachment, when its generator named a depth map.
|
|
269
|
+
* Null when it named none — which is what makes `"depth": true` refusable by
|
|
270
|
+
* name rather than silently falling back to a cylinder.
|
|
271
|
+
*/
|
|
272
|
+
depth: readonly number[] | null = null,
|
|
273
|
+
): DeformTransformReport {
|
|
274
|
+
if (transform === null || typeof transform !== 'object' || Array.isArray(transform)) {
|
|
275
|
+
throw new CompileError(`${where}: "transform" is ${JSON.stringify(transform)}; it is an object with a "kind"`);
|
|
276
|
+
}
|
|
277
|
+
const kind = (transform as { kind?: unknown }).kind;
|
|
278
|
+
if (typeof kind !== 'string' || !(DEFORM_TRANSFORM_KINDS as readonly string[]).includes(kind)) {
|
|
279
|
+
throw new CompileError(
|
|
280
|
+
`${where}: "transform" has kind ${JSON.stringify(kind)}; this spec evaluates ${DEFORM_TRANSFORM_KINDS.join(', ')}. ` +
|
|
281
|
+
'A model that is none of those is still authorable as a "vertices" run.',
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
const count = setup.length / 2;
|
|
285
|
+
const offsets = new Array<number>(setup.length);
|
|
286
|
+
let derived: string[];
|
|
287
|
+
let stated: string;
|
|
288
|
+
let formula: string;
|
|
289
|
+
// -- the three values the all-zero refusal at the bottom reads (issue #350) -
|
|
290
|
+
//
|
|
291
|
+
// `identity` is whether the transform's own scalars state the identity, and it
|
|
292
|
+
// is judged on the ROUNDED scalars rather than in float64 — a `degrees: 360`
|
|
293
|
+
// turn leaves `sin t` at −2.4e−16, which is 0 on the grid a model is evaluated
|
|
294
|
+
// on (`onModelGrid`), so a spec that states a whole revolution states the identity as
|
|
295
|
+
// surely as `degrees: 0` does. `band` is the largest magnitude the closed form
|
|
296
|
+
// reached *before* quantising, which is what separates a model that is
|
|
297
|
+
// arithmetically zero (a band of float noise, ~1e−15) from one that is real
|
|
298
|
+
// and smaller than that grid. `sampledTo` is the per-kind diagnosis.
|
|
299
|
+
let identity: boolean;
|
|
300
|
+
let identitySpelling: string;
|
|
301
|
+
let band = 0;
|
|
302
|
+
let sampledTo = '';
|
|
303
|
+
const widen = (d: number): number => {
|
|
304
|
+
const m = Math.abs(d);
|
|
305
|
+
if (m > band) band = m;
|
|
306
|
+
return d;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
switch (kind) {
|
|
310
|
+
case 'yaw':
|
|
311
|
+
case 'pitch': {
|
|
312
|
+
const t = transform as DeformTurn;
|
|
313
|
+
const degrees = num(t.degrees, 'degrees', where);
|
|
314
|
+
const about = t.about === undefined ? 0 : num(t.about, 'about', where);
|
|
315
|
+
// -- which surface the turn is projected off (issue #382) --------------
|
|
316
|
+
//
|
|
317
|
+
// Two models, one closed form. A cylinder derives `z` from how far off
|
|
318
|
+
// the axis a vertex sits; a depth map states it per vertex. Everything
|
|
319
|
+
// below is shared, and the only difference is where `z` comes from — the
|
|
320
|
+
// reason this is a branch on the input and not a second transform kind.
|
|
321
|
+
const fromDepth = t.depth === true;
|
|
322
|
+
if (fromDepth && t.radius !== undefined) {
|
|
323
|
+
throw new CompileError(
|
|
324
|
+
`${where}: transform ${kind} states both "depth": true and a radius ${JSON.stringify(t.radius)}. They are ` +
|
|
325
|
+
'two answers to how far forward each vertex sits — a cylinder derives it, a map states it — and a key ' +
|
|
326
|
+
'carrying both leaves a reader unable to say which one the output came from. Drop one.',
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
if (fromDepth && depth === null) {
|
|
330
|
+
throw new CompileError(
|
|
331
|
+
`${where}: transform ${kind} says "depth": true and this attachment has no depth map. The map is named on ` +
|
|
332
|
+
'the attachment\'s generator, as `"depth": { "image": …, "near": …, "zScale": … }`, because it describes ' +
|
|
333
|
+
'the art rather than this key. Name it there, or give this key a radius.',
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
if (fromDepth && depth !== null && depth.length !== count) {
|
|
337
|
+
// Unreachable while the sampler walks the same vertex list the mesh
|
|
338
|
+
// emitted; stated because a silent mismatch here would turn into a turn
|
|
339
|
+
// evaluated against another vertex's depth.
|
|
340
|
+
throw new CompileError(
|
|
341
|
+
`${where}: transform ${kind} has ${depth.length} sampled depths for ${count} vertices`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
const radius = fromDepth ? 0 : num(t.radius, 'radius', where);
|
|
345
|
+
if (!fromDepth && radius <= 0) {
|
|
346
|
+
throw new CompileError(`${where}: transform ${kind} has radius ${radius}; it is the radius of the cylinder the part is painted on, so a positive number`);
|
|
347
|
+
}
|
|
348
|
+
const rad = (degrees * Math.PI) / 180;
|
|
349
|
+
const cosMinus1 = Math.cos(rad) - 1;
|
|
350
|
+
const sin = Math.sin(rad);
|
|
351
|
+
// The driving coordinate is the one the axis is perpendicular to: x for a
|
|
352
|
+
// yaw (a vertical axis), y for a pitch (a horizontal one). The other
|
|
353
|
+
// component of every pair is 0, because a turn about an axis moves
|
|
354
|
+
// nothing along it.
|
|
355
|
+
const along = kind === 'yaw' ? 0 : 1;
|
|
356
|
+
for (let v = 0; v < count; v++) {
|
|
357
|
+
const u = setup[2 * v + along] - about;
|
|
358
|
+
// A vertex outside the cylinder has no surface to be painted on, and
|
|
359
|
+
// clamping its depth to 0 would silently evaluate a DIFFERENT model
|
|
360
|
+
// there — a flat edge on a curved part. Refused by name instead: this is
|
|
361
|
+
// FACE §4's "read R off the depth table, never off the PNG" as a check.
|
|
362
|
+
if (!fromDepth && Math.abs(u) > radius) {
|
|
363
|
+
throw new CompileError(
|
|
364
|
+
`${where}: transform ${kind} has radius ${radius}, and vertex ${v} sits at ${kind === 'yaw' ? 'x' : 'y'}=` +
|
|
365
|
+
`${setup[2 * v + along]}, which is ${round(Math.abs(u) - radius)} past it (about=${about}). The cylinder has no ` +
|
|
366
|
+
'surface there, so its depth would be 0 and the projection would be a different model at that vertex. ' +
|
|
367
|
+
'Raise the radius to where the part actually sits, or move the vertex.',
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
// A depth map needs no such check: it states a surface everywhere it
|
|
371
|
+
// covers, and a vertex it does NOT cover was already refused when the
|
|
372
|
+
// mesh was built (`sampleContourDepth`) rather than here, where the
|
|
373
|
+
// sheet is long out of reach.
|
|
374
|
+
const z = fromDepth ? (depth as readonly number[])[v] : Math.sqrt(radius * radius - u * u);
|
|
375
|
+
const d = u * cosMinus1 - z * sin;
|
|
376
|
+
offsets[2 * v + along] = round(widen(d));
|
|
377
|
+
offsets[2 * v + (1 - along)] = 0;
|
|
378
|
+
}
|
|
379
|
+
identity = round(cosMinus1) === 0 && round(sin) === 0;
|
|
380
|
+
identitySpelling = 'degrees 0';
|
|
381
|
+
sampledTo = fromDepth
|
|
382
|
+
? `The turn is ${degrees}°, and a projection of it can only vanish where every vertex shares one ` +
|
|
383
|
+
`${kind === 'yaw' ? 'x' : 'y'} AND one depth — check the depth map's sampled range in the mesh report, ` +
|
|
384
|
+
'which is 0 wide when the sheet is flat where this part sits'
|
|
385
|
+
: `The turn is ${degrees}°, and a projection of it can only vanish where every vertex shares one ` +
|
|
386
|
+
`${kind === 'yaw' ? 'x' : 'y'} — check that this attachment's setup geometry is the shape the radius says it is`;
|
|
387
|
+
const c = kind === 'yaw' ? 'x' : 'y';
|
|
388
|
+
stated = fromDepth
|
|
389
|
+
? `depth=true degrees=${degrees}${t.about === undefined ? '' : ` about=${about}`}`
|
|
390
|
+
: `radius=${radius} degrees=${degrees}${t.about === undefined ? '' : ` about=${about}`}`;
|
|
391
|
+
formula = fromDepth
|
|
392
|
+
? `d${c} = (${c}−about)·(cos t − 1) − z·sin t, z = the vertex's sampled depth`
|
|
393
|
+
: `d${c} = (${c}−about)·(cos t − 1) − z·sin t, z = √(radius² − (${c}−about)²)`;
|
|
394
|
+
derived = [
|
|
395
|
+
`t = ${round(rad)} rad`,
|
|
396
|
+
`cos t − 1 = ${round(cosMinus1)}`,
|
|
397
|
+
`sin t = ${round(sin)}`,
|
|
398
|
+
];
|
|
399
|
+
if (fromDepth) {
|
|
400
|
+
const zs = depth as readonly number[];
|
|
401
|
+
let zlo = Infinity;
|
|
402
|
+
let zhi = -Infinity;
|
|
403
|
+
for (const z of zs) {
|
|
404
|
+
if (z < zlo) zlo = z;
|
|
405
|
+
if (z > zhi) zhi = z;
|
|
406
|
+
}
|
|
407
|
+
// The depth range is what a reader checks the amplitude against: the
|
|
408
|
+
// deepest vertex moves `−zhi·sin t`, and that number is the one that
|
|
409
|
+
// either matches the art or does not.
|
|
410
|
+
derived.push(`z ∈ [${round(zlo)}, ${round(zhi)}] over ${zs.length} vertices`);
|
|
411
|
+
derived.push(`deepest shift = −z_max·sin t = ${round(-zhi * sin)}`);
|
|
412
|
+
} else {
|
|
413
|
+
derived.push(`centre shift = −radius·sin t = ${round(-radius * sin)}`);
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
case 'affine': {
|
|
418
|
+
const a = transform as DeformAffine;
|
|
419
|
+
const scale = pair(a.scale, 'scale', where);
|
|
420
|
+
const about = a.about === undefined ? ([0, 0] as [number, number]) : pair(a.about, 'about', where);
|
|
421
|
+
const det = scale[0] * scale[1];
|
|
422
|
+
if (det <= 0) {
|
|
423
|
+
throw new CompileError(
|
|
424
|
+
`${where}: transform affine has scale [${scale[0]}, ${scale[1]}], whose determinant is ${round(det)}. ` +
|
|
425
|
+
'At or below zero the map mirrors or collapses the geometry and EVERY triangle reverses its winding, ' +
|
|
426
|
+
'which is the fold A39_DEFORM_KEEPS_TRIANGLE_WINDING refuses. A positive determinant is what makes this ' +
|
|
427
|
+
'kind incapable of folding a mesh.',
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
for (let v = 0; v < count; v++) {
|
|
431
|
+
offsets[2 * v] = round(widen((scale[0] - 1) * (setup[2 * v] - about[0])));
|
|
432
|
+
offsets[2 * v + 1] = round(widen((scale[1] - 1) * (setup[2 * v + 1] - about[1])));
|
|
433
|
+
}
|
|
434
|
+
identity = round(scale[0] - 1) === 0 && round(scale[1] - 1) === 0;
|
|
435
|
+
identitySpelling = 'scale [1, 1]';
|
|
436
|
+
sampledTo =
|
|
437
|
+
`A scale about a fixed point moves nothing that SITS on it, so every vertex this attachment has lies at ` +
|
|
438
|
+
`about=[${about[0]}, ${about[1]}] on the axis the scale changes — the geometry has collapsed onto the point ` +
|
|
439
|
+
'the key holds still';
|
|
440
|
+
stated = `scale=[${scale[0]}, ${scale[1]}]${a.about === undefined ? '' : ` about=[${about[0]}, ${about[1]}]`}`;
|
|
441
|
+
formula = 'dx = (sx − 1)·(x − ax), dy = (sy − 1)·(y − ay)';
|
|
442
|
+
derived = [`sx − 1 = ${round(scale[0] - 1)}`, `sy − 1 = ${round(scale[1] - 1)}`, `det = sx·sy = ${round(det)} > 0, so no triangle can reverse`];
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
case 'wave': {
|
|
446
|
+
const w = transform as DeformWave;
|
|
447
|
+
const amplitude = num(w.amplitude, 'amplitude', where);
|
|
448
|
+
const wavelength = num(w.wavelength, 'wavelength', where);
|
|
449
|
+
const phase = w.phase === undefined ? 0 : num(w.phase, 'phase', where);
|
|
450
|
+
if (wavelength === 0) {
|
|
451
|
+
throw new CompileError(`${where}: transform wave has wavelength 0; one period cannot be zero long`);
|
|
452
|
+
}
|
|
453
|
+
const [along, axis] = axes(w.along, w.axis, 'wave', where);
|
|
454
|
+
const phaseRad = (phase * Math.PI) / 180;
|
|
455
|
+
const k = (2 * Math.PI) / wavelength;
|
|
456
|
+
for (let v = 0; v < count; v++) {
|
|
457
|
+
const d = amplitude * Math.sin(k * setup[2 * v + along] + phaseRad);
|
|
458
|
+
offsets[2 * v + axis] = round(widen(d));
|
|
459
|
+
offsets[2 * v + (1 - axis)] = 0;
|
|
460
|
+
}
|
|
461
|
+
const an = along === 0 ? 'x' : 'y';
|
|
462
|
+
identity = round(amplitude) === 0;
|
|
463
|
+
identitySpelling = 'amplitude 0';
|
|
464
|
+
// The sampling fact, measured off the array this call was handed rather
|
|
465
|
+
// than inferred from a topology the compiler does not have: it knows which
|
|
466
|
+
// coordinates it read, not where anybody's rows are. The smallest gap
|
|
467
|
+
// between two distinct ones is the finest detail the geometry can carry,
|
|
468
|
+
// and the ratio to it is the rule `gallery/nod`'s README states.
|
|
469
|
+
const distinct = [...new Set(Array.from({ length: count }, (_, v) => setup[2 * v + along]))].sort((p, q) => p - q);
|
|
470
|
+
let gap = Infinity;
|
|
471
|
+
for (let i = 1; i < distinct.length; i++) gap = Math.min(gap, distinct[i] - distinct[i - 1]);
|
|
472
|
+
sampledTo =
|
|
473
|
+
distinct.length < 2
|
|
474
|
+
? `Every vertex sits at ${an}=${distinct[0]}, so one value of the sinusoid covers the whole attachment and ` +
|
|
475
|
+
'this phase is where that one value crosses zero. A wave needs the coordinate it reads to VARY across the ' +
|
|
476
|
+
'geometry; a part that displaces as a whole is a bone, not a deform'
|
|
477
|
+
: `The closest two distinct ${an} coordinates in this attachment are ${round(gap)} apart, and a sinusoid has ` +
|
|
478
|
+
`to be sampled to exist: a wavelength of at least 4x that (${round(4 * gap)}) to read as a wave at all and ` +
|
|
479
|
+
`8x (${round(8 * gap)}) to read as a curve, where this key states ${round(wavelength / gap)}x. At 2x every ` +
|
|
480
|
+
'sample lands on the same pair of phases, and at the zero crossings that pair is (0, 0). `gallery/nod`\'s ' +
|
|
481
|
+
'README carries that rule and the measured table behind it';
|
|
482
|
+
stated = `amplitude=${amplitude} wavelength=${wavelength} phase=${phase} along=${an} axis=${axis === 0 ? 'x' : 'y'}`;
|
|
483
|
+
formula = `d${axis === 0 ? 'x' : 'y'} = amplitude · sin(2π·${an}/wavelength + phase)`;
|
|
484
|
+
derived = [`2π/wavelength = ${round(k)} rad per unit`, `phase = ${round(phaseRad)} rad`];
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
default: {
|
|
488
|
+
const b = transform as DeformBend;
|
|
489
|
+
const amount = num(b.amount, 'amount', where);
|
|
490
|
+
const from = num(b.from, 'from', where);
|
|
491
|
+
const to = num(b.to, 'to', where);
|
|
492
|
+
const power = b.power === undefined ? 2 : num(b.power, 'power', where);
|
|
493
|
+
if (!Number.isInteger(power) || power < 1) {
|
|
494
|
+
throw new CompileError(
|
|
495
|
+
`${where}: transform bend has power ${power}; it is a whole number ≥ 1 (1 is an affine shear, 2 a cantilever ` +
|
|
496
|
+
'that is flat at "from"). A fractional power is not evaluated because it has no value on the side of "from" ' +
|
|
497
|
+
'the part does not reach.',
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
if (to === from) {
|
|
501
|
+
throw new CompileError(`${where}: transform bend has from ${from} and to ${to}; they are the two ends of the bend, so they differ`);
|
|
502
|
+
}
|
|
503
|
+
const [along, axis] = axes(b.along, b.axis, 'bend', where);
|
|
504
|
+
const span = to - from;
|
|
505
|
+
let reach = 0;
|
|
506
|
+
for (let v = 0; v < count; v++) {
|
|
507
|
+
const u = (setup[2 * v + along] - from) / span;
|
|
508
|
+
if (Math.abs(u) > reach) reach = Math.abs(u);
|
|
509
|
+
offsets[2 * v + axis] = round(widen(amount * u ** power));
|
|
510
|
+
offsets[2 * v + (1 - axis)] = 0;
|
|
511
|
+
}
|
|
512
|
+
const an = along === 0 ? 'x' : 'y';
|
|
513
|
+
identity = round(amount) === 0;
|
|
514
|
+
identitySpelling = 'amount 0';
|
|
515
|
+
// `u` is 0 at `from` and 1 at `to`, so the two ways a stated bend vanishes
|
|
516
|
+
// are both statements about where the geometry sits in that span — and
|
|
517
|
+
// both are measured here rather than guessed.
|
|
518
|
+
sampledTo =
|
|
519
|
+
reach === 0
|
|
520
|
+
? `Every vertex sits at ${an}=${from}, which is "from" — the end the bend is anchored at, where the ` +
|
|
521
|
+
'displacement is 0 by construction. The span the key names does not cross the part it is keyed on'
|
|
522
|
+
: `The furthest any vertex reaches into the span is u=${round(reach)} of 1, and u^${power} of that is ` +
|
|
523
|
+
`${(reach ** power).toExponential(3)} — so the part occupies only the flat end of the curve. Move "to" to ` +
|
|
524
|
+
'where the geometry actually ends, or lower the power';
|
|
525
|
+
stated = `amount=${amount} from=${from} to=${to} power=${power} along=${an} axis=${axis === 0 ? 'x' : 'y'}`;
|
|
526
|
+
formula = `d${axis === 0 ? 'x' : 'y'} = amount · u^${power}, u = (${an} − from) / (to − from)`;
|
|
527
|
+
derived = [
|
|
528
|
+
`span = to − from = ${round(span)}`,
|
|
529
|
+
power === 1
|
|
530
|
+
? 'power 1 is an affine shear (det = 1), so no triangle can reverse'
|
|
531
|
+
: `power ${power} is not affine — the gradient at u is ${power}·amount·u^${power - 1}/span, so it is 0 at "from"`,
|
|
532
|
+
];
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// -- a model the geometry sampled to nothing (issue #350) ------------------
|
|
538
|
+
//
|
|
539
|
+
// The three refusals above catch a parameter that cannot mean what it says —
|
|
540
|
+
// `wavelength: 0`, a determinant at or below 0, a radius inside the part. This
|
|
541
|
+
// is the same principle one step later: every parameter is individually legal,
|
|
542
|
+
// and the model still evaluates to a run of zeros. The key then claims a
|
|
543
|
+
// deformation, emits nothing, and *gates green* — `A35` is right that the run
|
|
544
|
+
// fits and `A39` is right that no triangle moved, so neither can see it and
|
|
545
|
+
// this is the only place it can be said.
|
|
546
|
+
//
|
|
547
|
+
// ⭐ **The distinguishing condition is where the identity is stated.** A key
|
|
548
|
+
// that MEANS the setup pose says so in its own parameters — `degrees: 0` (or
|
|
549
|
+
// any whole revolution), `amplitude: 0`, `amount: 0`, `scale: [1, 1]` — or
|
|
550
|
+
// carries no run at all, which is the format's own `{ "t": … }`. Those pass.
|
|
551
|
+
// What is refused is the pair that cannot both be true: parameters that state
|
|
552
|
+
// a deformation, and an evaluation that is the identity. The two are not the
|
|
553
|
+
// same event, and before this they printed the same line.
|
|
554
|
+
//
|
|
555
|
+
// `count > 0` is not defensive noise: `every` on an empty array is `true`, so
|
|
556
|
+
// an attachment with no vertices would otherwise be refused with a message
|
|
557
|
+
// about arithmetic that never ran.
|
|
558
|
+
if (count > 0 && !identity && offsets.every((d) => d === 0)) {
|
|
559
|
+
throw new CompileError(
|
|
560
|
+
`${where}: transform ${kind} states ${stated}, and every one of this attachment's ${count} vertices evaluates ` +
|
|
561
|
+
`to an offset of 0 — the largest value the closed form reached at any of them is ${band.toExponential(3)}, ` +
|
|
562
|
+
'which quantises to 0 on the 1e-6 grid a model is evaluated on. So the key states a deformation and ' +
|
|
563
|
+
`emits the identity, and nothing downstream can tell it apart from a key that meant the setup pose. ` +
|
|
564
|
+
`${sampledTo}. A key that MEANS the identity states it in its own parameters (${identitySpelling}) or carries ` +
|
|
565
|
+
'no run at all.',
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
let maxOffset = 0;
|
|
570
|
+
let maxOffsetVertex = 0;
|
|
571
|
+
for (let v = 0; v < count; v++) {
|
|
572
|
+
const m = Math.hypot(offsets[2 * v], offsets[2 * v + 1]);
|
|
573
|
+
if (m > maxOffset) {
|
|
574
|
+
maxOffset = m;
|
|
575
|
+
maxOffsetVertex = v;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
kind: kind as DeformTransform['kind'],
|
|
580
|
+
stated,
|
|
581
|
+
derived,
|
|
582
|
+
formula,
|
|
583
|
+
vertexCount: count,
|
|
584
|
+
maxOffset: round(maxOffset),
|
|
585
|
+
maxOffsetVertex,
|
|
586
|
+
offsets,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** One required finite number, refused by field name rather than compiled as a NaN. */
|
|
591
|
+
function num(value: unknown, field: string, where: string): number {
|
|
592
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
593
|
+
throw new CompileError(`${where}: transform field "${field}" is ${JSON.stringify(value)}; it is a finite number the spec has to state`);
|
|
594
|
+
}
|
|
595
|
+
return value;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** One required `[a, b]`. */
|
|
599
|
+
function pair(value: unknown, field: string, where: string): [number, number] {
|
|
600
|
+
if (!Array.isArray(value) || value.length !== 2) {
|
|
601
|
+
throw new CompileError(`${where}: transform field "${field}" is ${JSON.stringify(value)}; it is a pair, [x, y]`);
|
|
602
|
+
}
|
|
603
|
+
return [num(value[0], `${field}[0]`, where), num(value[1], `${field}[1]`, where)];
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* The two axis fields, as indices into an `x, y` pair.
|
|
608
|
+
*
|
|
609
|
+
* `along === axis` is refused: a displacement of x driven by x is a stretch
|
|
610
|
+
* along one axis, which `affine` states as a scale — and states with a
|
|
611
|
+
* determinant, so it carries the proof that this kind cannot.
|
|
612
|
+
*/
|
|
613
|
+
function axes(along: unknown, axis: unknown, kind: string, where: string): [0 | 1, 0 | 1] {
|
|
614
|
+
for (const [name, value] of [
|
|
615
|
+
['along', along],
|
|
616
|
+
['axis', axis],
|
|
617
|
+
] as const) {
|
|
618
|
+
if (value !== 'x' && value !== 'y') {
|
|
619
|
+
throw new CompileError(`${where}: transform ${kind} has ${name}=${JSON.stringify(value)}; it is "x" or "y"`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (along === axis) {
|
|
623
|
+
throw new CompileError(
|
|
624
|
+
`${where}: transform ${kind} reads ${String(along)} and displaces ${String(axis)} — the same coordinate, which is a ` +
|
|
625
|
+
'stretch rather than a bend or a wave. An affine scale states that, and states the determinant that proves it ' +
|
|
626
|
+
'keeps every triangle\'s winding.',
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
return [along === 'x' ? 0 : 1, axis === 'x' ? 0 : 1];
|
|
630
|
+
}
|