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/keyorder.ts
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The order the Spine editor writes an object's keys in, per kind of object —
|
|
3
|
+
* and the one pass that puts every emitted object into it (issue #716).
|
|
4
|
+
*
|
|
5
|
+
* ## Why the emitter has an opinion about something JSON does not mean
|
|
6
|
+
*
|
|
7
|
+
* spine-core reads every object below by name (`SkeletonJson` asks a bone map
|
|
8
|
+
* for `"rotation"`, never for its third key), so a key's position reaches no
|
|
9
|
+
* parsed value and no gate can see it. What it reaches is the text: a rebuild of
|
|
10
|
+
* an editor export that writes `length, x, y, rotation` where the export wrote
|
|
11
|
+
* `length, rotation, x, y` is not the export, and #716's pass line — the rebuild
|
|
12
|
+
* and the export identical in canonical form, `JSON.stringify(JSON.parse(text),
|
|
13
|
+
* null, 2)` — is decided on text. Measured at `af8b082` over the twelve editor
|
|
14
|
+
* exports under `examples/`: **280** objects of the rebuilds carried their shared
|
|
15
|
+
* keys in another order — 269 in seven kinds of field order (region attachments
|
|
16
|
+
* 125, bones 87, the three constraint types 46, slots 7, the top level 4) and 11
|
|
17
|
+
* skins whose `attachments` map is keyed by slot NAME, which is not a field
|
|
18
|
+
* order and is `compile.ts`'s `editorSlotKeyOrder`, not a row here.
|
|
19
|
+
*
|
|
20
|
+
* ## Where the rows come from
|
|
21
|
+
*
|
|
22
|
+
* Every row is **read off the twelve exports**, never off memory of the editor's
|
|
23
|
+
* source: for each kind, every export object's key sequence is a constraint
|
|
24
|
+
* (*these keys, in this order*), and the row is the one order that satisfies all
|
|
25
|
+
* of them at once. A writer with a fixed field order produces sequences that are
|
|
26
|
+
* all subsequences of one total order, and the twelve are exactly that — **no
|
|
27
|
+
* two exports contradict each other on any kind**, which is a measurement and
|
|
28
|
+
* not an assumption: an export that did would leave that kind with no row to
|
|
29
|
+
* derive. `IG77` in `selftest.ts` re-takes it from the files every run and prints
|
|
30
|
+
* the counts: every object of every row's kind is in its row's order, and every
|
|
31
|
+
* key an export writes for that kind is in the row.
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ **Where the exports order a pair, the row is theirs; where they do not, it
|
|
34
|
+
* is rigc's, and that is a choice rather than a measurement.** Two keys no
|
|
35
|
+
* export object carries together have no measured order — a transform
|
|
36
|
+
* constraint's `rotation` and `x`, an ik key's `time` and `mix` — and the row
|
|
37
|
+
* places them the way `compile.ts` already emitted them at `af8b082`. So such a
|
|
38
|
+
* pair does not move, and nothing here claims the editor agrees. The pairs that
|
|
39
|
+
* are rigc's are: `animation` ik/transform/physics/attachments before
|
|
40
|
+
* `drawOrder`; `ik constraint` `mix` before `bendPositive`; `ik key` `time`
|
|
41
|
+
* before `mix`, `mix` before `softness`; `physics constraint` `x`, `y` before
|
|
42
|
+
* `rotate`; `transform constraint` `rotation` before `x`, `localSource` and
|
|
43
|
+
* `localTarget` before `mixRotate`, `mixRotate`, `mixX` before `mixY` before
|
|
44
|
+
* `mixScaleX`; `transform key` `time` before `mixRotate` before `mixX`.
|
|
45
|
+
*
|
|
46
|
+
* ## What a row does not list
|
|
47
|
+
*
|
|
48
|
+
* - **A kind with no row keeps rigc's order whole.** Those are the kinds no
|
|
49
|
+
* export writes with two keys or more: a linked mesh, a path or point
|
|
50
|
+
* attachment, a sequence, the path and slider constraints, an event
|
|
51
|
+
* definition, and the keys of the `scalex`/`scaley`/`shearx`/`sheary`/
|
|
52
|
+
* `inherit`, `rgb`/`alpha`/`rgba2`/`rgb2`, `sequence`, path, slider and
|
|
53
|
+
* physics `reset`/`gravity`/`strength` timelines. Unmeasured is not
|
|
54
|
+
* certified, so they are left exactly as the constructors build them.
|
|
55
|
+
* - **A key a row does not list keeps its own position.** A bone's `shearX`, a
|
|
56
|
+
* region's `name` and `path`, a header's `fps`: the row's keys are permuted
|
|
57
|
+
* among the places they occupy and every other key stays at the index the
|
|
58
|
+
* constructor gave it. ⚠️ The obvious alternative — unlisted keys after the
|
|
59
|
+
* listed ones — was rejected on a measured case: the editor wrote the 14
|
|
60
|
+
* physics `strength` keys of the corpus with `value` alone (every one at
|
|
61
|
+
* t=0), so that row would be `[value]` and `time`, which the editor writes
|
|
62
|
+
* first in every key kind the twelve carry it in, would be moved to the end.
|
|
63
|
+
* Rows of one key order nothing and are not rows.
|
|
64
|
+
*
|
|
65
|
+
* ## What is not a row, and why
|
|
66
|
+
*
|
|
67
|
+
* An object keyed by NAMES rather than fields — a bone's timelines, a slot's
|
|
68
|
+
* timelines, a transform constraint's `properties` and each `to` — is not
|
|
69
|
+
* reordered here, and for a reason the text cannot see: `SkeletonJson` builds an
|
|
70
|
+
* animation's timelines in the order it iterates those maps, and two timelines
|
|
71
|
+
* that write one property (`translate` and `translatex` on one bone) apply in
|
|
72
|
+
* that order. Their order is the runtime's, so it is not a key's position, and
|
|
73
|
+
* rigc already emits the export's own on all twelve (0 of 514 bone-timeline
|
|
74
|
+
* maps differ). `animations`, `skins` and a skin's slot keys are the three
|
|
75
|
+
* name-keyed collections the emitter sorts, and `compile.ts` owns all three.
|
|
76
|
+
*
|
|
77
|
+
* 🔒 Pure: no clock, no randomness, and the result depends only on each
|
|
78
|
+
* object's own key set and the order the constructor built it in — which is
|
|
79
|
+
* what `A18_DETERMINISTIC_EMIT` needs, and a second compile reproduces.
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/** One order per kind of object: the keys the editor writes, in the order it writes them. */
|
|
83
|
+
export type KeyOrderTable = Readonly<Record<string, readonly string[]>>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The editor's key order per kind, derived from the twelve exports (see the
|
|
87
|
+
* header). A kind is named by where the object sits: `bone`, `region
|
|
88
|
+
* attachment`, `ik constraint`, `bone rotate key` — `forEachKindedObject` is the
|
|
89
|
+
* one place that names them.
|
|
90
|
+
*/
|
|
91
|
+
export const EDITOR_KEY_ORDER: KeyOrderTable = {
|
|
92
|
+
'top level': ['skeleton', 'bones', 'slots', 'constraints', 'skins', 'events', 'animations'],
|
|
93
|
+
header: ['hash', 'spine', 'x', 'y', 'width', 'height', 'images', 'audio'],
|
|
94
|
+
bone: ['name', 'parent', 'length', 'rotation', 'x', 'y', 'scaleX', 'scaleY', 'inherit', 'color', 'icon'],
|
|
95
|
+
slot: ['name', 'bone', 'color', 'attachment', 'blend'],
|
|
96
|
+
'ik constraint': ['type', 'name', 'target', 'bones', 'mix', 'bendPositive'],
|
|
97
|
+
'transform constraint': [
|
|
98
|
+
'type',
|
|
99
|
+
'name',
|
|
100
|
+
'source',
|
|
101
|
+
'bones',
|
|
102
|
+
'rotation',
|
|
103
|
+
'x',
|
|
104
|
+
'y',
|
|
105
|
+
'properties',
|
|
106
|
+
'localSource',
|
|
107
|
+
'localTarget',
|
|
108
|
+
'mixRotate',
|
|
109
|
+
'mixX',
|
|
110
|
+
'mixY',
|
|
111
|
+
'mixScaleX',
|
|
112
|
+
'mixShearY',
|
|
113
|
+
],
|
|
114
|
+
'physics constraint': ['type', 'name', 'bone', 'x', 'y', 'rotate', 'damping'],
|
|
115
|
+
skin: ['name', 'attachments'],
|
|
116
|
+
'region attachment': ['x', 'y', 'scaleX', 'scaleY', 'rotation', 'width', 'height'],
|
|
117
|
+
'mesh attachment': ['type', 'uvs', 'triangles', 'vertices', 'hull', 'edges', 'width', 'height'],
|
|
118
|
+
'boundingbox attachment': ['type', 'vertexCount', 'vertices'],
|
|
119
|
+
'clipping attachment': ['type', 'end', 'vertexCount', 'vertices', 'color'],
|
|
120
|
+
animation: ['slots', 'bones', 'ik', 'transform', 'physics', 'attachments', 'drawOrder', 'events'],
|
|
121
|
+
'bone rotate key': ['time', 'value', 'curve'],
|
|
122
|
+
'bone translate key': ['time', 'x', 'y', 'curve'],
|
|
123
|
+
'bone translatex key': ['time', 'value', 'curve'],
|
|
124
|
+
'bone translatey key': ['time', 'value', 'curve'],
|
|
125
|
+
'bone scale key': ['time', 'x', 'y', 'curve'],
|
|
126
|
+
'bone shear key': ['time', 'x', 'y', 'curve'],
|
|
127
|
+
'slot attachment key': ['time', 'name'],
|
|
128
|
+
'slot rgba key': ['time', 'color', 'curve'],
|
|
129
|
+
'ik key': ['time', 'mix', 'softness', 'bendPositive', 'curve'],
|
|
130
|
+
'transform key': ['time', 'mixRotate', 'mixX', 'mixY', 'curve'],
|
|
131
|
+
'physics damping key': ['time', 'value'],
|
|
132
|
+
'physics inertia key': ['time', 'value'],
|
|
133
|
+
'physics mass key': ['time', 'value'],
|
|
134
|
+
'physics mix key': ['time', 'value', 'curve'],
|
|
135
|
+
'physics wind key': ['time', 'value'],
|
|
136
|
+
'attachment deform key': ['time', 'offset', 'vertices', 'curve'],
|
|
137
|
+
'drawOrder key': ['time', 'offsets'],
|
|
138
|
+
'drawOrder offset': ['slot', 'offset'],
|
|
139
|
+
'event key': ['time', 'name'],
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
type Rec = Record<string, unknown>;
|
|
143
|
+
|
|
144
|
+
function isRecord(value: unknown): value is Rec {
|
|
145
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function records(value: unknown): Rec[] {
|
|
149
|
+
return Array.isArray(value) ? value.filter(isRecord) : [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The animation groups that hold `<target>.<timeline> = keys[]`, and the word a key's kind is named by. */
|
|
153
|
+
const TIMELINE_FAMILIES: ReadonlyArray<readonly [group: string, family: string]> = [
|
|
154
|
+
['bones', 'bone'],
|
|
155
|
+
['slots', 'slot'],
|
|
156
|
+
['path', 'path'],
|
|
157
|
+
['physics', 'physics'],
|
|
158
|
+
['slider', 'slider'],
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
/** The groups where the constraint IS the timeline: `<group>.<constraint> = keys[]`. */
|
|
162
|
+
const CONSTRAINT_KEY_GROUPS: readonly string[] = ['ik', 'transform'];
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Visit every object of a Spine 4.3 skeleton file whose keys are FIELDS, with
|
|
166
|
+
* the kind it is and where it sits — parents before their children.
|
|
167
|
+
*
|
|
168
|
+
* The kind is read off the object's position in the format, plus `type` where
|
|
169
|
+
* the format branches on it (a constraint, an attachment). Name-keyed maps are
|
|
170
|
+
* walked through and not visited: they are not a field order (see the header).
|
|
171
|
+
*/
|
|
172
|
+
export function forEachKindedObject(skeleton: unknown, visit: (kind: string, object: Rec, path: string) => void): void {
|
|
173
|
+
if (!isRecord(skeleton)) return;
|
|
174
|
+
visit('top level', skeleton, '');
|
|
175
|
+
if (isRecord(skeleton.skeleton)) visit('header', skeleton.skeleton, '.skeleton');
|
|
176
|
+
records(skeleton.bones).forEach((bone, i) => visit('bone', bone, `.bones[${i}]`));
|
|
177
|
+
records(skeleton.slots).forEach((slot, i) => visit('slot', slot, `.slots[${i}]`));
|
|
178
|
+
records(skeleton.constraints).forEach((constraint, i) =>
|
|
179
|
+
visit(`${String(constraint.type)} constraint`, constraint, `.constraints[${i}]`),
|
|
180
|
+
);
|
|
181
|
+
records(skeleton.skins).forEach((skin, i) => {
|
|
182
|
+
const at = `.skins[${i}]`;
|
|
183
|
+
visit('skin', skin, at);
|
|
184
|
+
if (!isRecord(skin.attachments)) return;
|
|
185
|
+
for (const [slot, placeholders] of Object.entries(skin.attachments)) {
|
|
186
|
+
if (!isRecord(placeholders)) continue;
|
|
187
|
+
for (const [placeholder, attachment] of Object.entries(placeholders)) {
|
|
188
|
+
if (!isRecord(attachment)) continue;
|
|
189
|
+
const where = `${at}.attachments.${slot}.${placeholder}`;
|
|
190
|
+
visit(`${typeof attachment.type === 'string' ? attachment.type : 'region'} attachment`, attachment, where);
|
|
191
|
+
if (isRecord(attachment.sequence)) visit('sequence', attachment.sequence, `${where}.sequence`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
if (isRecord(skeleton.events)) {
|
|
196
|
+
for (const [name, event] of Object.entries(skeleton.events)) if (isRecord(event)) visit('event', event, `.events.${name}`);
|
|
197
|
+
}
|
|
198
|
+
if (!isRecord(skeleton.animations)) return;
|
|
199
|
+
for (const [name, animation] of Object.entries(skeleton.animations)) {
|
|
200
|
+
if (!isRecord(animation)) continue;
|
|
201
|
+
const at = `.animations.${name}`;
|
|
202
|
+
visit('animation', animation, at);
|
|
203
|
+
const timelines = (family: string, byTimeline: unknown, where: string): void => {
|
|
204
|
+
if (!isRecord(byTimeline)) return;
|
|
205
|
+
for (const [timeline, keys] of Object.entries(byTimeline)) {
|
|
206
|
+
records(keys).forEach((key, i) => visit(`${family} ${timeline} key`, key, `${where}.${timeline}[${i}]`));
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
for (const [group, family] of TIMELINE_FAMILIES) {
|
|
210
|
+
const byTarget = animation[group];
|
|
211
|
+
if (!isRecord(byTarget)) continue;
|
|
212
|
+
for (const [target, byTimeline] of Object.entries(byTarget)) timelines(family, byTimeline, `${at}.${group}.${target}`);
|
|
213
|
+
}
|
|
214
|
+
for (const group of CONSTRAINT_KEY_GROUPS) {
|
|
215
|
+
const byConstraint = animation[group];
|
|
216
|
+
if (!isRecord(byConstraint)) continue;
|
|
217
|
+
for (const [constraint, keys] of Object.entries(byConstraint)) {
|
|
218
|
+
records(keys).forEach((key, i) => visit(`${group} key`, key, `${at}.${group}.${constraint}[${i}]`));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (isRecord(animation.attachments)) {
|
|
222
|
+
for (const [skin, bySlot] of Object.entries(animation.attachments)) {
|
|
223
|
+
if (!isRecord(bySlot)) continue;
|
|
224
|
+
for (const [slot, byAttachment] of Object.entries(bySlot)) {
|
|
225
|
+
if (!isRecord(byAttachment)) continue;
|
|
226
|
+
for (const [attachment, byTimeline] of Object.entries(byAttachment)) {
|
|
227
|
+
timelines('attachment', byTimeline, `${at}.attachments.${skin}.${slot}.${attachment}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
records(animation.drawOrder).forEach((key, i) => {
|
|
233
|
+
visit('drawOrder key', key, `${at}.drawOrder[${i}]`);
|
|
234
|
+
records(key.offsets).forEach((offset, j) => visit('drawOrder offset', offset, `${at}.drawOrder[${i}].offsets[${j}]`));
|
|
235
|
+
});
|
|
236
|
+
records(animation.events).forEach((key, i) => visit('event key', key, `${at}.events[${i}]`));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Put one object's keys in its row's order, in place: the keys the row lists are
|
|
242
|
+
* permuted among the positions they hold, and every other key stays where it is.
|
|
243
|
+
*/
|
|
244
|
+
function arrange(object: Rec, rank: ReadonlyMap<string, number>): void {
|
|
245
|
+
const keys = Object.keys(object);
|
|
246
|
+
const listed = keys.filter((key) => rank.has(key)).sort((a, b) => rank.get(a)! - rank.get(b)!);
|
|
247
|
+
let next = 0;
|
|
248
|
+
const order = keys.map((key) => (rank.has(key) ? listed[next++] : key));
|
|
249
|
+
if (order.every((key, i) => key === keys[i])) return;
|
|
250
|
+
const entries = order.map((key) => [key, object[key]] as const);
|
|
251
|
+
for (const key of keys) delete object[key];
|
|
252
|
+
for (const [key, value] of entries) object[key] = value;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Every object of `skeleton` whose kind has a row, put into that row's order —
|
|
257
|
+
* in place, and returned. `table` is a parameter so the selftest can hand it a
|
|
258
|
+
* row reversed and watch its own check go red; the emitter passes nothing.
|
|
259
|
+
*/
|
|
260
|
+
export function inEditorKeyOrder<T extends object>(skeleton: T, table: KeyOrderTable = EDITOR_KEY_ORDER): T {
|
|
261
|
+
const ranks = new Map<string, ReadonlyMap<string, number>>();
|
|
262
|
+
forEachKindedObject(skeleton, (kind, object) => {
|
|
263
|
+
const row = table[kind];
|
|
264
|
+
if (row === undefined) return;
|
|
265
|
+
let rank = ranks.get(kind);
|
|
266
|
+
if (rank === undefined) {
|
|
267
|
+
rank = new Map(row.map((key, i) => [key, i]));
|
|
268
|
+
ranks.set(kind, rank);
|
|
269
|
+
}
|
|
270
|
+
arrange(object, rank);
|
|
271
|
+
});
|
|
272
|
+
return skeleton;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// the keys the 4.3 parser reads back the same way without them (#716 tranche 3)
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
//
|
|
279
|
+
// The editor writes a key only when its value is not the one `SkeletonJson`
|
|
280
|
+
// reads in its absence, and rigc wrote every key it was given. Measured at
|
|
281
|
+
// `d48c505` over the twelve editor exports under `examples/`: **2,338** keys of
|
|
282
|
+
// the rebuilds stated a value the export leaves to the parser — `time: 0` on a
|
|
283
|
+
// first key, a rotate key's `value: 0`, a scale key's `x: 1`, an ik key's
|
|
284
|
+
// `mix: 1` — and **87** attachment keys wrote `"name": null`. Each one loads
|
|
285
|
+
// the same `SkeletonData` with the key and without it, which is why no parse
|
|
286
|
+
// and no gate could see any of them.
|
|
287
|
+
//
|
|
288
|
+
// ## Why this is a table and not a parse
|
|
289
|
+
//
|
|
290
|
+
// The parser is the only authority on what a default is, and this table is
|
|
291
|
+
// not a second one: it is a table of the parser's readings that the selftest
|
|
292
|
+
// holds to the parser row by row (`S103` on the in-tree builds, `IG82` on the corpus) by loading an
|
|
293
|
+
// object of each kind with the key at the row's value and without it, and
|
|
294
|
+
// comparing everything `SkeletonData` holds — and then one float32 step off,
|
|
295
|
+
// which must differ. It cannot BE a parse: `compile.ts` must not link the
|
|
296
|
+
// runtime (`CLAUDE.md` *Conventions*, held by `CUR07`), and an emitter that
|
|
297
|
+
// asked the parser what to write would be the gate answering its own question.
|
|
298
|
+
//
|
|
299
|
+
// ## What a row holds
|
|
300
|
+
//
|
|
301
|
+
// - a **constant**: the value `getValue(map, key, default)` falls back to. A
|
|
302
|
+
// colour row is `ffffffff` because the parser leaves the colour white when
|
|
303
|
+
// the key is absent; a mode is spelled the way rigc spells it (`normal`,
|
|
304
|
+
// `percent`), which `Utils.enumValue` reads alike with either first letter.
|
|
305
|
+
// - `{ field, only }`: another field of the same object, as the parser reads it
|
|
306
|
+
// — a transform key's `mixY` falls back to its own `mixX`, which falls back to
|
|
307
|
+
// 1 — and, with `only`, the one value at which the key is left out. `only` is
|
|
308
|
+
// the editor's, measured: `sack-pro` writes `mixX: 0, mixY: 0` on six
|
|
309
|
+
// transform keys where the parser would read an absent `mixY` as that same
|
|
310
|
+
// `0`, and omits both where both are `1`. So `mixY` is left out only where it
|
|
311
|
+
// is `1` AND `mixX` reads `1` — the two rules' intersection, which is always
|
|
312
|
+
// a key the parser reads back the same and never one the editor writes.
|
|
313
|
+
// - `{ previous, first }`: the same field of the timeline's previous key, and
|
|
314
|
+
// `first` on key 0 — a sequence key's `delay` is the parser's `lastDelay`.
|
|
315
|
+
//
|
|
316
|
+
// ⚠️ **A kind with no row keeps every key, and so does a key its row does not
|
|
317
|
+
// list.** A row is here only when an object of its kind is in a build the
|
|
318
|
+
// selftest loads, because a row nothing loads is a claim about the parser
|
|
319
|
+
// nobody has checked, and a wrong one changes a loaded value in silence — where
|
|
320
|
+
// a missing one costs a key the parser reads the same either way. `S101` is what
|
|
321
|
+
// finds a missing one: it deletes every key of every in-tree build in turn and
|
|
322
|
+
// names each the parser reads back unchanged.
|
|
323
|
+
//
|
|
324
|
+
// ⚠️ **Not rows, on purpose:** a transform CONSTRAINT's `mixY` and
|
|
325
|
+
// `mixScaleY`. Their fallback is `setup.mixX` / `setup.mixScaleX`, and the
|
|
326
|
+
// parser reads those only for a property the constraint drives — so on a
|
|
327
|
+
// constraint that drives `y` and not `x` the fallback is the pose's own initial
|
|
328
|
+
// `0` (`TransformConstraintPose.js:34`), not `mixX`'s `1`. A `{ field }` row
|
|
329
|
+
// there was measured wrong on the corpus: it dropped `8-follow-through-pro-
|
|
330
|
+
// ball`'s `mixY: 1` on two constraints that drive `y` alone, they loaded at
|
|
331
|
+
// `mixY: 0`, and `A48` refused the build. A transform KEY reads every mix
|
|
332
|
+
// unconditionally, so its `mixY` row stands. Also not rows: the header's `x`, `y` and `fps`
|
|
333
|
+
// (`SkeletonJson.js:70` assigns them raw, so an absent origin loads as
|
|
334
|
+
// `undefined` rather than `0` — there is no parser default for it to equal);
|
|
335
|
+
// a region's `path` and an attachment's `name` (their fallbacks are the
|
|
336
|
+
// attachment's own name and placeholder, which rigc writes only where they
|
|
337
|
+
// differ); an event key's payload (its fallback is its event definition's, a
|
|
338
|
+
// value held in another object). And two fallbacks the parser does have but
|
|
339
|
+
// nothing here can load: a slider's `time`, read only on a slider with no
|
|
340
|
+
// `bone` (no build or export carries one), and a sequence's `digits: 0`, which
|
|
341
|
+
// no atlas a build resolves against can be read with — the frame names change
|
|
342
|
+
// with it and the parse throws either way. Both are written as stated.
|
|
343
|
+
//
|
|
344
|
+
// 🔒 Pure, and a function of each object's keys and values alone, so
|
|
345
|
+
// `A18_DETERMINISTIC_EMIT` still compares two identical texts.
|
|
346
|
+
|
|
347
|
+
/** A field's value where it is absent, as the 4.3 parser reads it. */
|
|
348
|
+
export type ParserDefault =
|
|
349
|
+
| number
|
|
350
|
+
| boolean
|
|
351
|
+
| string
|
|
352
|
+
| null
|
|
353
|
+
| { readonly field: string; readonly only?: number }
|
|
354
|
+
| { readonly previous: string; readonly first: number };
|
|
355
|
+
|
|
356
|
+
/** One row per kind of object: each field whose absence the parser reads as a value, and that value. */
|
|
357
|
+
export type ParserDefaultTable = Readonly<Record<string, Readonly<Record<string, ParserDefault>>>>;
|
|
358
|
+
|
|
359
|
+
const TIME_ONLY: Readonly<Record<string, ParserDefault>> = { time: 0 };
|
|
360
|
+
const VALUE_AT_ZERO: Readonly<Record<string, ParserDefault>> = { time: 0, value: 0 };
|
|
361
|
+
const VALUE_AT_ONE: Readonly<Record<string, ParserDefault>> = { time: 0, value: 1 };
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* The 4.3 parser's defaults, per kind of object — see the header above. Each
|
|
365
|
+
* is a `getValue` fallback of `SkeletonJson.js` in the linked `spine-core`, and
|
|
366
|
+
* the selftest loads every one of them rather than believing this list.
|
|
367
|
+
*/
|
|
368
|
+
export const PARSER_DEFAULTS: ParserDefaultTable = {
|
|
369
|
+
header: { referenceScale: 100 },
|
|
370
|
+
bone: {
|
|
371
|
+
length: 0,
|
|
372
|
+
rotation: 0,
|
|
373
|
+
x: 0,
|
|
374
|
+
y: 0,
|
|
375
|
+
scaleX: 1,
|
|
376
|
+
scaleY: 1,
|
|
377
|
+
shearX: 0,
|
|
378
|
+
shearY: 0,
|
|
379
|
+
inherit: 'normal',
|
|
380
|
+
skin: false,
|
|
381
|
+
iconSize: 1,
|
|
382
|
+
iconRotation: 0,
|
|
383
|
+
},
|
|
384
|
+
slot: { color: 'ffffffff', attachment: null, blend: 'normal', visible: true },
|
|
385
|
+
'ik constraint': { skin: false, mix: 1, softness: 0, bendPositive: true, compress: false, stretch: false },
|
|
386
|
+
'transform constraint': {
|
|
387
|
+
skin: false,
|
|
388
|
+
localSource: false,
|
|
389
|
+
localTarget: false,
|
|
390
|
+
additive: false,
|
|
391
|
+
clamp: false,
|
|
392
|
+
rotation: 0,
|
|
393
|
+
x: 0,
|
|
394
|
+
y: 0,
|
|
395
|
+
scaleX: 0,
|
|
396
|
+
scaleY: 0,
|
|
397
|
+
shearY: 0,
|
|
398
|
+
mixRotate: 1,
|
|
399
|
+
mixX: 1,
|
|
400
|
+
mixScaleX: 1,
|
|
401
|
+
mixShearY: 1,
|
|
402
|
+
},
|
|
403
|
+
'path constraint': {
|
|
404
|
+
skin: false,
|
|
405
|
+
positionMode: 'percent',
|
|
406
|
+
spacingMode: 'length',
|
|
407
|
+
rotateMode: 'tangent',
|
|
408
|
+
rotation: 0,
|
|
409
|
+
position: 0,
|
|
410
|
+
spacing: 0,
|
|
411
|
+
mixRotate: 1,
|
|
412
|
+
mixX: 1,
|
|
413
|
+
mixY: { field: 'mixX', only: 1 },
|
|
414
|
+
},
|
|
415
|
+
'physics constraint': {
|
|
416
|
+
skin: false,
|
|
417
|
+
x: 0,
|
|
418
|
+
y: 0,
|
|
419
|
+
rotate: 0,
|
|
420
|
+
scaleX: 0,
|
|
421
|
+
shearX: 0,
|
|
422
|
+
limit: 5000,
|
|
423
|
+
fps: 60,
|
|
424
|
+
inertia: 0.5,
|
|
425
|
+
strength: 100,
|
|
426
|
+
damping: 0.85,
|
|
427
|
+
mass: 1,
|
|
428
|
+
wind: 0,
|
|
429
|
+
gravity: 0,
|
|
430
|
+
mix: 1,
|
|
431
|
+
inertiaGlobal: false,
|
|
432
|
+
strengthGlobal: false,
|
|
433
|
+
dampingGlobal: false,
|
|
434
|
+
massGlobal: false,
|
|
435
|
+
windGlobal: false,
|
|
436
|
+
gravityGlobal: false,
|
|
437
|
+
mixGlobal: false,
|
|
438
|
+
},
|
|
439
|
+
'slider constraint': { skin: false, additive: false, loop: false, mix: 1, from: 0, to: 0, scale: 1, max: 0, local: false },
|
|
440
|
+
'region attachment': { x: 0, y: 0, scaleX: 1, scaleY: 1, rotation: 0, color: 'ffffffff' },
|
|
441
|
+
'mesh attachment': { color: 'ffffffff', hull: 0, width: 0, height: 0 },
|
|
442
|
+
'path attachment': { closed: false, constantSpeed: true },
|
|
443
|
+
'clipping attachment': { convex: false, inverse: false },
|
|
444
|
+
sequence: { start: 1, setup: 0 },
|
|
445
|
+
event: { int: 0, float: 0, string: '', audio: null },
|
|
446
|
+
'bone rotate key': VALUE_AT_ZERO,
|
|
447
|
+
'bone translate key': { time: 0, x: 0, y: 0 },
|
|
448
|
+
'bone translatex key': VALUE_AT_ZERO,
|
|
449
|
+
'bone translatey key': VALUE_AT_ZERO,
|
|
450
|
+
'bone scale key': { time: 0, x: 1, y: 1 },
|
|
451
|
+
'bone scalex key': VALUE_AT_ONE,
|
|
452
|
+
'bone scaley key': VALUE_AT_ONE,
|
|
453
|
+
'bone shear key': { time: 0, x: 0, y: 0 },
|
|
454
|
+
'slot attachment key': { time: 0, name: null },
|
|
455
|
+
'slot rgba key': TIME_ONLY,
|
|
456
|
+
'ik key': { time: 0, mix: 1, softness: 0, bendPositive: true, compress: false, stretch: false },
|
|
457
|
+
'transform key': { time: 0, mixRotate: 1, mixX: 1, mixY: { field: 'mixX', only: 1 }, mixScaleX: 1, mixScaleY: 1, mixShearY: 1 },
|
|
458
|
+
'path position key': VALUE_AT_ZERO,
|
|
459
|
+
'physics damping key': VALUE_AT_ZERO,
|
|
460
|
+
'physics inertia key': VALUE_AT_ZERO,
|
|
461
|
+
'physics mass key': VALUE_AT_ZERO,
|
|
462
|
+
'physics strength key': VALUE_AT_ZERO,
|
|
463
|
+
'physics wind key': VALUE_AT_ZERO,
|
|
464
|
+
'physics mix key': VALUE_AT_ONE,
|
|
465
|
+
'attachment deform key': { time: 0, offset: 0 },
|
|
466
|
+
'attachment sequence key': { time: 0, index: 0, mode: 'hold', delay: { previous: 'delay', first: 0 } },
|
|
467
|
+
'drawOrder key': TIME_ONLY,
|
|
468
|
+
'event key': TIME_ONLY,
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* The `referenceScale` a header stating none is read as: the parser's
|
|
473
|
+
* `getValue(skeletonMap, "referenceScale", 100)`, taken from
|
|
474
|
+
* `PARSER_DEFAULTS.header` (which the selftest loads rather than believes) so
|
|
475
|
+
* the model and the omission pass cannot name two different numbers.
|
|
476
|
+
* `compile.ts` gives the model this value when the rig spec states none.
|
|
477
|
+
*
|
|
478
|
+
* Here rather than in `src/emit_spine.ts`, which defined it until issue
|
|
479
|
+
* #1026: the compiler reads it to fill the model, and the entry that compiles
|
|
480
|
+
* a model without calling the Spine emitter (`compileModel`) reads nothing
|
|
481
|
+
* from the emitter's module. `src/emit_spine.ts` re-exports it.
|
|
482
|
+
*/
|
|
483
|
+
export const UNSTATED_REFERENCE_SCALE: number = ((): number => {
|
|
484
|
+
const v = PARSER_DEFAULTS.header?.referenceScale;
|
|
485
|
+
if (typeof v !== 'number') throw new Error(`internal: PARSER_DEFAULTS.header.referenceScale is ${JSON.stringify(v)}, not a number`);
|
|
486
|
+
return v;
|
|
487
|
+
})();
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* A physics constraint's parameters and their parser defaults
|
|
491
|
+
* (`SkeletonJson.js:295-319`), in the order the motion spec's physics table
|
|
492
|
+
* writes them. The same values are `PARSER_DEFAULTS['physics constraint']`'s;
|
|
493
|
+
* this list is also an ORDER, which that row is not: `compile.ts` reads it for
|
|
494
|
+
* the names the table copies, and `src/emit_spine.ts` for the order the table's
|
|
495
|
+
* constraint is written in and the defaults it leaves out. Here, beside the
|
|
496
|
+
* parser's other defaults, since issue #1026 — for the reason
|
|
497
|
+
* `UNSTATED_REFERENCE_SCALE` gives.
|
|
498
|
+
*/
|
|
499
|
+
export const PHYSICS_PARAMS: ReadonlyArray<readonly [string, number]> = [
|
|
500
|
+
['inertia', 0.5],
|
|
501
|
+
['strength', 100],
|
|
502
|
+
['damping', 0.85],
|
|
503
|
+
['mass', 1],
|
|
504
|
+
['wind', 0],
|
|
505
|
+
['gravity', 0],
|
|
506
|
+
['mix', 1],
|
|
507
|
+
['fps', 60],
|
|
508
|
+
['limit', 5000],
|
|
509
|
+
];
|
|
510
|
+
|
|
511
|
+
/** `….rotate[3]` → its stem and 3; anything that does not end in an index → null. */
|
|
512
|
+
function trailingIndex(path: string): { stem: string; index: number } | null {
|
|
513
|
+
const m = /^(.*)\[(\d+)\]$/.exec(path);
|
|
514
|
+
return m === null ? null : { stem: m[1], index: Number(m[2]) };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** An object a row is read on, and — for a timeline key — the key before it. */
|
|
518
|
+
export interface ParserReadingSite {
|
|
519
|
+
readonly object: Readonly<Record<string, unknown>>;
|
|
520
|
+
/** The timeline's previous key; null on key 0 and on anything that is not a timeline key. */
|
|
521
|
+
readonly previous: () => ParserReadingSite | null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* What the parser reads for `field` at `site`: the value written, or the row's
|
|
526
|
+
* where it is absent — `undefined` when the row does not list the field and it
|
|
527
|
+
* is absent.
|
|
528
|
+
*/
|
|
529
|
+
export function parserReading(row: Readonly<Record<string, ParserDefault>>, site: ParserReadingSite, field: string): unknown {
|
|
530
|
+
if (field in site.object) return site.object[field];
|
|
531
|
+
const rule = row[field];
|
|
532
|
+
if (rule === undefined || rule === null || typeof rule !== 'object') return rule;
|
|
533
|
+
if ('field' in rule) return parserReading(row, site, rule.field);
|
|
534
|
+
const previous = site.previous();
|
|
535
|
+
return previous === null ? rule.first : parserReading(row, previous, rule.previous);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Whether the emitter leaves `field` out of the object at `site`: it is
|
|
540
|
+
* written, its row lists it, and the parser reads the same value without it —
|
|
541
|
+
* at the row's `only` value, where the row has one. The one decision, shared by
|
|
542
|
+
* the pass below and by `ingest`, which says a restated default out loud only
|
|
543
|
+
* where this would still write it.
|
|
544
|
+
*/
|
|
545
|
+
export function parserOmits(row: Readonly<Record<string, ParserDefault>>, site: ParserReadingSite, field: string): boolean {
|
|
546
|
+
if (!(field in site.object)) return false;
|
|
547
|
+
const rule = row[field];
|
|
548
|
+
if (rule === undefined) return false;
|
|
549
|
+
const value = site.object[field];
|
|
550
|
+
if (rule !== null && typeof rule === 'object' && 'field' in rule && rule.only !== undefined && !Object.is(value, rule.only)) {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
const without: Rec = { ...site.object };
|
|
554
|
+
delete without[field];
|
|
555
|
+
return Object.is(parserReading(row, { object: without, previous: site.previous }, field), value);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Every key of `skeleton` whose value is the one the 4.3 parser reads in its
|
|
560
|
+
* absence, removed — in place, and returned. Each is decided against the object
|
|
561
|
+
* as its constructor built it, before anything is removed, so a `{ field }` or
|
|
562
|
+
* `{ previous }` row reads exactly what the parser will. `table` is a parameter
|
|
563
|
+
* so the selftest can plant a wrong row; the emitter passes nothing.
|
|
564
|
+
*/
|
|
565
|
+
export function withoutParserDefaults<T extends object>(skeleton: T, table: ParserDefaultTable = PARSER_DEFAULTS): T {
|
|
566
|
+
const byPath = new Map<string, Rec>();
|
|
567
|
+
const siteAt = (path: string, object: Rec): ParserReadingSite => ({
|
|
568
|
+
object,
|
|
569
|
+
previous: () => {
|
|
570
|
+
const at = trailingIndex(path);
|
|
571
|
+
if (at === null || at.index === 0) return null;
|
|
572
|
+
const stem = `${at.stem}[${at.index - 1}]`;
|
|
573
|
+
const before = byPath.get(stem);
|
|
574
|
+
return before === undefined ? null : siteAt(stem, before);
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
const removed: Array<{ object: Rec; field: string }> = [];
|
|
578
|
+
forEachKindedObject(skeleton, (kind, object, path) => {
|
|
579
|
+
byPath.set(path, object);
|
|
580
|
+
const row = table[kind];
|
|
581
|
+
if (row === undefined) return;
|
|
582
|
+
const site = siteAt(path, object);
|
|
583
|
+
for (const field of Object.keys(row)) if (parserOmits(row, site, field)) removed.push({ object, field });
|
|
584
|
+
});
|
|
585
|
+
for (const { object, field } of removed) delete object[field];
|
|
586
|
+
return skeleton;
|
|
587
|
+
}
|