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/render_core.ts
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
* The render's second poser (issue #968, step 3d of issue #380): the posing
|
|
3
3
|
* seam of `src/render.ts` (`Poser` / `Posed`) implemented over rigc's own
|
|
4
4
|
* core — the compiled model document (`skeleton.model.json`) posed by
|
|
5
|
-
* `src/core/`,
|
|
6
|
-
*
|
|
5
|
+
* `src/core/`, each drawn region placed on its page by the document's own
|
|
6
|
+
* `pages` section (issue #1016; a `rigc-compiled/1` document, which has none,
|
|
7
|
+
* by the atlas beside it, read by rigc's own reader `src/atlas.ts`). Nothing
|
|
8
|
+
* here links spine-core, and nothing here is reached
|
|
7
9
|
* from `src/core/`: the core stays pure, and this module is the adapter from
|
|
8
10
|
* its raw entry to the renderer's shapes.
|
|
9
11
|
*
|
|
10
12
|
* ⭐ Why a module of its own rather than a second block in `src/render.ts`.
|
|
11
|
-
* `render.ts` links spine-core (
|
|
12
|
-
* `spinePoser`), so a core poser written there would share an import list
|
|
13
|
+
* `render.ts` links spine-core (an export's atlas pages and texture
|
|
14
|
+
* substitution, and `spinePoser`), so a core poser written there would share an import list
|
|
13
15
|
* with the runtime it replaces — and "the core poser imports nothing from
|
|
14
16
|
* spine-core" could then only be said, not read off the file. Here it can be:
|
|
15
17
|
* the value imports below are the core, the atlas reader and nothing else, and
|
|
@@ -32,7 +34,11 @@
|
|
|
32
34
|
* region a drawn attachment samples (`drawnRegions` over the pose's `shown`
|
|
33
35
|
* records and draw order), a region's four UVs (`regionPageUvs`) and a
|
|
34
36
|
* mesh's (`meshPageUvs`), each held to `sequence.getUVs(index)` at
|
|
35
|
-
* tolerance 0 on every corpus by `core_gate`'s `uvs` blocks.
|
|
37
|
+
* tolerance 0 on every corpus by `core_gate`'s `uvs` blocks. Where each
|
|
38
|
+
* region sits — its page, `x`, `y`, turn and the page's size — is the
|
|
39
|
+
* document's `pages` section (issue #1016, `placementOf`), the atlas `build`
|
|
40
|
+
* wrote spelled into the document, so a build draws with its `.atlas`
|
|
41
|
+
* removed.
|
|
36
42
|
* - **The tint** is the slot's light colour times the attachment's colour,
|
|
37
43
|
* channel by channel, as `src/render.ts`'s `tintOf` forms it; the dark
|
|
38
44
|
* colour is the slot's, absent where the slot carries none.
|
|
@@ -80,15 +86,16 @@ import type {
|
|
|
80
86
|
PoseOptions,
|
|
81
87
|
Posed,
|
|
82
88
|
Poser,
|
|
89
|
+
SkinRoster,
|
|
83
90
|
SlotSubset,
|
|
84
91
|
} from './render.ts';
|
|
85
|
-
import { atlasRegionLookup, parseAtlasText
|
|
86
|
-
import { spineFileSha256 } from './model.ts';
|
|
92
|
+
import { atlasRegionLookup, parseAtlasText } from './atlas.ts';
|
|
93
|
+
import { pagesOfAtlas, spineFileSha256, type ModelPage } from './model.ts';
|
|
87
94
|
import { clipThrough, type ClipShape, type ShapeClipper } from './core/clipping.ts';
|
|
88
|
-
import { CoreInputError, readModel, sourceOfDoc, underSkin, type CompiledDocument, type CoreSlotRow } from './core/index.ts';
|
|
95
|
+
import { activeBones, CoreInputError, readModel, sourceOfDoc, underSkin, type CompiledDocument, type CoreSlotRow } from './core/index.ts';
|
|
89
96
|
import { poseRawAnimation, poseRawSetup, type RawDrawn, type RawPose } from './core/raw.ts';
|
|
90
97
|
import { CORE_ALL_SKINS, CORE_DEFAULT_SKIN, lookupSkins } from './core/skins.ts';
|
|
91
|
-
import { drawnRegions, meshPageUvs, readUvSequences, regionPageUvs, type DrawnRegion, type UvSource } from './core/uvs.ts';
|
|
98
|
+
import { documentPageLookup, drawnRegions, meshPageUvs, readUvSequences, regionPageUvs, type DrawnRegion, type UvRegion, type UvSource } from './core/uvs.ts';
|
|
92
99
|
import { regionCorners, worldVertices } from './core/vertices.ts';
|
|
93
100
|
import type { CoreWorld } from './core/world.ts';
|
|
94
101
|
|
|
@@ -256,20 +263,27 @@ export function clipSourceOf(
|
|
|
256
263
|
/** The runtime's own triangulation of a region's quad — `src/render.ts`'s `QUAD_TRIANGLES`. */
|
|
257
264
|
const QUAD_TRIANGLES: readonly number[] = [0, 1, 2, 2, 3, 0];
|
|
258
265
|
|
|
259
|
-
/**
|
|
266
|
+
/** A region as texture substitution reads it: the page-UV rules' numbers, and the name and `index:` its key is made of (`regionKey`). */
|
|
267
|
+
type TextureRegion = UvRegion & { name: string; index: number };
|
|
268
|
+
|
|
269
|
+
/** Everything a core pose reads besides the pose: the document under one skin, and where each region sits on its page. */
|
|
260
270
|
interface CoreInput {
|
|
261
271
|
doc: CompiledDocument;
|
|
262
272
|
source: UvSource;
|
|
263
|
-
/** The
|
|
264
|
-
region: (name: string) =>
|
|
273
|
+
/** The region a name draws — the document's `pages`, or for a `/1` document the atlas's — what an original-art UV reads its trim from. */
|
|
274
|
+
region: (name: string) => TextureRegion | null;
|
|
265
275
|
}
|
|
266
276
|
|
|
267
277
|
/**
|
|
268
278
|
* The document posed with no skin set — spine-core's initial state, every slot
|
|
269
279
|
* resolved through the default skin alone — or refused by name where that
|
|
270
280
|
* equivalence was not measured (the header's *The skin*).
|
|
281
|
+
*
|
|
282
|
+
* Exported for the model side of the validator (issue #1025), which poses a
|
|
283
|
+
* slot the way `validate()` does — a fresh skeleton, no skin set — and must
|
|
284
|
+
* resolve that state by this rule rather than by a copy of it.
|
|
271
285
|
*/
|
|
272
|
-
function noSkinView(doc: CompiledDocument): CompiledDocument {
|
|
286
|
+
export function noSkinView(doc: CompiledDocument): CompiledDocument {
|
|
273
287
|
if (doc.skins.length === 0) return underSkin(doc, CORE_ALL_SKINS);
|
|
274
288
|
const fallback = doc.skins.find((k) => k.name === CORE_DEFAULT_SKIN);
|
|
275
289
|
if (fallback === undefined) {
|
|
@@ -300,12 +314,12 @@ function worldOf(pose: RawPose): Map<string, CoreWorld> {
|
|
|
300
314
|
}
|
|
301
315
|
|
|
302
316
|
/** `regionKey` in `src/render.ts`: the trimmed name and the sequence index — the key a substitution matches on. */
|
|
303
|
-
function regionKey(region:
|
|
317
|
+
function regionKey(region: TextureRegion): string {
|
|
304
318
|
return `${region.name.trim()}#${region.index}`;
|
|
305
319
|
}
|
|
306
320
|
|
|
307
321
|
/** `artUvsOf` in `src/render.ts`, over rigc's atlas reader: a mesh's own UVs, a region's kept rectangle in the drawing's space. */
|
|
308
|
-
function artUvsOf(drawn: RawDrawn, region:
|
|
322
|
+
function artUvsOf(drawn: RawDrawn, region: TextureRegion): PieceTexture | undefined {
|
|
309
323
|
if (drawn.kind === 'mesh') return { region: regionKey(region), artUvs: [...drawn.uvs] };
|
|
310
324
|
if (region.degrees !== 0) return undefined;
|
|
311
325
|
const ow = region.originalWidth;
|
|
@@ -515,9 +529,70 @@ function restOf(view: CompiledDocument, setup: RawPose, shown: readonly Attachme
|
|
|
515
529
|
}
|
|
516
530
|
|
|
517
531
|
/**
|
|
518
|
-
* `
|
|
519
|
-
*
|
|
520
|
-
*
|
|
532
|
+
* The first place two `pages` sections differ, by path, or `null` when they
|
|
533
|
+
* are the same — the page count, a page's name or size, a region count, or a
|
|
534
|
+
* region's name or one of its numbers.
|
|
535
|
+
*/
|
|
536
|
+
export function firstPageDifference(stated: readonly ModelPage[], found: readonly ModelPage[]): string | null {
|
|
537
|
+
if (stated.length !== found.length) return `the document states ${stated.length} page(s), the atlas has ${found.length}`;
|
|
538
|
+
for (let i = 0; i < stated.length; i++) {
|
|
539
|
+
const a = stated[i];
|
|
540
|
+
const b = found[i];
|
|
541
|
+
for (const key of ['name', 'width', 'height'] as const) {
|
|
542
|
+
if (a[key] !== b[key]) return `pages[${i}].${key} is ${JSON.stringify(a[key])} in the document and ${JSON.stringify(b[key])} in the atlas`;
|
|
543
|
+
}
|
|
544
|
+
if (a.regions.length !== b.regions.length) return `pages[${i}] "${a.name}" holds ${a.regions.length} region(s) in the document and ${b.regions.length} in the atlas`;
|
|
545
|
+
for (let j = 0; j < a.regions.length; j++) {
|
|
546
|
+
const r = a.regions[j];
|
|
547
|
+
const q = b.regions[j];
|
|
548
|
+
for (const key of Object.keys(r) as Array<keyof typeof r>) {
|
|
549
|
+
if (r[key] !== q[key]) return `pages[${i}] "${a.name}" region ${JSON.stringify(r.name)}: ${key} is ${JSON.stringify(r[key])} in the document and ${JSON.stringify(q[key])} in the atlas`;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Where each region a core pose draws sits on its page, read from the
|
|
558
|
+
* document's `pages` section (issue #1016) — or, for a `rigc-compiled/1`
|
|
559
|
+
* document, which has none, from `atlasText` as before. With both, the atlas
|
|
560
|
+
* must be the one the document was written beside: a page or region that
|
|
561
|
+
* differs is refused naming the first difference, so the core never draws the
|
|
562
|
+
* build's placement over another atlas's pages while spine-core, reading that
|
|
563
|
+
* atlas, would draw another picture.
|
|
564
|
+
*/
|
|
565
|
+
function placementOf(doc: CompiledDocument, atlasText: string, where: string): { lookup: UvSource['lookup']; region: CoreInput['region'] } {
|
|
566
|
+
if (doc.pages !== null) {
|
|
567
|
+
if (atlasText !== '') {
|
|
568
|
+
const differs = firstPageDifference(doc.pages, pagesOfAtlas(atlasText));
|
|
569
|
+
if (differs !== null) {
|
|
570
|
+
throw new CoreInputError(
|
|
571
|
+
`the atlas beside ${where} is not the one it was written beside: ${differs} — ` +
|
|
572
|
+
'the core would draw the build\'s placement over pages the atlas has rearranged',
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const lookup = documentPageLookup(doc.pages);
|
|
577
|
+
return { lookup, region: (name) => lookup(name)?.region ?? null };
|
|
578
|
+
}
|
|
579
|
+
if (atlasText === '') {
|
|
580
|
+
throw new CoreInputError(
|
|
581
|
+
`${where} is a ${doc.spec} document, which does not state where each region sits on its page (the pages section, issue #1016), ` +
|
|
582
|
+
'and no atlas was given to read it from — rebuild it to carry them, or pose it beside the atlas it was built with',
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
const lookup = atlasRegionLookup(parseAtlasText(atlasText));
|
|
586
|
+
return { lookup, region: (name) => lookup(name)?.region ?? null };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* `Poser` over rigc's own core: `modelText` a `rigc-compiled/2` document
|
|
591
|
+
* (`skeleton.model.json`), which states where each region sits on its page
|
|
592
|
+
* (`pages`, issue #1016), so `atlasText` may be `''`; given, it is held to
|
|
593
|
+
* the document's `pages` (`placementOf`). A `rigc-compiled/1` document states
|
|
594
|
+
* no placement and is drawn through `atlasText`, the atlas `build` wrote
|
|
595
|
+
* beside it, as before; with none given it is refused by name. With `skeleton`, the Spine file beside the document
|
|
521
596
|
* is held to the digest the document records (`spine.sha256`, issue #968) and
|
|
522
597
|
* refused, naming both digests, when it is not that build's. Refused by
|
|
523
598
|
* `CoreInputError`, naming why, where the document or the atlas cannot be read; a pose the core leaves a block of out
|
|
@@ -542,8 +617,7 @@ export function corePoser(modelText: string, atlasText: string, where = 'skeleto
|
|
|
542
617
|
throw new CoreInputError(`${where}: not JSON — ${(err as Error).message}`);
|
|
543
618
|
}
|
|
544
619
|
const sequences = readUvSequences(parsed);
|
|
545
|
-
const
|
|
546
|
-
const lookup = atlasRegionLookup(atlas);
|
|
620
|
+
const { lookup, region } = placementOf(doc, atlasText, where);
|
|
547
621
|
const views = new Map<string, CompiledDocument>();
|
|
548
622
|
const inputOf = (skin: string | undefined): CoreInput => {
|
|
549
623
|
const key = skin === undefined ? '' : `=${skin}`;
|
|
@@ -552,7 +626,7 @@ export function corePoser(modelText: string, atlasText: string, where = 'skeleto
|
|
|
552
626
|
view = skin === undefined ? noSkinView(doc) : underSkin(doc, skin);
|
|
553
627
|
views.set(key, view);
|
|
554
628
|
}
|
|
555
|
-
return { doc: view, source: { lookup, sequences }, region
|
|
629
|
+
return { doc: view, source: { lookup, sequences }, region };
|
|
556
630
|
};
|
|
557
631
|
const roster: SubsetRoster = {
|
|
558
632
|
declared: doc.slots.map((s) => s.name),
|
|
@@ -580,3 +654,86 @@ export function corePoser(modelText: string, atlasText: string, where = 'skeleto
|
|
|
580
654
|
},
|
|
581
655
|
};
|
|
582
656
|
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* The skin roster behind the core poser (`SkinRoster` in `src/render.ts`,
|
|
660
|
+
* issue #1014): the bones each skin leaves unposed, read off the model
|
|
661
|
+
* document — `unposedBones` over `activeBones` of the skin's view, the
|
|
662
|
+
* predicate the raw pose flags its bones with, and under no skin the view the
|
|
663
|
+
* core poses with no skin set (`noSkinView`, refused by `CoreInputError` where
|
|
664
|
+
* it refuses to pose). `skins` is the Spine file's own list, in its order, which
|
|
665
|
+
* the document does not hold (`resolveSkinView`'s note in `./core/index.ts`).
|
|
666
|
+
*
|
|
667
|
+
* Measured against the runtime's reading (`skinRosterOf`, a fresh skeleton's
|
|
668
|
+
* `active` under each skin and under none) on every rigc build the tree
|
|
669
|
+
* carries: the same bones, under every skin. Since issue #1020 it is handed the
|
|
670
|
+
* document `coreDocumentFacts` read for the run's other facts, so the roster
|
|
671
|
+
* costs no reading of its own; a skin's view is still built only when a
|
|
672
|
+
* question is asked of it.
|
|
673
|
+
*/
|
|
674
|
+
function coreSkinRoster(doc: CompiledDocument, skins: readonly string[]): SkinRoster {
|
|
675
|
+
return {
|
|
676
|
+
skins,
|
|
677
|
+
unposedUnder: (skin) => {
|
|
678
|
+
const view = skin === undefined ? noSkinView(doc) : underSkin(doc, skin);
|
|
679
|
+
const active = activeBones(view);
|
|
680
|
+
return unposedBones(view.bones.map((b) => ({ name: b.name, parent: b.parent ?? null, active: active.has(b.name) })));
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* What `render` and `check` read off a rigc build's model document besides the
|
|
687
|
+
* pose (issue #1020) — so a build the core poses draws with its
|
|
688
|
+
* `skeleton.atlas` gone, and reads off `skeleton.json` only what the document
|
|
689
|
+
* does not state.
|
|
690
|
+
*/
|
|
691
|
+
export interface CoreDocumentFacts {
|
|
692
|
+
/** The document's spec: `rigc-compiled/2` states where each region sits on its page, `rigc-compiled/1` does not. */
|
|
693
|
+
spec: string;
|
|
694
|
+
/**
|
|
695
|
+
* Every page the `pages` section states, by name, in file order — the
|
|
696
|
+
* images the draw samples, read by these names; `null` for a
|
|
697
|
+
* `rigc-compiled/1` document, whose pages only its atlas names.
|
|
698
|
+
*/
|
|
699
|
+
pageNames: readonly string[] | null;
|
|
700
|
+
/** The skin roster behind the core poser (`coreSkinRoster`), over this one reading. */
|
|
701
|
+
roster: SkinRoster;
|
|
702
|
+
/**
|
|
703
|
+
* The slot subset's roster (`subsetOver`): the slots in the document's draw
|
|
704
|
+
* order, its default skin, and the skins the document files a slot's
|
|
705
|
+
* attachments under. A refusal lists those skins, and the order it lists
|
|
706
|
+
* them in is the Spine file's, which the document does not hold
|
|
707
|
+
* (`./core/index.ts`, *Several skins filling one placeholder*) — so they are
|
|
708
|
+
* put in `skins`' order, the Spine file's own list.
|
|
709
|
+
*/
|
|
710
|
+
subset: SubsetRoster;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* The document's facts for `render` and `check` (`CoreDocumentFacts`), read
|
|
715
|
+
* once. `skins` is the Spine file's skin list, in its order — the one fact
|
|
716
|
+
* here the document does not state. Refused by `CoreInputError` where
|
|
717
|
+
* `readModel` refuses the document.
|
|
718
|
+
*/
|
|
719
|
+
export function coreDocumentFacts(modelText: string, where: string, skins: readonly string[]): CoreDocumentFacts {
|
|
720
|
+
const doc = readModel(modelText, where);
|
|
721
|
+
const rank = (name: string): number => {
|
|
722
|
+
const at = skins.indexOf(name);
|
|
723
|
+
return at < 0 ? skins.length : at;
|
|
724
|
+
};
|
|
725
|
+
return {
|
|
726
|
+
spec: doc.spec,
|
|
727
|
+
pageNames: doc.pages === null ? null : doc.pages.map((page) => page.name),
|
|
728
|
+
roster: coreSkinRoster(doc, skins),
|
|
729
|
+
subset: {
|
|
730
|
+
declared: doc.slots.map((s) => s.name),
|
|
731
|
+
carriers: (slot) =>
|
|
732
|
+
doc.skins
|
|
733
|
+
.filter((k) => Object.keys(k.attachments[slot] ?? {}).length > 0)
|
|
734
|
+
.map((k) => k.name)
|
|
735
|
+
.sort((a, b) => rank(a) - rank(b)),
|
|
736
|
+
defaultSkin: doc.skins.some((k) => k.name === CORE_DEFAULT_SKIN) ? CORE_DEFAULT_SKIN : null,
|
|
737
|
+
},
|
|
738
|
+
};
|
|
739
|
+
}
|
package/src/transform.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* measured polygon. That is why the old code refused rotation instead of ignoring
|
|
12
12
|
* it, and why this file exists rather than a relaxed assertion.
|
|
13
13
|
*
|
|
14
|
-
* The composition
|
|
14
|
+
* The composition, for a bone with a parent in the `normal` mode:
|
|
15
15
|
*
|
|
16
16
|
* la = cos(rot) lb = cos(rot + 90) = -sin(rot)
|
|
17
17
|
* lc = sin(rot) ld = sin(rot + 90) = cos(rot)
|
|
@@ -28,9 +28,44 @@
|
|
|
28
28
|
* at scale 2 measured **0.752×** the runtime's own `PathConstraint.curves` on the
|
|
29
29
|
* build's posed geometry, and a production rig's weighted path 2.35×. The
|
|
30
30
|
* weights were blended all along; the matrices they were blended through were
|
|
31
|
-
* wrong.
|
|
32
|
-
*
|
|
31
|
+
* wrong.
|
|
32
|
+
*
|
|
33
|
+
* ⭐ **`computeWorldTransforms` is the core's evaluator** (`worldTransforms` in
|
|
34
|
+
* [`src/core/world.ts`](core/world.ts), issue #1015), whose every rule — the five
|
|
35
|
+
* inherit modes, the collapsed parent axis, the operation orders — was measured
|
|
36
|
+
* against the runtime's pose dump and is held there by the core suite. Since
|
|
37
|
+
* issue #1021 it runs under the runtime's own arithmetic (`RUNTIME_ARITHMETIC`,
|
|
38
|
+
* the evaluator's default), so the matrices every point is bound through are the
|
|
39
|
+
* ones the runtime poses the build with, to the bit. This file keeps what is the
|
|
40
|
+
* compiler's own: the coordinate contract, the refusals by name, the world
|
|
41
|
+
* rotation a region cancels, and the inverses.
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ **Why the runtime's arithmetic and not the textbook's** (issue #1021). A
|
|
44
|
+
* point is bound by inverting a setup matrix; the runtime then poses it through
|
|
45
|
+
* its own. The two used to differ in the last bits — `Math.PI` against the
|
|
46
|
+
* runtime's 3.1415927, `[cos, −sin, sin, cos]` against the y column taken at
|
|
47
|
+
* `rotation + 90`, radians to degrees by division against multiplication — and
|
|
48
|
+
* that is not noise below the file's precision: an unrotated root reads
|
|
49
|
+
* `b = −2.3e-8` in the runtime, so a point 256 units above its bone was bound
|
|
50
|
+
* 5.9e-6 off before the float32 spelling was even applied. Measured from inside
|
|
51
|
+
* `compile` (every authored world position `toBoneLocal` was handed) against
|
|
52
|
+
* spine-core's pose of the emitted build, the old arithmetic put `gallery/look`'s
|
|
53
|
+
* 312 bound points up to 8.6e-5 from where they were authored (RMS 5.1e-5) and
|
|
54
|
+
* `gallery/flex`'s 258 up to 2.3e-5; the runtime's puts every one of them at the
|
|
55
|
+
* float32 floor — the distance the float32 spelling of the exact inverse alone
|
|
56
|
+
* accounts for (look 8.1e-6, flex 4.1e-6). `CO24` holds it.
|
|
57
|
+
*
|
|
58
|
+
* 🔸 **The rule has a second half: what is MEASURED reads the exact frame.**
|
|
59
|
+
* The runtime's arithmetic is for what is bound — the matrices a point is
|
|
60
|
+
* inverted through. Anything the compiler measures of the authored rig and
|
|
61
|
+
* reports, compares against a threshold, sorts by or refuses on is a statement
|
|
62
|
+
* about the spec, and reads `computeExactFrameTransforms` (below): a depth turn
|
|
63
|
+
* ceiling, a ring's control angles, a `segments` mesh's segments and falloff.
|
|
64
|
+
* An angle the author stated as 90 has to print as 90.
|
|
33
65
|
*/
|
|
66
|
+
import type { ModelBone } from './model.ts';
|
|
67
|
+
import { modeMatrix, RUNTIME_ARITHMETIC, worldTransforms, type CoreWorld, type WorldArithmetic } from './core/world.ts';
|
|
68
|
+
|
|
34
69
|
/**
|
|
35
70
|
* What `computeWorldTransforms` reads off a bone — a structural type, so both a
|
|
36
71
|
* Spine bone (`SpineBone`, what spine-parts and every skeleton reader hold) and
|
|
@@ -68,8 +103,6 @@ export interface BoneTransform {
|
|
|
68
103
|
|
|
69
104
|
export class TransformError extends Error {}
|
|
70
105
|
|
|
71
|
-
const DEG = Math.PI / 180;
|
|
72
|
-
|
|
73
106
|
/**
|
|
74
107
|
* Crop pixels (y down, origin top-left) -> Spine world (y up, origin at the
|
|
75
108
|
* bottom-left of the crop).
|
|
@@ -85,6 +118,20 @@ export function cropToSpineY(cropY: number, cropHeight: number): number {
|
|
|
85
118
|
return cropHeight - cropY;
|
|
86
119
|
}
|
|
87
120
|
|
|
121
|
+
/**
|
|
122
|
+
* The textbook's arithmetic — `Math.PI`, a bone with the default scale and no
|
|
123
|
+
* shear framed as `[cos, −sin, sin, cos]`, radians to degrees by dividing by
|
|
124
|
+
* the degree factor — in which an unrotated bone's frame is the identity
|
|
125
|
+
* exactly. Every point the compiler binds goes through the runtime's instead
|
|
126
|
+
* (the header's ⚠️); this is what `computeExactFrameTransforms` evaluates
|
|
127
|
+
* under, and nothing else.
|
|
128
|
+
*/
|
|
129
|
+
const EXACT_FRAME_ARITHMETIC: WorldArithmetic = {
|
|
130
|
+
radiansPerDegree: Math.PI / 180,
|
|
131
|
+
degreesOf: (radians) => radians / (Math.PI / 180),
|
|
132
|
+
rotationOnlyFrame: true,
|
|
133
|
+
};
|
|
134
|
+
|
|
88
135
|
/**
|
|
89
136
|
* World transform of every bone, in declaration order.
|
|
90
137
|
*
|
|
@@ -92,69 +139,76 @@ export function cropToSpineY(cropY: number, cropHeight: number): number {
|
|
|
92
139
|
* requires (it resolves `parent` by name against the bones already read), so a
|
|
93
140
|
* violation here is a violation there.
|
|
94
141
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* runtime
|
|
142
|
+
* The matrices and origins are the core's (`worldTransforms`, under the
|
|
143
|
+
* runtime's arithmetic — the header's ⚠️). What is decided here is what the core does not say:
|
|
144
|
+
* an undeclared parent and an `inherit` no mode answers to are refused by name
|
|
145
|
+
* as `TransformError`, a bone whose `parent` is empty is a root as it always was
|
|
146
|
+
* here, and `worldRotation` is read off the matrix — a root with the default
|
|
147
|
+
* scale and no shear keeps its stated rotation, unwrapped. The angle is turned
|
|
148
|
+
* into degrees as the runtime turns one (`RUNTIME_DEG`, by multiplying), so
|
|
149
|
+
* the rotation a region writes to cancel it is read back by the runtime as
|
|
150
|
+
* that angle.
|
|
103
151
|
*/
|
|
104
152
|
export function computeWorldTransforms(bones: readonly PosableBone[]): Map<string, BoneTransform> {
|
|
105
|
-
|
|
153
|
+
return evaluate(bones, RUNTIME_ARITHMETIC);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The same bones under `EXACT_FRAME_ARITHMETIC`: the frame the compiler
|
|
158
|
+
* MEASURES the authored rig in, and binds nothing through (issue #1021) — a
|
|
159
|
+
* mesh's depth turn ceiling (`sampleMeshDepth` in `compile.ts`), a ring's
|
|
160
|
+
* control angles (`ringControlAngles`, which orders the split and refuses a
|
|
161
|
+
* tie by printing the angle), and a `segments` mesh's segments, the distances
|
|
162
|
+
* its falloff and `minWeight` read and its shared-origin refusal. It differs from
|
|
163
|
+
* `computeWorldTransforms` only in the last bits, and that is exactly why it is
|
|
164
|
+
* kept (issue #1021): the ceiling is a statement about the authored mesh, and
|
|
165
|
+
* its reader treats an axis whose area term is exactly zero as one that cannot
|
|
166
|
+
* fold. The runtime frames an unrotated bone with `b = −2.3e-8`, so in its
|
|
167
|
+
* bind space a sheet whose depth rises linearly in x — which cannot fold a
|
|
168
|
+
* pitch at any angle — reads a pitch ceiling of 89.99997°, and `gallery/look`'s
|
|
169
|
+
* two hair locks gained four such ceilings with their counts (`TC03`, `TC06`
|
|
170
|
+
* went red on it). A control angle read through the runtime's frame printed
|
|
171
|
+
* an authored 90 as 89.99999734089101 in its tie refusal (`RF46`). On the 19
|
|
172
|
+
* recipes the only bytes this keeps from moving are look's ceilings; no
|
|
173
|
+
* emitted position goes through it.
|
|
174
|
+
*/
|
|
175
|
+
export function computeExactFrameTransforms(bones: readonly PosableBone[]): Map<string, BoneTransform> {
|
|
176
|
+
return evaluate(bones, EXACT_FRAME_ARITHMETIC);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function evaluate(bones: readonly PosableBone[], arithmetic: WorldArithmetic): Map<string, BoneTransform> {
|
|
180
|
+
const model: ModelBone[] = [];
|
|
181
|
+
const declared = new Set<string>();
|
|
106
182
|
for (const bone of bones) {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const shearX = bone.shearX ?? 0;
|
|
111
|
-
const shearY = bone.shearY ?? 0;
|
|
112
|
-
const plain = scaleX === 1 && scaleY === 1 && shearX === 0 && shearY === 0;
|
|
113
|
-
let la: number;
|
|
114
|
-
let lb: number;
|
|
115
|
-
let lc: number;
|
|
116
|
-
let ld: number;
|
|
117
|
-
if (plain) {
|
|
118
|
-
const cos = Math.cos(rotation * DEG);
|
|
119
|
-
const sin = Math.sin(rotation * DEG);
|
|
120
|
-
la = cos;
|
|
121
|
-
lb = -sin;
|
|
122
|
-
lc = sin;
|
|
123
|
-
ld = cos;
|
|
124
|
-
} else {
|
|
125
|
-
const rx = (rotation + shearX) * DEG;
|
|
126
|
-
const ry = (rotation + 90 + shearY) * DEG;
|
|
127
|
-
la = Math.cos(rx) * scaleX;
|
|
128
|
-
lb = Math.cos(ry) * scaleY;
|
|
129
|
-
lc = Math.sin(rx) * scaleX;
|
|
130
|
-
ld = Math.sin(ry) * scaleY;
|
|
131
|
-
}
|
|
132
|
-
const x = bone.x ?? 0;
|
|
133
|
-
const y = bone.y ?? 0;
|
|
134
|
-
if (!bone.parent) {
|
|
135
|
-
const worldRotation = plain ? rotation : (Math.atan2(lc, la) / DEG + 360) % 360;
|
|
136
|
-
out.set(bone.name, { a: la, b: lb, c: lc, d: ld, worldX: x, worldY: y, worldRotation });
|
|
137
|
-
continue;
|
|
183
|
+
const parent = bone.parent ? bone.parent : undefined;
|
|
184
|
+
if (parent !== undefined && !declared.has(parent)) {
|
|
185
|
+
throw new TransformError(`bone "${bone.name}" names parent "${parent}", which is not declared before it`);
|
|
138
186
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
187
|
+
model.push({
|
|
188
|
+
name: bone.name,
|
|
189
|
+
parent,
|
|
190
|
+
x: bone.x,
|
|
191
|
+
y: bone.y,
|
|
192
|
+
rotation: bone.rotation,
|
|
193
|
+
scaleX: bone.scaleX,
|
|
194
|
+
scaleY: bone.scaleY,
|
|
195
|
+
shearX: bone.shearX,
|
|
196
|
+
shearY: bone.shearY,
|
|
197
|
+
inheritMode: parent === undefined ? undefined : inheritMode(bone),
|
|
150
198
|
});
|
|
199
|
+
declared.add(bone.name);
|
|
200
|
+
}
|
|
201
|
+
const worlds = worldTransforms(model, null, modeMatrix, arithmetic);
|
|
202
|
+
const out = new Map<string, BoneTransform>();
|
|
203
|
+
for (const bone of model) {
|
|
204
|
+
const w = worlds.get(bone.name) as CoreWorld;
|
|
205
|
+
const plain = (bone.scaleX ?? 1) === 1 && (bone.scaleY ?? 1) === 1 && (bone.shearX ?? 0) === 0 && (bone.shearY ?? 0) === 0;
|
|
206
|
+
const worldRotation = bone.parent === undefined && plain ? (bone.rotation ?? 0) : (arithmetic.degreesOf(Math.atan2(w.c, w.a)) + 360) % 360;
|
|
207
|
+
out.set(bone.name, { a: w.a, b: w.b, c: w.c, d: w.d, worldX: w.worldX, worldY: w.worldY, worldRotation });
|
|
151
208
|
}
|
|
152
209
|
return out;
|
|
153
210
|
}
|
|
154
211
|
|
|
155
|
-
/** `MathUtils.epsilon2` — the threshold `noRotationOrReflection` tests a degenerate parent against. */
|
|
156
|
-
const EPSILON2 = 0.00001 * 0.00001;
|
|
157
|
-
|
|
158
212
|
type InheritMode = 'normal' | 'onlyTranslation' | 'noRotationOrReflection' | 'noScale' | 'noScaleOrReflection';
|
|
159
213
|
|
|
160
214
|
const INHERIT_MODES: readonly InheritMode[] = ['normal', 'onlyTranslation', 'noRotationOrReflection', 'noScale', 'noScaleOrReflection'];
|
|
@@ -177,75 +231,6 @@ function inheritMode(bone: PosableBone): InheritMode {
|
|
|
177
231
|
return mode;
|
|
178
232
|
}
|
|
179
233
|
|
|
180
|
-
/**
|
|
181
|
-
* `BonePose.updateWorldTransform`'s switch (`BonePose.js:136-215`) for a bone
|
|
182
|
-
* with a parent, at a skeleton scale of 1 — so every `sx`/`sy` factor there is 1
|
|
183
|
-
* and is left out rather than multiplied in.
|
|
184
|
-
*/
|
|
185
|
-
function inheritedMatrix(
|
|
186
|
-
inherit: InheritMode,
|
|
187
|
-
p: BoneTransform,
|
|
188
|
-
local: [number, number, number, number],
|
|
189
|
-
rotation: number,
|
|
190
|
-
scaleX: number,
|
|
191
|
-
scaleY: number,
|
|
192
|
-
shearX: number,
|
|
193
|
-
shearY: number,
|
|
194
|
-
): [number, number, number, number] {
|
|
195
|
-
const [la, lb, lc, ld] = local;
|
|
196
|
-
if (inherit === 'normal') {
|
|
197
|
-
return [p.a * la + p.b * lc, p.a * lb + p.b * ld, p.c * la + p.d * lc, p.c * lb + p.d * ld];
|
|
198
|
-
}
|
|
199
|
-
if (inherit === 'onlyTranslation') return [la, lb, lc, ld];
|
|
200
|
-
if (inherit === 'noRotationOrReflection') {
|
|
201
|
-
let pa = p.a;
|
|
202
|
-
let pb = p.b;
|
|
203
|
-
let pc = p.c;
|
|
204
|
-
let pd = p.d;
|
|
205
|
-
let s = pa * pa + pc * pc;
|
|
206
|
-
let r: number;
|
|
207
|
-
if (s > EPSILON2) {
|
|
208
|
-
s = Math.abs(pa * pd - pb * pc) / s;
|
|
209
|
-
pb = pc * s;
|
|
210
|
-
pd = pa * s;
|
|
211
|
-
r = rotation - Math.atan2(pc, pa) / DEG;
|
|
212
|
-
} else {
|
|
213
|
-
pa = 0;
|
|
214
|
-
pc = 0;
|
|
215
|
-
r = rotation - 90 + Math.atan2(pd, pb) / DEG;
|
|
216
|
-
}
|
|
217
|
-
const rx = (r + shearX) * DEG;
|
|
218
|
-
const ry = (r + shearY + 90) * DEG;
|
|
219
|
-
const ra = Math.cos(rx) * scaleX;
|
|
220
|
-
const rb = Math.cos(ry) * scaleY;
|
|
221
|
-
const rc = Math.sin(rx) * scaleX;
|
|
222
|
-
const rd = Math.sin(ry) * scaleY;
|
|
223
|
-
return [pa * ra - pb * rc, pa * rb - pb * rd, pc * ra + pd * rc, pc * rb + pd * rd];
|
|
224
|
-
}
|
|
225
|
-
// noScale / noScaleOrReflection
|
|
226
|
-
const r = rotation * DEG;
|
|
227
|
-
const cos = Math.cos(r);
|
|
228
|
-
const sin = Math.sin(r);
|
|
229
|
-
let za = p.a * cos + p.b * sin;
|
|
230
|
-
let zc = p.c * cos + p.d * sin;
|
|
231
|
-
const s = 1 / Math.sqrt(za * za + zc * zc);
|
|
232
|
-
za *= s;
|
|
233
|
-
zc *= s;
|
|
234
|
-
let zb = -zc;
|
|
235
|
-
let zd = za;
|
|
236
|
-
if (inherit === 'noScale' && p.a * p.d - p.b * p.c < 0) {
|
|
237
|
-
zb = -zb;
|
|
238
|
-
zd = -zd;
|
|
239
|
-
}
|
|
240
|
-
const rx = shearX * DEG;
|
|
241
|
-
const ry = (90 + shearY) * DEG;
|
|
242
|
-
const ra = Math.cos(rx) * scaleX;
|
|
243
|
-
const rb = Math.cos(ry) * scaleY;
|
|
244
|
-
const rc = Math.sin(rx) * scaleX;
|
|
245
|
-
const rd = Math.sin(ry) * scaleY;
|
|
246
|
-
return [za * ra + zb * rc, za * rb + zb * rd, zc * ra + zd * rc, zc * rb + zd * rd];
|
|
247
|
-
}
|
|
248
|
-
|
|
249
234
|
/**
|
|
250
235
|
* World point -> the bone's local space. Used for bind coordinates and offsets.
|
|
251
236
|
*
|
package/src/types.ts
CHANGED
|
@@ -1098,11 +1098,12 @@ export interface SpineClippingAttachment {
|
|
|
1098
1098
|
* is re-parameterised by the trip rather than corrupted by it.
|
|
1099
1099
|
*
|
|
1100
1100
|
* ⇒ **rigc emits that computation, not a sampler aimed at it** (issue #560).
|
|
1101
|
-
* `pathCurveLengths` in [`compile.ts`](compile.ts)
|
|
1102
|
-
*
|
|
1103
|
-
*
|
|
1104
|
-
*
|
|
1105
|
-
*
|
|
1101
|
+
* `pathCurveLengths` in [`compile.ts`](compile.ts) measures with the core's
|
|
1102
|
+
* curve table (`curveLengthTable` in `src/core/constraints_path.ts`, issue
|
|
1103
|
+
* #1015), down to `Math.sqrt(dx * dx + dy * dy)` rather than `Math.hypot` and
|
|
1104
|
+
* `0.16666667` rather than `1 / 6`; `PS67`, `PS68` and `PS186` in `selftest.ts`
|
|
1105
|
+
* hold it there by requiring it to reproduce a real `PathConstraint.curves`
|
|
1106
|
+
* array **bit for bit** on the runtime's own posed chain. Measured after the change, all
|
|
1106
1107
|
* seven entries of both editor exports above come back at the precision the
|
|
1107
1108
|
* editor prints them.
|
|
1108
1109
|
*
|