spine-rigc 1.6.2 → 1.8.0
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/cli.ts +166 -60
- package/docs/AUTHORING.md +25 -16
- package/package.json +1 -1
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/skin_entries.ts +35 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/kinds.ts +102 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/index.ts +130 -0
- package/src/assertions/model/parse.ts +136 -0
- package/src/assertions/model/skin_entries.ts +58 -0
- package/src/assertions/model/slot_colour.ts +80 -0
- package/src/assertions/reasons.ts +127 -0
- package/src/assertions/values.ts +39 -0
- package/src/atlas.ts +26 -16
- package/src/check.ts +70 -47
- package/src/compile.ts +118 -93
- package/src/core/constraints_path.ts +60 -38
- package/src/core/constraints_physics.ts +22 -4
- package/src/core/index.ts +102 -15
- package/src/core/uvs.ts +25 -5
- package/src/core/world.ts +80 -24
- package/src/deformmeasure.ts +383 -157
- package/src/deformreport.ts +3 -0
- package/src/deformstructure.ts +335 -0
- package/src/emit.ts +17 -6
- package/src/ingest.ts +18 -13
- package/src/keyorder.ts +2 -2
- package/src/model.ts +114 -13
- package/src/render.ts +520 -59
- package/src/render_core.ts +178 -21
- package/src/transform.ts +113 -128
- package/src/types.ts +6 -5
- package/src/validate.ts +200 -490
package/src/core/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* rigc's own core: it reads the compiled model as `build` writes it
|
|
3
|
-
* (`rigc-compiled/
|
|
3
|
+
* (`rigc-compiled/2`, and `/1` before issue #1016; `skeleton.model.json`)
|
|
4
|
+
* and poses it (issue #925, step 2a
|
|
4
5
|
* of issue #380). It is the second dumper `tools/pose_oracle.ts` was shaped
|
|
5
6
|
* for: what it poses is written into the same `pose-oracle/2` document the
|
|
6
7
|
* runtime's dump is, and `compare` holds the two to each other.
|
|
@@ -153,11 +154,13 @@
|
|
|
153
154
|
* World transforms come from the core's own evaluator, `worldTransforms` in
|
|
154
155
|
* `./world.ts`, written from what a bone's fields mean and from measurement
|
|
155
156
|
* against the runtime's dump; its header states each measured choice. It is
|
|
156
|
-
* not `src/transform.ts`'s `computeWorldTransforms
|
|
157
|
-
* the compiler's
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
157
|
+
* not reached through `src/transform.ts`'s `computeWorldTransforms`: that is
|
|
158
|
+
* the compiler's adapter over this same evaluator (issues #1015 and #1021 —
|
|
159
|
+
* the same arithmetic since #1021), and the core importing the compiler would
|
|
160
|
+
* be a cycle. The compiler's evaluator before #1015, called as it was, read
|
|
161
|
+
* IDENTICAL on 1 of the 12 recipes the core poses (DIFF on 11, worst 61
|
|
162
|
+
* millionths against a tolerance of one — issue #925's report). The owner's
|
|
163
|
+
* decision is recorded on issue #380. `poseSetup` takes a
|
|
161
164
|
* `CorePlant` — the evaluator, the skin resolution, the colour and blend readings — so
|
|
162
165
|
* the suite's plants can pass a copy of one of them; nothing else passes one.
|
|
163
166
|
*
|
|
@@ -168,7 +171,7 @@
|
|
|
168
171
|
* child process, no file system — `readModel` takes the document's TEXT — and
|
|
169
172
|
* nothing from the Spine runtime package, as a value or as a type.
|
|
170
173
|
*/
|
|
171
|
-
import type { ModelAtlasRect, ModelBone, ModelSlot, ModelVertices, SkinTableEntry } from '../model.ts';
|
|
174
|
+
import type { ModelAtlasRect, ModelBone, ModelPage, ModelPageRegion, ModelSlot, ModelVertices, SkinTableEntry } from '../model.ts';
|
|
172
175
|
import { worldTransforms, type CoreWorld } from './world.ts';
|
|
173
176
|
import { readAnimationTimelines, type CoreAnimationTimelines } from './animation.ts';
|
|
174
177
|
import { readEventDefs, type CoreEventDef } from './events.ts';
|
|
@@ -183,8 +186,19 @@ import { attachmentStates, type DeformBlender, type DeformEvaluator, type Sequen
|
|
|
183
186
|
import { drawOrderAt, type DrawOrderEvaluator } from './draw_order.ts';
|
|
184
187
|
import type { EventsFired } from './events.ts';
|
|
185
188
|
|
|
186
|
-
/**
|
|
187
|
-
|
|
189
|
+
/**
|
|
190
|
+
* The document spec `build` writes today, and the one this reader takes with
|
|
191
|
+
* the `pages` section (issue #1016).
|
|
192
|
+
*/
|
|
193
|
+
export const CORE_DOCUMENT_SPEC = 'rigc-compiled/2';
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The spec before issue #1016: the same sections without `pages`. Read as
|
|
197
|
+
* before — the draw then takes where each region sits from the atlas beside
|
|
198
|
+
* the document (`corePoser` in `src/render_core.ts` says so by name), because
|
|
199
|
+
* a `/1` document does not state it.
|
|
200
|
+
*/
|
|
201
|
+
export const CORE_DOCUMENT_SPEC_1 = 'rigc-compiled/1';
|
|
188
202
|
|
|
189
203
|
/** Who posed a dump the core wrote — the oracle document's `dumper`. */
|
|
190
204
|
export const CORE_DUMPER = 'rigc-core';
|
|
@@ -196,10 +210,19 @@ export class CoreInputError extends Error {}
|
|
|
196
210
|
export const CORE_SECTIONS = [
|
|
197
211
|
'referenceScale', 'bones', 'slots', 'skins', 'constraints', 'events', 'animations',
|
|
198
212
|
'images', 'pageGrids', 'droppedStates', 'absentParts', 'meshBones', 'meshes', 'physics', 'deformTransforms', 'trackDerivations', 'rig',
|
|
213
|
+
// Issue #1016: where each region sits on its page, in the atlas written beside the document (`pagesOfAtlas` in `src/model.ts`). `/2` only.
|
|
214
|
+
'pages',
|
|
199
215
|
// Issue #968: the digest of the `skeleton.json` written beside the document (`spineFileSha256` in `src/model.ts`).
|
|
200
216
|
'spine',
|
|
201
217
|
] as const;
|
|
202
218
|
|
|
219
|
+
/** The sections of a `rigc-compiled/1` document: `CORE_SECTIONS` without `pages`. */
|
|
220
|
+
export const CORE_SECTIONS_1: readonly string[] = CORE_SECTIONS.filter((key) => key !== 'pages');
|
|
221
|
+
|
|
222
|
+
/** The fields of a page and of a region in the `pages` section, as the writer lists them (`MODEL_PAGE_FIELDS`, `MODEL_PAGE_REGION_FIELDS` in `src/model.ts`, mirrored). */
|
|
223
|
+
export const CORE_PAGE_FIELDS = ['name', 'width', 'height', 'regions'] as const;
|
|
224
|
+
export const CORE_PAGE_REGION_FIELDS = ['name', 'x', 'y', 'width', 'height', 'offsetX', 'offsetY', 'originalWidth', 'originalHeight', 'degrees', 'index'] as const;
|
|
225
|
+
|
|
203
226
|
/** The fields a bone record may carry, as the writer lists them. A field outside this list is refused. */
|
|
204
227
|
export const CORE_BONE_FIELDS = ['name', 'parent', 'length', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'shearX', 'shearY', 'inheritMode', 'skinRequired', 'editor'] as const;
|
|
205
228
|
const BONE_NUMBERS = ['length', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'shearX', 'shearY'] as const;
|
|
@@ -297,7 +320,7 @@ export interface CoreAnimation {
|
|
|
297
320
|
}
|
|
298
321
|
|
|
299
322
|
/**
|
|
300
|
-
* A `rigc-compiled/1` document, read. `bones` and `slots` are checked field by
|
|
323
|
+
* A `rigc-compiled/2` (or `/1`) document, read. `bones` and `slots` are checked field by
|
|
301
324
|
* field against the writer's own records; skins and constraints are read as
|
|
302
325
|
* far as their names and memberships, and every other section is only
|
|
303
326
|
* required to be present — no construct this card admits reads it.
|
|
@@ -319,6 +342,66 @@ export interface CompiledDocument {
|
|
|
319
342
|
animations: CoreAnimation[];
|
|
320
343
|
/** The digest of the `skeleton.json` `build` wrote beside the document (issue #968) — what a render holds the file beside it to before posing it here. */
|
|
321
344
|
spine: { sha256: string };
|
|
345
|
+
/**
|
|
346
|
+
* Every page of the atlas `build` wrote beside the document and every region
|
|
347
|
+
* on it, in file order (issue #1016) — where each drawing sits, which the
|
|
348
|
+
* draw's page UVs read. `null` for a `rigc-compiled/1` document, which does
|
|
349
|
+
* not state it.
|
|
350
|
+
*/
|
|
351
|
+
pages: ModelPage[] | null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* The `pages` section, checked: a list of pages, each holding exactly
|
|
356
|
+
* `CORE_PAGE_FIELDS` — a non-empty name, a size of two positive finite
|
|
357
|
+
* numbers, a list of regions — and each region exactly
|
|
358
|
+
* `CORE_PAGE_REGION_FIELDS`, a non-empty name and nine finite numbers. Every
|
|
359
|
+
* fault is named by its path. Nothing is defaulted: a field the writer always
|
|
360
|
+
* writes is required here.
|
|
361
|
+
*/
|
|
362
|
+
function readPages(value: unknown, problems: string[]): ModelPage[] {
|
|
363
|
+
if (!Array.isArray(value)) {
|
|
364
|
+
problems.push(`pages is ${JSON.stringify(value) ?? 'absent'}, not a list of pages`);
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
const out: ModelPage[] = [];
|
|
368
|
+
value.forEach((raw, i) => {
|
|
369
|
+
const where = `pages[${i}]`;
|
|
370
|
+
if (!isRecord(raw)) {
|
|
371
|
+
problems.push(`${where} is not an object`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const before = problems.length;
|
|
375
|
+
const label = typeof raw.name === 'string' ? `${where} "${raw.name}"` : where;
|
|
376
|
+
unknownFields(raw, CORE_PAGE_FIELDS, label, problems);
|
|
377
|
+
if (typeof raw.name !== 'string' || raw.name === '') problems.push(`${where}: name is ${JSON.stringify(raw.name) ?? 'absent'}, not a non-empty string`);
|
|
378
|
+
for (const key of ['width', 'height'] as const) {
|
|
379
|
+
const v = raw[key];
|
|
380
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || v <= 0) problems.push(`${label}: ${key} is ${JSON.stringify(v) ?? 'absent'}, not a positive finite number — a page UV divides by it`);
|
|
381
|
+
}
|
|
382
|
+
const regions: ModelPageRegion[] = [];
|
|
383
|
+
if (!Array.isArray(raw.regions)) problems.push(`${label}: regions is not a list`);
|
|
384
|
+
else {
|
|
385
|
+
raw.regions.forEach((region, j) => {
|
|
386
|
+
const at = `${label}.regions[${j}]`;
|
|
387
|
+
if (!isRecord(region)) {
|
|
388
|
+
problems.push(`${at} is not an object`);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const named = typeof region.name === 'string' ? `${at} "${region.name}"` : at;
|
|
392
|
+
unknownFields(region, CORE_PAGE_REGION_FIELDS, named, problems);
|
|
393
|
+
if (typeof region.name !== 'string' || region.name === '') problems.push(`${at}: name is ${JSON.stringify(region.name) ?? 'absent'}, not a non-empty string`);
|
|
394
|
+
for (const key of CORE_PAGE_REGION_FIELDS) {
|
|
395
|
+
if (key === 'name') continue;
|
|
396
|
+
const v = region[key];
|
|
397
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) problems.push(`${named}: ${key} is ${JSON.stringify(v) ?? 'absent'}, not a finite number`);
|
|
398
|
+
}
|
|
399
|
+
regions.push(region as unknown as ModelPageRegion);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
if (problems.length === before) out.push({ name: raw.name as string, width: raw.width as number, height: raw.height as number, regions });
|
|
403
|
+
});
|
|
404
|
+
return out;
|
|
322
405
|
}
|
|
323
406
|
|
|
324
407
|
/** The `spine` section's value, checked: `{ "sha256": <64 lowercase hex> }` and nothing else, each fault named by path (issue #968). */
|
|
@@ -615,7 +698,8 @@ function readConstraints(value: unknown, animations: readonly CoreAnimation[], b
|
|
|
615
698
|
}
|
|
616
699
|
|
|
617
700
|
/**
|
|
618
|
-
* Read a `rigc-compiled/
|
|
701
|
+
* Read a `rigc-compiled/2` document — or a `rigc-compiled/1` one, which has
|
|
702
|
+
* no `pages` and is read with `pages: null` — from its text, refusing by name a text
|
|
619
703
|
* that is not JSON, a wrong `spec`, a missing section (`referenceScale`
|
|
620
704
|
* among them, issue #958), a section the document does not have, a
|
|
621
705
|
* `referenceScale` that is not a finite number, and — in the records these constructs read (bones, slots,
|
|
@@ -633,12 +717,15 @@ export function readModel(text: string, where = 'the model document'): CompiledD
|
|
|
633
717
|
throw new CoreInputError(`${where}: not JSON — ${(err as Error).message}`);
|
|
634
718
|
}
|
|
635
719
|
if (!isRecord(value)) throw new CoreInputError(`${where}: not a JSON object`);
|
|
636
|
-
if (value.spec !== CORE_DOCUMENT_SPEC) throw new CoreInputError(`${where}: spec is ${JSON.stringify(value.spec)}, not "${CORE_DOCUMENT_SPEC}"`);
|
|
720
|
+
if (value.spec !== CORE_DOCUMENT_SPEC && value.spec !== CORE_DOCUMENT_SPEC_1) throw new CoreInputError(`${where}: spec is ${JSON.stringify(value.spec)}, not "${CORE_DOCUMENT_SPEC}" (or "${CORE_DOCUMENT_SPEC_1}", read without its pages)`);
|
|
721
|
+
const spec = value.spec;
|
|
722
|
+
const sections: readonly string[] = spec === CORE_DOCUMENT_SPEC ? CORE_SECTIONS : CORE_SECTIONS_1;
|
|
637
723
|
const problems: string[] = [];
|
|
638
|
-
for (const key of
|
|
724
|
+
for (const key of sections) if (!(key in value)) problems.push(`section "${key}" is missing`);
|
|
639
725
|
for (const key of Object.keys(value)) {
|
|
640
|
-
if (key !== 'spec' && !
|
|
726
|
+
if (key !== 'spec' && !sections.includes(key)) problems.push(`section "${key}" is not one a ${spec} document has`);
|
|
641
727
|
}
|
|
728
|
+
const pages = spec === CORE_DOCUMENT_SPEC && 'pages' in value ? readPages(value.pages, problems) : null;
|
|
642
729
|
// The skeleton's reference scale (issue #958): wind and gravity act over it, so a missing one is the section refusal above, and a value the runtime could not read as a number is refused here by name.
|
|
643
730
|
const referenceScale = typeof value.referenceScale === 'number' && Number.isFinite(value.referenceScale) ? value.referenceScale : NaN;
|
|
644
731
|
if ('referenceScale' in value && Number.isNaN(referenceScale)) problems.push(`referenceScale is ${JSON.stringify(value.referenceScale)}, not a finite number — wind and gravity act over it`);
|
|
@@ -666,7 +753,7 @@ export function readModel(text: string, where = 'the model document'): CompiledD
|
|
|
666
753
|
});
|
|
667
754
|
}
|
|
668
755
|
if (problems.length > 0) throw new CoreInputError(`${where}: ${problems.length} problem(s): ${problems.join('; ')}`);
|
|
669
|
-
const doc: CompiledDocument = { spec
|
|
756
|
+
const doc: CompiledDocument = { spec, skin: CORE_ALL_SKINS, referenceScale, bones, slots, skins, constraints, animations, spine: { sha256: spine }, pages };
|
|
670
757
|
resolveSkinView(doc);
|
|
671
758
|
return doc;
|
|
672
759
|
}
|
package/src/core/uvs.ts
CHANGED
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
* and `sequence.getUVs(index)` — and `MeshAttachment.computeUVs`'s job, the
|
|
6
6
|
* mapping texture substitution calls, as pure functions.
|
|
7
7
|
*
|
|
8
|
-
* The
|
|
8
|
+
* The page layout is not a model record (`ModelAtlasRect`'s 🔸 in
|
|
9
9
|
* `src/model.ts`: the page, `x`, `y` and `rotate` are the packer's
|
|
10
|
-
* arrangement, not the drawing)
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* arrangement, not the drawing). It reaches these rules as a lookup
|
|
11
|
+
* (`UvLookup`), so nothing here links the runtime or opens a file: since
|
|
12
|
+
* issue #1016 from the document's own `pages` section, which `build` spells
|
|
13
|
+
* from the atlas it writes (`documentPageLookup` below); for a
|
|
14
|
+
* `rigc-compiled/1` document, which has none, from the atlas beside it, read
|
|
15
|
+
* by rigc's own reader (`parseAtlasText` and `atlasRegionLookup` in
|
|
16
|
+
* `src/atlas.ts`).
|
|
14
17
|
*
|
|
15
18
|
* ## How every rule below was fixed
|
|
16
19
|
*
|
|
@@ -143,6 +146,7 @@ import { applyConstraints } from './constraints.ts';
|
|
|
143
146
|
import type { SliderApplication } from './constraints_slider.ts';
|
|
144
147
|
import { attachmentStates } from './deform.ts';
|
|
145
148
|
import type { ShownGeometry } from './vertices.ts';
|
|
149
|
+
import type { ModelPage, ModelPageRegion } from '../model.ts';
|
|
146
150
|
import { worldTransforms } from './world.ts';
|
|
147
151
|
|
|
148
152
|
/** A region as these rules read it — `AtlasRegion` in `src/atlas.ts` is one, and so is spine-core's `TextureAtlasRegion`. */
|
|
@@ -168,6 +172,22 @@ export interface UvPage {
|
|
|
168
172
|
/** The region an atlas draws under a name, and its page — the first of that name, as the header states; `null` when there is none. */
|
|
169
173
|
export type UvLookup = (name: string) => { page: UvPage; region: UvRegion } | null;
|
|
170
174
|
|
|
175
|
+
/**
|
|
176
|
+
* The lookup over a document's `pages` section (issue #1016): the first region
|
|
177
|
+
* of a name in file order, pages in order and regions in each page's order —
|
|
178
|
+
* the order `pagesOfAtlas` in `src/model.ts` copies from the atlas, so this is
|
|
179
|
+
* `atlasRegionLookup` over the same text, with no atlas read. The name is
|
|
180
|
+
* compared as stored, untrimmed, as the header's *Which region* states.
|
|
181
|
+
*/
|
|
182
|
+
export function documentPageLookup(pages: readonly ModelPage[]): (name: string) => { page: UvPage; region: ModelPageRegion } | null {
|
|
183
|
+
const first = new Map<string, { page: UvPage; region: ModelPageRegion }>();
|
|
184
|
+
for (const page of pages) {
|
|
185
|
+
const at: UvPage = { name: page.name, width: page.width, height: page.height };
|
|
186
|
+
for (const region of page.regions) if (!first.has(region.name)) first.set(region.name, { page: at, region });
|
|
187
|
+
}
|
|
188
|
+
return (name) => first.get(name) ?? null;
|
|
189
|
+
}
|
|
190
|
+
|
|
171
191
|
/**
|
|
172
192
|
* The readings the header's tables rejected, each a switch — what the core
|
|
173
193
|
* suite's `CU` plants pass to show the gate names it. The functions below with
|
package/src/core/world.ts
CHANGED
|
@@ -3,9 +3,24 @@
|
|
|
3
3
|
* the compiled model's bones (issue #925). Written from what a bone's fields
|
|
4
4
|
* mean and from measurement against the runtime's pose dump
|
|
5
5
|
* (`tools/pose_oracle.ts dump`, spine-core 4.3.13), and held to it by the core
|
|
6
|
-
* suite's equivalence controls
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* suite's equivalence controls.
|
|
7
|
+
*
|
|
8
|
+
* ⭐ **The compiler's setup transforms are this evaluator too** (issue #1015),
|
|
9
|
+
* under the same arithmetic (issue #1021). `src/transform.ts` held a second
|
|
10
|
+
* evaluator of its own until #1015, and the two differed in exactly three
|
|
11
|
+
* places, none of them a different matrix: the degree factor (`Math.PI / 180`
|
|
12
|
+
* there, the runtime's 3.1415927 here), the frame of a bone with scale 1 and
|
|
13
|
+
* no shear (`[cos, −sin, sin, cos]` of the rotation there, the y column at
|
|
14
|
+
* `rotation + 90` here), and how a radian angle is turned into degrees
|
|
15
|
+
* (divided by the degree factor there, multiplied by `180 / pi` here). #1015
|
|
16
|
+
* made them a parameter (`WorldArithmetic`) so that no byte moved; #1021
|
|
17
|
+
* measured which binds closer — every point the compiler binds from a world
|
|
18
|
+
* position, posed by spine-core from the emitted build, against that position
|
|
19
|
+
* — and the runtime's three put every one at the float32 floor where the
|
|
20
|
+
* compiler's left `gallery/look`'s up to 8.6e-5 off. So the compiler passes
|
|
21
|
+
* none now, and `RUNTIME_ARITHMETIC`, the measured choices below, is the only
|
|
22
|
+
* arithmetic anything here runs under; the parameter stays for the plants
|
|
23
|
+
* that swap one choice back in to show a control fire.
|
|
9
24
|
*
|
|
10
25
|
* ## What each measured choice is
|
|
11
26
|
*
|
|
@@ -99,6 +114,28 @@ export const RUNTIME_DEG = 180 / RUNTIME_PI;
|
|
|
99
114
|
/** A parent x axis whose squared length is at most this is collapsed for `noRotationOrReflection` (issue #979): `1e-5` squared, as measured — see the header. */
|
|
100
115
|
export const COLLAPSED_X_AXIS_SQ = 0.00001 * 0.00001;
|
|
101
116
|
|
|
117
|
+
/**
|
|
118
|
+
* The three places an evaluator's arithmetic may differ in the last bit while
|
|
119
|
+
* computing the same matrix (issue #1015) — see the header. The core poses and
|
|
120
|
+
* the compiler binds with `RUNTIME_ARITHMETIC` (issue #1021); another value is
|
|
121
|
+
* a plant's.
|
|
122
|
+
*/
|
|
123
|
+
export interface WorldArithmetic {
|
|
124
|
+
/** Degrees to radians: every angle is multiplied by it. */
|
|
125
|
+
radiansPerDegree: number;
|
|
126
|
+
/** Radians to degrees — the angle `noRotationOrReflection` takes off a parent's axis. */
|
|
127
|
+
degreesOf: (radians: number) => number;
|
|
128
|
+
/** A bone with scale 1 and no shear takes `[cos r, −sin r, sin r, cos r]` instead of the y column at `r + 90`; the general frame everywhere else. */
|
|
129
|
+
rotationOnlyFrame: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The runtime's arithmetic, as measured (the header's three choices): what the core poses with. */
|
|
133
|
+
export const RUNTIME_ARITHMETIC: WorldArithmetic = {
|
|
134
|
+
radiansPerDegree: RAD,
|
|
135
|
+
degreesOf: (radians) => radians * RUNTIME_DEG,
|
|
136
|
+
rotationOnlyFrame: false,
|
|
137
|
+
};
|
|
138
|
+
|
|
102
139
|
/** One bone's world transform: the matrix `[a b; c d]` and the origin, y up. */
|
|
103
140
|
export interface CoreWorld {
|
|
104
141
|
a: number;
|
|
@@ -114,16 +151,27 @@ export type CoreInheritMode = 'normal' | 'onlyTranslation' | 'noRotationOrReflec
|
|
|
114
151
|
|
|
115
152
|
export type M2 = [number, number, number, number];
|
|
116
153
|
|
|
117
|
-
/** What computes a child's world matrix from its mode, its parent's world
|
|
118
|
-
export type InheritComputation = (mode: CoreInheritMode, parent: CoreWorld, bone: ModelBone) => M2;
|
|
154
|
+
/** What computes a child's world matrix from its mode, its parent's world, its own fields and the arithmetic in force. */
|
|
155
|
+
export type InheritComputation = (mode: CoreInheritMode, parent: CoreWorld, bone: ModelBone, arithmetic: WorldArithmetic) => M2;
|
|
119
156
|
|
|
120
157
|
/** The matrix of a rotation, shears and scales, in the column form the header states. */
|
|
121
|
-
function frame(rotation: number, shearX: number, shearY: number, scaleX: number, scaleY: number): M2 {
|
|
122
|
-
const xAngle = (rotation + shearX) *
|
|
123
|
-
const yAngle = (rotation + 90 + shearY) *
|
|
158
|
+
function frame(rotation: number, shearX: number, shearY: number, scaleX: number, scaleY: number, rad: number = RAD): M2 {
|
|
159
|
+
const xAngle = (rotation + shearX) * rad;
|
|
160
|
+
const yAngle = (rotation + 90 + shearY) * rad;
|
|
124
161
|
return [Math.cos(xAngle) * scaleX, Math.cos(yAngle) * scaleY, Math.sin(xAngle) * scaleX, Math.sin(yAngle) * scaleY];
|
|
125
162
|
}
|
|
126
163
|
|
|
164
|
+
/** A bone's own frame — a root's matrix, and the local factor of `normal` and `onlyTranslation` — under `arithmetic`. */
|
|
165
|
+
function localFrame(arithmetic: WorldArithmetic, rotation: number, shearX: number, shearY: number, scaleX: number, scaleY: number): M2 {
|
|
166
|
+
const rad = arithmetic.radiansPerDegree;
|
|
167
|
+
if (arithmetic.rotationOnlyFrame && scaleX === 1 && scaleY === 1 && shearX === 0 && shearY === 0) {
|
|
168
|
+
const cos = Math.cos(rotation * rad);
|
|
169
|
+
const sin = Math.sin(rotation * rad);
|
|
170
|
+
return [cos, -sin, sin, cos];
|
|
171
|
+
}
|
|
172
|
+
return frame(rotation, shearX, shearY, scaleX, scaleY, rad);
|
|
173
|
+
}
|
|
174
|
+
|
|
127
175
|
function times(p: M2, q: M2): M2 {
|
|
128
176
|
return [p[0] * q[0] + p[1] * q[2], p[0] * q[1] + p[1] * q[3], p[2] * q[0] + p[3] * q[2], p[2] * q[1] + p[3] * q[3]];
|
|
129
177
|
}
|
|
@@ -135,8 +183,8 @@ function times(p: M2, q: M2): M2 {
|
|
|
135
183
|
* which builds the frame to read the bone's columns in from the rotation it
|
|
136
184
|
* has just read, through this same arithmetic (issue #966).
|
|
137
185
|
*/
|
|
138
|
-
export function noScaleDirection(p: M2, rotation: number): [number, number] {
|
|
139
|
-
const r = rotation *
|
|
186
|
+
export function noScaleDirection(p: M2, rotation: number, rad: number = RAD): [number, number] {
|
|
187
|
+
const r = rotation * rad;
|
|
140
188
|
const cos = Math.cos(r);
|
|
141
189
|
const sin = Math.sin(r);
|
|
142
190
|
const ux = p[0] * cos + p[1] * sin;
|
|
@@ -146,19 +194,20 @@ export function noScaleDirection(p: M2, rotation: number): [number, number] {
|
|
|
146
194
|
return [ux * inverse, uy * inverse];
|
|
147
195
|
}
|
|
148
196
|
|
|
149
|
-
/** The world matrix of a child under `parent` in `mode
|
|
150
|
-
function modeMatrix(mode: CoreInheritMode, parent: CoreWorld, bone: ModelBone): M2 {
|
|
197
|
+
/** The world matrix of a child under `parent` in `mode`, under `arithmetic` (the runtime's unless a caller passes its own). */
|
|
198
|
+
function modeMatrix(mode: CoreInheritMode, parent: CoreWorld, bone: ModelBone, arithmetic: WorldArithmetic = RUNTIME_ARITHMETIC): M2 {
|
|
151
199
|
const rotation = bone.rotation ?? 0;
|
|
152
200
|
const shearX = bone.shearX ?? 0;
|
|
153
201
|
const shearY = bone.shearY ?? 0;
|
|
154
202
|
const scaleX = bone.scaleX ?? 1;
|
|
155
203
|
const scaleY = bone.scaleY ?? 1;
|
|
204
|
+
const rad = arithmetic.radiansPerDegree;
|
|
156
205
|
const p: M2 = [parent.a, parent.b, parent.c, parent.d];
|
|
157
206
|
switch (mode) {
|
|
158
207
|
case 'normal':
|
|
159
|
-
return times(p,
|
|
208
|
+
return times(p, localFrame(arithmetic, rotation, shearX, shearY, scaleX, scaleY));
|
|
160
209
|
case 'onlyTranslation':
|
|
161
|
-
return
|
|
210
|
+
return localFrame(arithmetic, rotation, shearX, shearY, scaleX, scaleY);
|
|
162
211
|
case 'noRotationOrReflection': {
|
|
163
212
|
const xLengthSq = p[0] * p[0] + p[2] * p[2];
|
|
164
213
|
let conformal: M2;
|
|
@@ -166,23 +215,23 @@ function modeMatrix(mode: CoreInheritMode, parent: CoreWorld, bone: ModelBone):
|
|
|
166
215
|
if (xLengthSq > COLLAPSED_X_AXIS_SQ) {
|
|
167
216
|
const k = Math.abs(p[0] * p[3] - p[1] * p[2]) / xLengthSq;
|
|
168
217
|
conformal = [p[0], -p[2] * k, p[2], p[0] * k];
|
|
169
|
-
r = rotation - Math.atan2(p[2], p[0])
|
|
218
|
+
r = rotation - arithmetic.degreesOf(Math.atan2(p[2], p[0]));
|
|
170
219
|
} else {
|
|
171
220
|
// A collapsed x axis (issue #979): the parent's y column with its x component negated, and the rotation less 90 plus the y column's angle — see the header.
|
|
172
221
|
conformal = [0, -p[1], 0, p[3]];
|
|
173
|
-
r = rotation - 90 + Math.atan2(p[3], p[1])
|
|
222
|
+
r = rotation - 90 + arithmetic.degreesOf(Math.atan2(p[3], p[1]));
|
|
174
223
|
}
|
|
175
224
|
// The y angle adds the shear before the right angle here, unlike `frame` (issue #966): `(r + 90 + shearY)` reads last-bit off on 81 of 205 sheared or scaled bones at a 1e9 amplifier, `(r + shearY + 90)` on none; `r = rotation − parentAngle` taken first (adding the shear before the parent's angle is taken off reads 102 of 205 off).
|
|
176
|
-
const xAngle = (r + shearX) *
|
|
177
|
-
const yAngle = (r + shearY + 90) *
|
|
225
|
+
const xAngle = (r + shearX) * rad;
|
|
226
|
+
const yAngle = (r + shearY + 90) * rad;
|
|
178
227
|
return times(conformal, [Math.cos(xAngle) * scaleX, Math.cos(yAngle) * scaleY, Math.sin(xAngle) * scaleX, Math.sin(yAngle) * scaleY]);
|
|
179
228
|
}
|
|
180
229
|
case 'noScale':
|
|
181
230
|
case 'noScaleOrReflection': {
|
|
182
|
-
const [ux, uy] = noScaleDirection(p, rotation);
|
|
231
|
+
const [ux, uy] = noScaleDirection(p, rotation, rad);
|
|
183
232
|
const flip = mode === 'noScale' && p[0] * p[3] - p[1] * p[2] < 0 ? -1 : 1;
|
|
184
233
|
const turned: M2 = [ux, -uy * flip, uy, ux * flip];
|
|
185
|
-
return times(turned, frame(0, shearX, shearY, scaleX, scaleY));
|
|
234
|
+
return times(turned, frame(0, shearX, shearY, scaleX, scaleY, rad));
|
|
186
235
|
}
|
|
187
236
|
}
|
|
188
237
|
}
|
|
@@ -205,9 +254,16 @@ function modeOf(bone: ModelBone): CoreInheritMode {
|
|
|
205
254
|
* world as a skeleton is created — and a bone under it, active or not, is
|
|
206
255
|
* posed from those zeros like any child. `inherit` replaces the mode
|
|
207
256
|
* computation — the core suite's plant passes a copy with one mode's sign
|
|
208
|
-
* flipped, and nothing else does.
|
|
257
|
+
* flipped, and nothing else does. `arithmetic` is the runtime's unless a
|
|
258
|
+
* caller states its own, and only a plant does: `src/transform.ts` takes the
|
|
259
|
+
* default since issue #1021, as every posing path does.
|
|
209
260
|
*/
|
|
210
|
-
export function worldTransforms(
|
|
261
|
+
export function worldTransforms(
|
|
262
|
+
bones: readonly ModelBone[],
|
|
263
|
+
active: ReadonlySet<string> | null = null,
|
|
264
|
+
inherit: InheritComputation = modeMatrix,
|
|
265
|
+
arithmetic: WorldArithmetic = RUNTIME_ARITHMETIC,
|
|
266
|
+
): Map<string, CoreWorld> {
|
|
211
267
|
const out = new Map<string, CoreWorld>();
|
|
212
268
|
for (const bone of bones) {
|
|
213
269
|
if (active !== null && !active.has(bone.name)) {
|
|
@@ -217,13 +273,13 @@ export function worldTransforms(bones: readonly ModelBone[], active: ReadonlySet
|
|
|
217
273
|
const x = bone.x ?? 0;
|
|
218
274
|
const y = bone.y ?? 0;
|
|
219
275
|
if (bone.parent === undefined) {
|
|
220
|
-
const [a, b, c, d] =
|
|
276
|
+
const [a, b, c, d] = localFrame(arithmetic, bone.rotation ?? 0, bone.shearX ?? 0, bone.shearY ?? 0, bone.scaleX ?? 1, bone.scaleY ?? 1);
|
|
221
277
|
out.set(bone.name, { a, b, c, d, worldX: x, worldY: y });
|
|
222
278
|
continue;
|
|
223
279
|
}
|
|
224
280
|
const parent = out.get(bone.parent);
|
|
225
281
|
if (parent === undefined) throw new Error(`bone "${bone.name}": parent "${bone.parent}" is not posed before it`);
|
|
226
|
-
const [a, b, c, d] = inherit(modeOf(bone), parent, bone);
|
|
282
|
+
const [a, b, c, d] = inherit(modeOf(bone), parent, bone, arithmetic);
|
|
227
283
|
out.set(bone.name, { a, b, c, d, worldX: parent.a * x + parent.b * y + parent.worldX, worldY: parent.c * x + parent.d * y + parent.worldY });
|
|
228
284
|
}
|
|
229
285
|
return out;
|