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,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The core's third construct: every drawn attachment's world vertices at the
|
|
3
|
+
* setup pose (issue #931, step 2c of issue #380) — the oracle's
|
|
4
|
+
* `setup.attachments` block, a region's four corners and a mesh's vertices,
|
|
5
|
+
* and its `setup.clips` block, a clipping polygon and the slot its clip ends
|
|
6
|
+
* at. It is the first construct where the core's arithmetic meets geometry the
|
|
7
|
+
* compiler measured, so exact equivalence here is also a check on the model.
|
|
8
|
+
*
|
|
9
|
+
* ## How every rule below was fixed
|
|
10
|
+
*
|
|
11
|
+
* By posing hand-written skeletons through `tools/pose_oracle.ts dump`
|
|
12
|
+
* (spine-core 4.3.13, `--skin all`, `--physics none`) and comparing a formula
|
|
13
|
+
* over the core's own world transforms (`./world.ts`) with the rows the dump
|
|
14
|
+
* printed, at tolerance 0 on the oracle's six-decimal grid. The runtime's
|
|
15
|
+
* source was not read. The bone rows read exact in every probe, so every
|
|
16
|
+
* residual below belongs to the attachment rule. The core suite's `CO17`
|
|
17
|
+
* holds a hand-written skeleton of every case against the core at tolerance 0.
|
|
18
|
+
*
|
|
19
|
+
* ## A region's four corners
|
|
20
|
+
*
|
|
21
|
+
* Let `W, H` be the record's `width, height`, `sx, sy` its `scaleX, scaleY`
|
|
22
|
+
* (1 unstated), and `ox, oy, w, h, ow, oh` its atlas rectangle's `offsetX,
|
|
23
|
+
* offsetY, width, height, originalWidth, originalHeight` (`ModelAtlasRect`,
|
|
24
|
+
* issue #935). With `kx = W/ow·sx` and `ky = H/oh·sy`:
|
|
25
|
+
*
|
|
26
|
+
* x1 = −W/2·sx + ox·kx, y1 = −H/2·sy + oy·ky, x2 = x1 + w·kx, y2 = y1 + h·ky
|
|
27
|
+
*
|
|
28
|
+
* and the corners, in this order, are `(x1,y1) (x1,y2) (x2,y2) (x2,y1)` —
|
|
29
|
+
* bottom-left, upper-left, upper-right, bottom-right, as the oracle's header
|
|
30
|
+
* words them. Each is turned by the record's `rotation` (degrees, pi written
|
|
31
|
+
* as `./world.ts`'s `RUNTIME_PI`), moved by its `x, y` (0 unstated), and
|
|
32
|
+
* carried through the slot bone's world matrix: `a·x + b·y + worldX`,
|
|
33
|
+
* `c·x + d·y + worldY`. Every record field is read as the double the document
|
|
34
|
+
* spells.
|
|
35
|
+
*
|
|
36
|
+
* Measured on 6000 hand-written regions — 70% of them trimmed, atlas `rotate`
|
|
37
|
+
* 0, 90, 180 or 270, placement in ±50, rotation in ±180 and scale in ±2 —
|
|
38
|
+
* each on one of ten bones rotated, scaled, sheared and reflecting, posed
|
|
39
|
+
* four times: field values float32-exact and spelled with five non-float32
|
|
40
|
+
* decimals, each on a page of `scale:` 1 and 0.37 (1500 each):
|
|
41
|
+
*
|
|
42
|
+
* | reading | exact |
|
|
43
|
+
* | --- | ---: |
|
|
44
|
+
* | the rule above, fields as doubles | 6000 of 6000 |
|
|
45
|
+
* | the trim ignored (`w, h` for `ow, oh`, offsets 0) | 1796 of 6000, worst 335.43 units |
|
|
46
|
+
* | the fields read through `Math.fround`, on the non-float32 spellings | 13 of 3000, worst 41 millionths |
|
|
47
|
+
* | the atlas `bounds` taken as transposed at 90 and 270 | 3007 of 6000, worst 1634.26 units |
|
|
48
|
+
*
|
|
49
|
+
* So the page's `scale:` and the atlas's `rotate` do not enter the corners;
|
|
50
|
+
* the rectangle's `width` and `height` are in the drawing's orientation, as
|
|
51
|
+
* `AtlasRegion` in `src/atlas.ts` states.
|
|
52
|
+
*
|
|
53
|
+
* - **A region with a `sequence` draws its setup frame's rectangle.** Four
|
|
54
|
+
* probes of one three-frame sequence whose frames differ in their trims:
|
|
55
|
+
* `setup` unstated and `setup: 0` matched frame 0's corners, `setup: 1`
|
|
56
|
+
* frame 1's and `setup: 2` frame 2's, and no other frame's. The frame is
|
|
57
|
+
* the model's `sequence.atlas[setup ?? 0]`.
|
|
58
|
+
* - ⛔ **A region whose rectangle is `null` leaves the whole block out, by
|
|
59
|
+
* name.** `null` is the model's statement that the build had no source for
|
|
60
|
+
* that region (`ModelRegionAttachment.atlas`); posing it at a trim of 0
|
|
61
|
+
* would be inventing the four numbers the corners read — the build pair of
|
|
62
|
+
* issue #931 posed one such document 11.925 world units apart. A block
|
|
63
|
+
* with one row missing would read as a roster difference, not an absence,
|
|
64
|
+
* so the block is absent and every such slot is named.
|
|
65
|
+
*
|
|
66
|
+
* ## A mesh's vertices, and a clipping polygon's
|
|
67
|
+
*
|
|
68
|
+
* - Unweighted: each local `x, y` through the slot bone's world matrix plus
|
|
69
|
+
* its origin.
|
|
70
|
+
* - Weighted: per vertex, the sum over its bindings in order of
|
|
71
|
+
* `(x·a + y·b + worldX)·weight` and `(x·c + y·d + worldY)·weight`, each
|
|
72
|
+
* binding's bone looked up BY NAME (`ModelBinding`) — its world, not the
|
|
73
|
+
* slot bone's.
|
|
74
|
+
* - ⚠️ **Every vertex coordinate, bind coordinate and weight is read through
|
|
75
|
+
* `Math.fround` first**: the runtime stores a vertex attachment's array as
|
|
76
|
+
* float32, while it reads a bone's and a region's numbers as the doubles
|
|
77
|
+
* the text spells (`src/model.ts`'s 🔸). Measured on 1200 meshes over the
|
|
78
|
+
* ten bones (60% weighted over one to four of them): float32-exact inputs
|
|
79
|
+
* 600 of 600 either way; inputs with five non-float32 decimals 600 of 600
|
|
80
|
+
* through `Math.fround`, 0 of 600 as doubles (worst 11 millionths). On the
|
|
81
|
+
* corpus, `gallery/flex`'s weighted meshes and `gallery/squash`'s ball read
|
|
82
|
+
* 1 to 3 millionths off as doubles and exact through `Math.fround`.
|
|
83
|
+
* - A linked mesh is written by the oracle as kind `mesh` under its own name,
|
|
84
|
+
* and its vertices are its SOURCE mesh's (`skin`, `slot`, `source` in the
|
|
85
|
+
* model) through the linked mesh's own slot bone: a linked mesh on a
|
|
86
|
+
* rotated, reflected and sheared bone, sourced from a mesh in another slot
|
|
87
|
+
* of another skin, read exact.
|
|
88
|
+
* - The atlas does not enter a mesh's vertices, and a mesh's `sequence`
|
|
89
|
+
* changes none of them.
|
|
90
|
+
* - A clipping polygon follows the mesh rule, and its row's `end` is the
|
|
91
|
+
* model's `end` slot or `null`: 1200 polygons drawn as the meshes above,
|
|
92
|
+
* half of them ending at a slot, read exact and with the right end through
|
|
93
|
+
* `Math.fround` (600 of 600 on each spelling), and 0 of 600 as doubles on
|
|
94
|
+
* the non-float32 spelling (worst 11 millionths).
|
|
95
|
+
*
|
|
96
|
+
* ## Which slots, and in what order
|
|
97
|
+
*
|
|
98
|
+
* A slot is listed when what it shows at setup (`shownAttachment`, issue
|
|
99
|
+
* #928) is a region, a mesh or a linked mesh (`setup.attachments`) or a
|
|
100
|
+
* clipping polygon (`setup.clips`), whether or not its bone is active — an
|
|
101
|
+
* inactive bone's world is all zeros, and the oracle's ill-conditioned rule
|
|
102
|
+
* excludes it. Rows are in the model's slot order, which is the setup draw
|
|
103
|
+
* order (`ModelSlot`): measured the same order as spine-core's on 19 of 19
|
|
104
|
+
* recipes. A bounding box and a path attachment have no row: the oracle's
|
|
105
|
+
* dump writes no vertices for either, so neither can be posed against it.
|
|
106
|
+
*
|
|
107
|
+
* ## At a sample, and under a deform or a sequence (issue #955)
|
|
108
|
+
*
|
|
109
|
+
* `poseGeometry` poses a sample's attachments the same way, through the
|
|
110
|
+
* sample's bones and in its draw order. A slot a deform timeline moved
|
|
111
|
+
* carries its deform array (`ShownGeometry.deform`): the vertices are the
|
|
112
|
+
* array's (`deformedVertices` in `./deform.ts`) and their coordinates are read
|
|
113
|
+
* AS THEY ARE, not through `Math.fround` again — a lerped deform is a double
|
|
114
|
+
* the runtime does not round (`./deform.ts`'s header: the lerped positions
|
|
115
|
+
* rounded to float32 missed 791 of 800 samples). A region a sequence
|
|
116
|
+
* timeline stepped carries its frame (`ShownGeometry.frame`), and its corners
|
|
117
|
+
* read that frame's rectangle (`CoreRegionGeometry.frames`).
|
|
118
|
+
*/
|
|
119
|
+
import type { ModelAtlasRect, ModelBinding, ModelVertices } from '../model.ts';
|
|
120
|
+
import { RUNTIME_PI, type CoreWorld } from './world.ts';
|
|
121
|
+
import { deformedVertices } from './deform.ts';
|
|
122
|
+
import { poseClipped, REGION_TRIANGLES, REGION_UVS, type CoreClippedRow, type DrawStep, type ShapeClipper, type TriangleClipper } from './clipping.ts';
|
|
123
|
+
|
|
124
|
+
const RAD = RUNTIME_PI / 180;
|
|
125
|
+
|
|
126
|
+
/** What a region record carries for its corners — `atlas` already the rectangle of the frame shown at setup. */
|
|
127
|
+
export interface CoreRegionGeometry {
|
|
128
|
+
x?: number;
|
|
129
|
+
y?: number;
|
|
130
|
+
rotation?: number;
|
|
131
|
+
scaleX?: number;
|
|
132
|
+
scaleY?: number;
|
|
133
|
+
width: number;
|
|
134
|
+
height: number;
|
|
135
|
+
/** The rectangle drawn at setup, or `null` when the model states the build had none. */
|
|
136
|
+
atlas: ModelAtlasRect | null;
|
|
137
|
+
/** A sequence's rectangles, one per frame (`sequence.atlas`) — what a sequence timeline's frame draws (`./deform.ts`). */
|
|
138
|
+
frames?: ModelAtlasRect[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The geometry a record carries, by kind, as far as the construct reads it. */
|
|
142
|
+
export type CoreGeometry =
|
|
143
|
+
| { kind: 'region'; region: CoreRegionGeometry }
|
|
144
|
+
| { kind: 'mesh'; vertices: ModelVertices; uvs: number[]; triangles: number[]; hull?: number }
|
|
145
|
+
| { kind: 'linkedmesh'; skin: string; slot: string; source: string }
|
|
146
|
+
| { kind: 'clipping'; end: string | null; vertices: ModelVertices; inverse: boolean; convex: boolean }
|
|
147
|
+
| { kind: 'boundingbox'; vertices: ModelVertices }
|
|
148
|
+
| { kind: 'path'; vertices: ModelVertices; closed: boolean; constantSpeed: boolean; lengths: number[] };
|
|
149
|
+
|
|
150
|
+
/** One row of `setup.attachments`: `[slot, attachment, kind, vertices]`, the oracle's row. */
|
|
151
|
+
export type CoreAttachmentRow = [string, string, 'region' | 'mesh', Array<number | null>];
|
|
152
|
+
/** One row of `setup.clips`: `[slot, attachment, end, polygon]`, the oracle's row. */
|
|
153
|
+
export type CoreClipRow = [string, string, string | null, Array<number | null>];
|
|
154
|
+
|
|
155
|
+
/** What computes a region's corners in the slot bone's world: `regionCorners` unless a caller passes another. */
|
|
156
|
+
export type RegionPoser = (region: CoreRegionGeometry & { atlas: ModelAtlasRect }, bone: CoreWorld) => number[];
|
|
157
|
+
/** What computes a vertex array's world positions: `worldVertices` unless a caller passes another. */
|
|
158
|
+
export type VertexPoser = (vertices: ModelVertices, bone: CoreWorld, world: ReadonlyMap<string, CoreWorld>, coords?: (v: number) => number) => number[];
|
|
159
|
+
|
|
160
|
+
/** A deformed array's coordinates are read as they are (`./deform.ts`, `deformedVertices`): they are not the stored float32s. */
|
|
161
|
+
export const EXACT_COORDS = (v: number): number => v;
|
|
162
|
+
|
|
163
|
+
/** A region's four corners in world units — the header's rule. */
|
|
164
|
+
export function regionCorners(region: CoreRegionGeometry & { atlas: ModelAtlasRect }, bone: CoreWorld): number[] {
|
|
165
|
+
const { width: W, height: H } = region;
|
|
166
|
+
const sx = region.scaleX ?? 1;
|
|
167
|
+
const sy = region.scaleY ?? 1;
|
|
168
|
+
const r = region.atlas;
|
|
169
|
+
const kx = (W / r.originalWidth) * sx;
|
|
170
|
+
const ky = (H / r.originalHeight) * sy;
|
|
171
|
+
const x1 = (-W / 2) * sx + r.offsetX * kx;
|
|
172
|
+
const y1 = (-H / 2) * sy + r.offsetY * ky;
|
|
173
|
+
const x2 = x1 + r.width * kx;
|
|
174
|
+
const y2 = y1 + r.height * ky;
|
|
175
|
+
const angle = (region.rotation ?? 0) * RAD;
|
|
176
|
+
const cos = Math.cos(angle);
|
|
177
|
+
const sin = Math.sin(angle);
|
|
178
|
+
const x = region.x ?? 0;
|
|
179
|
+
const y = region.y ?? 0;
|
|
180
|
+
const out: number[] = [];
|
|
181
|
+
for (const [px, py] of [[x1, y1], [x1, y2], [x2, y2], [x2, y1]]) {
|
|
182
|
+
// The operation order is measured to the bit (issue #966): the placement is added to the cosine term before the sine term is taken off — `px·cos − py·sin + x` reads 1 ulp off on 748 attachment-samples of six corpus rows.
|
|
183
|
+
const lx = px * cos + x - py * sin;
|
|
184
|
+
const ly = py * cos + y + px * sin;
|
|
185
|
+
out.push(bone.a * lx + bone.b * ly + bone.worldX, bone.c * lx + bone.d * ly + bone.worldY);
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* A vertex array's world positions — the header's mesh rule, every stored
|
|
192
|
+
* number through `Math.fround`; with `coords`, the coordinates through it
|
|
193
|
+
* instead (a deformed array's are read as they are, `EXACT_COORDS`), the
|
|
194
|
+
* weights through `Math.fround` still.
|
|
195
|
+
*/
|
|
196
|
+
export function worldVertices(vertices: ModelVertices, bone: CoreWorld, world: ReadonlyMap<string, CoreWorld>, coords: (v: number) => number = Math.fround): number[] {
|
|
197
|
+
const out: number[] = [];
|
|
198
|
+
if (!vertices.weighted) {
|
|
199
|
+
for (let i = 0; i + 1 < vertices.xy.length; i += 2) {
|
|
200
|
+
const x = coords(vertices.xy[i]);
|
|
201
|
+
const y = coords(vertices.xy[i + 1]);
|
|
202
|
+
out.push(x * bone.a + y * bone.b + bone.worldX, x * bone.c + y * bone.d + bone.worldY);
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
for (const influences of vertices.bindings) {
|
|
207
|
+
let wx = 0;
|
|
208
|
+
let wy = 0;
|
|
209
|
+
for (const binding of influences) {
|
|
210
|
+
const t = world.get(binding.bone);
|
|
211
|
+
if (t === undefined) throw new Error(`a binding names bone "${binding.bone}", which has no world transform (readModel refuses it first)`);
|
|
212
|
+
const x = coords(binding.x);
|
|
213
|
+
const y = coords(binding.y);
|
|
214
|
+
const weight = Math.fround(binding.weight);
|
|
215
|
+
wx += (x * t.a + y * t.b + t.worldX) * weight;
|
|
216
|
+
wy += (x * t.c + y * t.d + t.worldY) * weight;
|
|
217
|
+
}
|
|
218
|
+
out.push(wx, wy);
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
224
|
+
const finite = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);
|
|
225
|
+
const RECT_FIELDS = ['width', 'height', 'offsetX', 'offsetY', 'originalWidth', 'originalHeight'] as const;
|
|
226
|
+
|
|
227
|
+
function readRect(value: unknown, where: string, problems: string[]): ModelAtlasRect | undefined {
|
|
228
|
+
if (!isRecord(value)) {
|
|
229
|
+
problems.push(`${where} is ${JSON.stringify(value)}, not an atlas rectangle`);
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
const before = problems.length;
|
|
233
|
+
for (const key of Object.keys(value)) if (!(RECT_FIELDS as readonly string[]).includes(key)) problems.push(`${where}: field "${key}" is not one this reader knows; it reads [${RECT_FIELDS.join(', ')}]`);
|
|
234
|
+
for (const key of RECT_FIELDS) if (!finite(value[key])) problems.push(`${where}: ${key} is ${JSON.stringify(value[key]) ?? 'absent'}, not a finite number`);
|
|
235
|
+
return problems.length === before ? (value as unknown as ModelAtlasRect) : undefined;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** A `ModelVertices`, checked: an even run of finite numbers, or per vertex a non-empty list of bindings each naming a bone of the document. */
|
|
239
|
+
function readVertices(value: unknown, where: string, bones: ReadonlySet<string>, problems: string[]): ModelVertices | undefined {
|
|
240
|
+
if (!isRecord(value) || typeof value.weighted !== 'boolean') {
|
|
241
|
+
problems.push(`${where} is not { weighted: false, xy } or { weighted: true, bindings }`);
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
const before = problems.length;
|
|
245
|
+
if (!value.weighted) {
|
|
246
|
+
const xy = value.xy;
|
|
247
|
+
if (!Array.isArray(xy) || xy.length % 2 !== 0 || !xy.every(finite)) problems.push(`${where}.xy is not an even-length list of finite numbers`);
|
|
248
|
+
return problems.length === before ? { weighted: false, xy: xy as number[] } : undefined;
|
|
249
|
+
}
|
|
250
|
+
const bindings = value.bindings;
|
|
251
|
+
if (!Array.isArray(bindings)) {
|
|
252
|
+
problems.push(`${where}.bindings is not a list`);
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
bindings.forEach((influences, i) => {
|
|
256
|
+
if (!Array.isArray(influences) || influences.length === 0) {
|
|
257
|
+
problems.push(`${where}.bindings[${i}] is not a non-empty list of bindings`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
influences.forEach((b: unknown, j) => {
|
|
261
|
+
const at = `${where}.bindings[${i}][${j}]`;
|
|
262
|
+
if (!isRecord(b)) {
|
|
263
|
+
problems.push(`${at} is not an object`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (typeof b.bone !== 'string' || !bones.has(b.bone)) problems.push(`${at}: bone ${JSON.stringify(b.bone)} is not a bone of this document`);
|
|
267
|
+
for (const key of ['x', 'y', 'weight'] as const) if (!finite(b[key])) problems.push(`${at}: ${key} is ${JSON.stringify(b[key]) ?? 'absent'}, not a finite number`);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
return problems.length === before ? { weighted: true, bindings: bindings as ModelBinding[][] } : undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The geometry of one attachment record, read and checked field by field —
|
|
275
|
+
* called by `readModel` for every record of every skin. Returns `undefined`
|
|
276
|
+
* when something was refused (the problem is pushed, naming the path). What is
|
|
277
|
+
* refused mirrors the writer (`attachmentOf` in `src/model.ts`): a region
|
|
278
|
+
* carrying neither a rectangle nor a sequence, or both; a sequence whose
|
|
279
|
+
* `atlas` is not one rectangle per frame, or whose setup frame is not one of
|
|
280
|
+
* them; and a binding, a linked mesh's link or a clip's `end` naming nothing.
|
|
281
|
+
* A linked mesh's source is resolved later, once every skin is read.
|
|
282
|
+
*/
|
|
283
|
+
export function readGeometry(raw: Record<string, unknown>, kind: CoreGeometry['kind'], where: string, bones: ReadonlySet<string>, slots: ReadonlySet<string>, problems: string[]): CoreGeometry | undefined {
|
|
284
|
+
const before = problems.length;
|
|
285
|
+
switch (kind) {
|
|
286
|
+
case 'region': {
|
|
287
|
+
for (const key of ['x', 'y', 'rotation', 'scaleX', 'scaleY'] as const) if (raw[key] !== undefined && !finite(raw[key])) problems.push(`${where}: ${key} is ${JSON.stringify(raw[key])}, not a finite number`);
|
|
288
|
+
for (const key of ['width', 'height'] as const) if (!finite(raw[key])) problems.push(`${where}: ${key} is ${JSON.stringify(raw[key]) ?? 'absent'}, not a finite number`);
|
|
289
|
+
const hasRect = raw.atlas !== undefined;
|
|
290
|
+
const hasSequence = raw.sequence !== undefined;
|
|
291
|
+
let atlas: ModelAtlasRect | null | undefined;
|
|
292
|
+
let frames: ModelAtlasRect[] | undefined;
|
|
293
|
+
if (hasRect === hasSequence) {
|
|
294
|
+
problems.push(`${where}: the region carries ${hasRect ? 'both an atlas rectangle and a sequence' : 'neither an atlas rectangle nor a sequence'}; the writer states exactly one of the two, and a sequence holds a rectangle per frame (issue #935)`);
|
|
295
|
+
} else if (hasRect) {
|
|
296
|
+
// `readModel` has read the rectangle itself (`readAtlasRect`); here only its value is taken.
|
|
297
|
+
atlas = raw.atlas === null ? null : isRecord(raw.atlas) ? (raw.atlas as unknown as ModelAtlasRect) : undefined;
|
|
298
|
+
} else {
|
|
299
|
+
const seq = raw.sequence;
|
|
300
|
+
if (!isRecord(seq)) problems.push(`${where}.sequence is not an object`);
|
|
301
|
+
else {
|
|
302
|
+
const count = seq.count;
|
|
303
|
+
const setup = seq.setup ?? 0;
|
|
304
|
+
if (typeof count !== 'number' || !Number.isInteger(count) || count < 1) problems.push(`${where}.sequence: count is ${JSON.stringify(count)}, not a whole number of at least 1`);
|
|
305
|
+
else if (typeof setup !== 'number' || !Number.isInteger(setup) || setup < 0 || setup >= count) problems.push(`${where}.sequence: setup is ${JSON.stringify(seq.setup)}, not a frame of the ${count}`);
|
|
306
|
+
if (!Array.isArray(seq.atlas) || seq.atlas.length !== count) problems.push(`${where}.sequence: atlas is not a list of ${JSON.stringify(count)} rectangle(s), one per frame`);
|
|
307
|
+
else {
|
|
308
|
+
const rects = seq.atlas.map((r, i) => readRect(r, `${where}.sequence.atlas[${i}]`, problems));
|
|
309
|
+
if (typeof setup === 'number' && Number.isInteger(setup) && setup >= 0 && setup < rects.length) atlas = rects[setup];
|
|
310
|
+
if (rects.every((r) => r !== undefined)) frames = rects as ModelAtlasRect[];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (problems.length !== before || atlas === undefined) return undefined;
|
|
315
|
+
const pick = (key: 'x' | 'y' | 'rotation' | 'scaleX' | 'scaleY'): { [k: string]: number } => (raw[key] === undefined ? {} : { [key]: raw[key] as number });
|
|
316
|
+
return {
|
|
317
|
+
kind: 'region',
|
|
318
|
+
region: { ...pick('x'), ...pick('y'), ...pick('rotation'), ...pick('scaleX'), ...pick('scaleY'), width: raw.width as number, height: raw.height as number, atlas, ...(frames === undefined ? {} : { frames }) },
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
case 'linkedmesh': {
|
|
322
|
+
for (const key of ['skin', 'slot', 'source'] as const) if (typeof raw[key] !== 'string' || raw[key] === '') problems.push(`${where}: ${key} is ${JSON.stringify(raw[key]) ?? 'absent'}, not a non-empty string`);
|
|
323
|
+
if (typeof raw.slot === 'string' && !slots.has(raw.slot)) problems.push(`${where}: slot "${raw.slot}" is not a slot of this document`);
|
|
324
|
+
if (problems.length !== before) return undefined;
|
|
325
|
+
return { kind: 'linkedmesh', skin: raw.skin as string, slot: raw.slot as string, source: raw.source as string };
|
|
326
|
+
}
|
|
327
|
+
case 'mesh':
|
|
328
|
+
case 'clipping':
|
|
329
|
+
case 'boundingbox':
|
|
330
|
+
case 'path': {
|
|
331
|
+
const vertices = readVertices(raw.vertices, `${where}.vertices`, bones, problems);
|
|
332
|
+
if (kind === 'clipping' && raw.end !== undefined && (typeof raw.end !== 'string' || !slots.has(raw.end))) problems.push(`${where}: end is ${JSON.stringify(raw.end)}, not a slot of this document`);
|
|
333
|
+
if (kind === 'clipping') for (const key of ['inverse', 'convex'] as const) if (raw[key] !== undefined && typeof raw[key] !== 'boolean') problems.push(`${where}: ${key} is ${JSON.stringify(raw[key])}, not a boolean`);
|
|
334
|
+
const mesh = kind === 'mesh' && vertices !== undefined ? readMeshTriangles(raw, vertices, where, problems) : undefined;
|
|
335
|
+
if (vertices === undefined || problems.length !== before) return undefined;
|
|
336
|
+
if (kind === 'mesh') return mesh === undefined ? undefined : { kind, vertices, ...mesh };
|
|
337
|
+
if (kind === 'clipping') return { kind, end: typeof raw.end === 'string' ? raw.end : null, vertices, inverse: raw.inverse === true, convex: raw.convex === true };
|
|
338
|
+
if (kind === 'boundingbox') return { kind, vertices };
|
|
339
|
+
return readPathGeometry(raw, vertices, where, problems);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* A mesh's `uvs` and `triangles`, what the clipper reads besides its vertices
|
|
346
|
+
* (issue #964, `./clipping.ts`): one finite pair per vertex, and whole
|
|
347
|
+
* indices into the vertices in threes — as the writer writes them.
|
|
348
|
+
*/
|
|
349
|
+
function readMeshTriangles(raw: Record<string, unknown>, vertices: ModelVertices, where: string, problems: string[]): { uvs: number[]; triangles: number[]; hull?: number } | undefined {
|
|
350
|
+
const before = problems.length;
|
|
351
|
+
const count = vertices.weighted ? vertices.bindings.length : vertices.xy.length / 2;
|
|
352
|
+
const uvs = raw.uvs;
|
|
353
|
+
const triangles = raw.triangles;
|
|
354
|
+
if (!Array.isArray(uvs) || !uvs.every(finite) || uvs.length !== 2 * count) problems.push(`${where}.uvs is not ${2 * count} finite numbers, one pair per vertex`);
|
|
355
|
+
if (!Array.isArray(triangles) || triangles.length % 3 !== 0 || !triangles.every((i) => Number.isInteger(i) && i >= 0 && i < count)) problems.push(`${where}.triangles is not a list of whole indices below ${count}, in threes`);
|
|
356
|
+
// The hull's vertex count (issue #966, retained for the raw entry): a whole number no larger than the vertices.
|
|
357
|
+
const hull = raw.hull;
|
|
358
|
+
if (hull !== undefined && !(typeof hull === 'number' && Number.isInteger(hull) && hull >= 0 && hull <= count)) problems.push(`${where}.hull is ${JSON.stringify(hull)}, not a whole number of vertices from 0 to ${count}`);
|
|
359
|
+
return problems.length === before ? { uvs: uvs as number[], triangles: triangles as number[], ...(hull === undefined ? {} : { hull: hull as number }) } : undefined;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* A path attachment's own fields past its vertices (construct 5's second cut,
|
|
364
|
+
* `./constraints_path.ts`): `closed` and `constantSpeed` as the writer states
|
|
365
|
+
* them — absent reads the parser's `false` and `true` (the `pathAttachments`
|
|
366
|
+
* block of a dump of a record stating neither) — and `lengths`, one finite
|
|
367
|
+
* entry per three points, as the writer always writes it (`buildRigPath` in
|
|
368
|
+
* `src/compile.ts`). `vertexCount` must be the vertices' own count and a
|
|
369
|
+
* multiple of 3: the writer refuses any other, and the curve walk reads
|
|
370
|
+
* groups of three.
|
|
371
|
+
*/
|
|
372
|
+
function readPathGeometry(raw: Record<string, unknown>, vertices: ModelVertices, where: string, problems: string[]): CoreGeometry | undefined {
|
|
373
|
+
const before = problems.length;
|
|
374
|
+
for (const key of ['closed', 'constantSpeed'] as const) if (raw[key] !== undefined && typeof raw[key] !== 'boolean') problems.push(`${where}: ${key} is ${JSON.stringify(raw[key])}, not a boolean`);
|
|
375
|
+
const count = vertices.weighted ? vertices.bindings.length : vertices.xy.length / 2;
|
|
376
|
+
if (raw.vertexCount !== count) problems.push(`${where}: vertexCount is ${JSON.stringify(raw.vertexCount) ?? 'absent'}, and the vertices hold ${count} point(s)`);
|
|
377
|
+
else if (count % 3 !== 0 || count < (raw.closed === true ? 3 : 6)) problems.push(`${where}: vertexCount is ${count}, not a multiple of 3 of at least ${raw.closed === true ? 3 : 6} — the curve walk reads knots and handles in threes`);
|
|
378
|
+
const lengths = raw.lengths;
|
|
379
|
+
if (!Array.isArray(lengths) || !lengths.every(finite)) problems.push(`${where}: lengths is not a list of finite numbers`);
|
|
380
|
+
else if (lengths.length !== count / 3) problems.push(`${where}: lengths has ${lengths.length} entry(ies) where the vertices make ${count / 3}`);
|
|
381
|
+
if (problems.length !== before) return undefined;
|
|
382
|
+
return { kind: 'path', vertices, closed: raw.closed === true, constantSpeed: raw.constantSpeed !== false, lengths: lengths as number[] };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** One slot as the construct reads it: its name and bone, and what it shows — the record's name, kind and geometry — or nothing. */
|
|
386
|
+
export interface ShownGeometry {
|
|
387
|
+
slot: string;
|
|
388
|
+
bone: string;
|
|
389
|
+
/** The name the oracle's row carries (`shownRow`). */
|
|
390
|
+
name: string;
|
|
391
|
+
placeholder: string;
|
|
392
|
+
skin: string;
|
|
393
|
+
geometry: CoreGeometry;
|
|
394
|
+
/** The deform array a deform timeline set on the slot (`./deform.ts`), or none. */
|
|
395
|
+
deform?: readonly number[];
|
|
396
|
+
/** The frame a sequence timeline set on a region's series (`./deform.ts`), or none: the setup frame. */
|
|
397
|
+
frame?: number;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Resolves a linked mesh's source to its vertex array, or names why not. */
|
|
401
|
+
export type SourceOf = (skin: string, slot: string, source: string) => ModelVertices | string;
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* What the draw walk of `./clipping.ts` needs past the shown records: the
|
|
405
|
+
* draw order (every slot, showing something or not — a clip ends at a slot
|
|
406
|
+
* whatever it shows), the active bones, and a mesh's `uvs` and `triangles`
|
|
407
|
+
* by where a linked mesh's source is filed.
|
|
408
|
+
*/
|
|
409
|
+
export interface DrawWalk {
|
|
410
|
+
order: readonly string[];
|
|
411
|
+
active: ReadonlySet<string>;
|
|
412
|
+
meshOf: (skin: string, slot: string, source: string) => { uvs: number[]; triangles: number[] } | undefined;
|
|
413
|
+
clip?: TriangleClipper;
|
|
414
|
+
/** What the drawn rows are cut through (`clipThrough`), replaceable by the render suite's plants. */
|
|
415
|
+
through?: ShapeClipper;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* The two blocks from the slots' shown records, in slot order, every number
|
|
420
|
+
* through `round` (the oracle's, `gridRound` in `./index.ts`), or — for
|
|
421
|
+
* `attachments` — the reason it is absent: a shown region whose rectangle is
|
|
422
|
+
* `null` (the header's ⛔), every one named. With `draw`, also the `clipped`
|
|
423
|
+
* block (issue #964, `./clipping.ts`) from the unrounded geometry, or why it
|
|
424
|
+
* is absent.
|
|
425
|
+
*/
|
|
426
|
+
export function poseGeometry(
|
|
427
|
+
shown: readonly ShownGeometry[],
|
|
428
|
+
world: ReadonlyMap<string, CoreWorld>,
|
|
429
|
+
sourceOf: SourceOf,
|
|
430
|
+
round: (v: number) => number | null,
|
|
431
|
+
plant: { region?: RegionPoser; vertices?: VertexPoser } = {},
|
|
432
|
+
draw?: DrawWalk,
|
|
433
|
+
): { attachments: CoreAttachmentRow[] | null; attachmentsWhy: string | null; clips: CoreClipRow[]; clipped: CoreClippedRow[] | null; clippedWhy: string | null; drawnClipped: CoreClippedRow[] | null; drawnClippedWhy: string | null } {
|
|
434
|
+
const region = plant.region ?? regionCorners;
|
|
435
|
+
const vertices = plant.vertices ?? worldVertices;
|
|
436
|
+
const attachments: CoreAttachmentRow[] = [];
|
|
437
|
+
const clips: CoreClipRow[] = [];
|
|
438
|
+
const nulls: string[] = [];
|
|
439
|
+
const steps = new Map<string, DrawStep>();
|
|
440
|
+
for (const s of shown) {
|
|
441
|
+
const bone = world.get(s.bone);
|
|
442
|
+
if (bone === undefined) throw new Error(`slot "${s.slot}": bone "${s.bone}" has no world transform`);
|
|
443
|
+
const g = s.geometry;
|
|
444
|
+
// A deform array replaces the stored vertices, its coordinates read as they are (`./deform.ts`).
|
|
445
|
+
const drawn = (v: ModelVertices): number[] => (s.deform === undefined ? vertices(v, bone, world) : vertices(deformedVertices(v, s.deform), bone, world, EXACT_COORDS));
|
|
446
|
+
if (g.kind === 'region') {
|
|
447
|
+
const rect = s.frame === undefined ? g.region.atlas : (g.region.frames?.[s.frame] ?? g.region.atlas);
|
|
448
|
+
if (rect === null) {
|
|
449
|
+
nulls.push(`slot "${s.slot}" shows region "${s.name}" (skin "${s.skin}", placeholder "${s.placeholder}")`);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const corners = region({ ...g.region, atlas: rect }, bone);
|
|
453
|
+
attachments.push([s.slot, s.name, 'region', corners.map(round)]);
|
|
454
|
+
steps.set(s.slot, { slot: s.slot, kind: 'draw', attachment: s.name, vertices: corners, triangles: [...REGION_TRIANGLES], uvs: [...REGION_UVS] });
|
|
455
|
+
} else if (g.kind === 'mesh') {
|
|
456
|
+
const world = drawn(g.vertices);
|
|
457
|
+
attachments.push([s.slot, s.name, 'mesh', world.map(round)]);
|
|
458
|
+
steps.set(s.slot, { slot: s.slot, kind: 'draw', attachment: s.name, vertices: world, triangles: g.triangles, uvs: g.uvs });
|
|
459
|
+
} else if (g.kind === 'linkedmesh') {
|
|
460
|
+
const source = sourceOf(g.skin, g.slot, g.source);
|
|
461
|
+
if (typeof source === 'string') throw new Error(`slot "${s.slot}": ${source} (readModel refuses it first)`);
|
|
462
|
+
const world = drawn(source);
|
|
463
|
+
attachments.push([s.slot, s.name, 'mesh', world.map(round)]);
|
|
464
|
+
// A linked mesh draws its source's triangles over its source's UVs.
|
|
465
|
+
const mesh = draw?.meshOf(g.skin, g.slot, g.source);
|
|
466
|
+
if (draw !== undefined && mesh === undefined) throw new Error(`slot "${s.slot}": the linked mesh's source "${g.source}" carries no triangles (readModel refuses it first)`);
|
|
467
|
+
if (mesh !== undefined) steps.set(s.slot, { slot: s.slot, kind: 'draw', attachment: s.name, vertices: world, triangles: mesh.triangles, uvs: mesh.uvs });
|
|
468
|
+
} else if (g.kind === 'clipping') {
|
|
469
|
+
const polygon = drawn(g.vertices);
|
|
470
|
+
clips.push([s.slot, s.name, g.end, polygon.map(round)]);
|
|
471
|
+
steps.set(s.slot, { slot: s.slot, kind: 'clip', attachment: s.name, active: draw?.active.has(s.bone) ?? false, end: g.end, polygon, inverse: g.inverse, convex: g.convex });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const attachmentsWhy = nulls.length === 0
|
|
475
|
+
? null
|
|
476
|
+
: `${nulls.join('; ')} — the model states the build had no atlas rectangle for it (atlas: null), and its corners read the trim and original size; a trim of 0 is not assumed`;
|
|
477
|
+
let clipped: CoreClippedRow[] | null = null;
|
|
478
|
+
let clippedWhy: string | null = draw === undefined ? 'the draw order was not given' : attachmentsWhy;
|
|
479
|
+
let drawnClipped: CoreClippedRow[] | null = null;
|
|
480
|
+
let drawnClippedWhy: string | null = clippedWhy;
|
|
481
|
+
if (draw !== undefined && attachmentsWhy === null) {
|
|
482
|
+
const walk = draw.order.map((slot): DrawStep => steps.get(slot) ?? { slot, kind: 'none' });
|
|
483
|
+
const posed = poseClipped(walk, round, draw.clip, draw.through);
|
|
484
|
+
clipped = posed.rows;
|
|
485
|
+
clippedWhy = posed.why;
|
|
486
|
+
drawnClipped = posed.drawn;
|
|
487
|
+
drawnClippedWhy = posed.drawnWhy;
|
|
488
|
+
}
|
|
489
|
+
return { attachments: attachmentsWhy === null ? attachments : null, attachmentsWhy, clips, clipped, clippedWhy, drawnClipped, drawnClippedWhy };
|
|
490
|
+
}
|