spine-rigc 1.6.2 → 1.7.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 +82 -43
- package/docs/AUTHORING.md +9 -6
- package/package.json +1 -1
- package/src/atlas.ts +26 -16
- package/src/check.ts +37 -39
- package/src/compile.ts +59 -74
- package/src/core/constraints_path.ts +60 -38
- package/src/core/constraints_physics.ts +22 -4
- package/src/core/index.ts +95 -10
- package/src/core/uvs.ts +25 -5
- package/src/core/world.ts +79 -24
- package/src/deformmeasure.ts +6 -3
- package/src/emit.ts +17 -6
- package/src/ingest.ts +18 -13
- package/src/keyorder.ts +2 -2
- package/src/model.ts +109 -11
- package/src/render.ts +347 -34
- package/src/render_core.ts +115 -18
- package/src/transform.ts +62 -126
- package/src/types.ts +6 -5
- package/src/validate.ts +51 -29
package/src/render_core.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
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
|
*
|
|
@@ -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,12 +263,15 @@ 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
|
/**
|
|
@@ -300,12 +310,12 @@ function worldOf(pose: RawPose): Map<string, CoreWorld> {
|
|
|
300
310
|
}
|
|
301
311
|
|
|
302
312
|
/** `regionKey` in `src/render.ts`: the trimmed name and the sequence index — the key a substitution matches on. */
|
|
303
|
-
function regionKey(region:
|
|
313
|
+
function regionKey(region: TextureRegion): string {
|
|
304
314
|
return `${region.name.trim()}#${region.index}`;
|
|
305
315
|
}
|
|
306
316
|
|
|
307
317
|
/** `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:
|
|
318
|
+
function artUvsOf(drawn: RawDrawn, region: TextureRegion): PieceTexture | undefined {
|
|
309
319
|
if (drawn.kind === 'mesh') return { region: regionKey(region), artUvs: [...drawn.uvs] };
|
|
310
320
|
if (region.degrees !== 0) return undefined;
|
|
311
321
|
const ow = region.originalWidth;
|
|
@@ -515,9 +525,70 @@ function restOf(view: CompiledDocument, setup: RawPose, shown: readonly Attachme
|
|
|
515
525
|
}
|
|
516
526
|
|
|
517
527
|
/**
|
|
518
|
-
* `
|
|
519
|
-
*
|
|
520
|
-
*
|
|
528
|
+
* The first place two `pages` sections differ, by path, or `null` when they
|
|
529
|
+
* are the same — the page count, a page's name or size, a region count, or a
|
|
530
|
+
* region's name or one of its numbers.
|
|
531
|
+
*/
|
|
532
|
+
export function firstPageDifference(stated: readonly ModelPage[], found: readonly ModelPage[]): string | null {
|
|
533
|
+
if (stated.length !== found.length) return `the document states ${stated.length} page(s), the atlas has ${found.length}`;
|
|
534
|
+
for (let i = 0; i < stated.length; i++) {
|
|
535
|
+
const a = stated[i];
|
|
536
|
+
const b = found[i];
|
|
537
|
+
for (const key of ['name', 'width', 'height'] as const) {
|
|
538
|
+
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`;
|
|
539
|
+
}
|
|
540
|
+
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`;
|
|
541
|
+
for (let j = 0; j < a.regions.length; j++) {
|
|
542
|
+
const r = a.regions[j];
|
|
543
|
+
const q = b.regions[j];
|
|
544
|
+
for (const key of Object.keys(r) as Array<keyof typeof r>) {
|
|
545
|
+
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`;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Where each region a core pose draws sits on its page, read from the
|
|
554
|
+
* document's `pages` section (issue #1016) — or, for a `rigc-compiled/1`
|
|
555
|
+
* document, which has none, from `atlasText` as before. With both, the atlas
|
|
556
|
+
* must be the one the document was written beside: a page or region that
|
|
557
|
+
* differs is refused naming the first difference, so the core never draws the
|
|
558
|
+
* build's placement over another atlas's pages while spine-core, reading that
|
|
559
|
+
* atlas, would draw another picture.
|
|
560
|
+
*/
|
|
561
|
+
function placementOf(doc: CompiledDocument, atlasText: string, where: string): { lookup: UvSource['lookup']; region: CoreInput['region'] } {
|
|
562
|
+
if (doc.pages !== null) {
|
|
563
|
+
if (atlasText !== '') {
|
|
564
|
+
const differs = firstPageDifference(doc.pages, pagesOfAtlas(atlasText));
|
|
565
|
+
if (differs !== null) {
|
|
566
|
+
throw new CoreInputError(
|
|
567
|
+
`the atlas beside ${where} is not the one it was written beside: ${differs} — ` +
|
|
568
|
+
'the core would draw the build\'s placement over pages the atlas has rearranged',
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const lookup = documentPageLookup(doc.pages);
|
|
573
|
+
return { lookup, region: (name) => lookup(name)?.region ?? null };
|
|
574
|
+
}
|
|
575
|
+
if (atlasText === '') {
|
|
576
|
+
throw new CoreInputError(
|
|
577
|
+
`${where} is a ${doc.spec} document, which does not state where each region sits on its page (the pages section, issue #1016), ` +
|
|
578
|
+
'and no atlas was given to read it from — rebuild it to carry them, or pose it beside the atlas it was built with',
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
const lookup = atlasRegionLookup(parseAtlasText(atlasText));
|
|
582
|
+
return { lookup, region: (name) => lookup(name)?.region ?? null };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* `Poser` over rigc's own core: `modelText` a `rigc-compiled/2` document
|
|
587
|
+
* (`skeleton.model.json`), which states where each region sits on its page
|
|
588
|
+
* (`pages`, issue #1016), so `atlasText` may be `''`; given, it is held to
|
|
589
|
+
* the document's `pages` (`placementOf`). A `rigc-compiled/1` document states
|
|
590
|
+
* no placement and is drawn through `atlasText`, the atlas `build` wrote
|
|
591
|
+
* beside it, as before; with none given it is refused by name. With `skeleton`, the Spine file beside the document
|
|
521
592
|
* is held to the digest the document records (`spine.sha256`, issue #968) and
|
|
522
593
|
* refused, naming both digests, when it is not that build's. Refused by
|
|
523
594
|
* `CoreInputError`, naming why, where the document or the atlas cannot be read; a pose the core leaves a block of out
|
|
@@ -542,8 +613,7 @@ export function corePoser(modelText: string, atlasText: string, where = 'skeleto
|
|
|
542
613
|
throw new CoreInputError(`${where}: not JSON — ${(err as Error).message}`);
|
|
543
614
|
}
|
|
544
615
|
const sequences = readUvSequences(parsed);
|
|
545
|
-
const
|
|
546
|
-
const lookup = atlasRegionLookup(atlas);
|
|
616
|
+
const { lookup, region } = placementOf(doc, atlasText, where);
|
|
547
617
|
const views = new Map<string, CompiledDocument>();
|
|
548
618
|
const inputOf = (skin: string | undefined): CoreInput => {
|
|
549
619
|
const key = skin === undefined ? '' : `=${skin}`;
|
|
@@ -552,7 +622,7 @@ export function corePoser(modelText: string, atlasText: string, where = 'skeleto
|
|
|
552
622
|
view = skin === undefined ? noSkinView(doc) : underSkin(doc, skin);
|
|
553
623
|
views.set(key, view);
|
|
554
624
|
}
|
|
555
|
-
return { doc: view, source: { lookup, sequences }, region
|
|
625
|
+
return { doc: view, source: { lookup, sequences }, region };
|
|
556
626
|
};
|
|
557
627
|
const roster: SubsetRoster = {
|
|
558
628
|
declared: doc.slots.map((s) => s.name),
|
|
@@ -580,3 +650,30 @@ export function corePoser(modelText: string, atlasText: string, where = 'skeleto
|
|
|
580
650
|
},
|
|
581
651
|
};
|
|
582
652
|
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* The skin roster behind the core poser (`SkinRoster` in `src/render.ts`,
|
|
656
|
+
* issue #1014): the bones each skin leaves unposed, read off the model
|
|
657
|
+
* document — `unposedBones` over `activeBones` of the skin's view, the
|
|
658
|
+
* predicate the raw pose flags its bones with, and under no skin the view the
|
|
659
|
+
* core poses with no skin set (`noSkinView`, refused by `CoreInputError` where
|
|
660
|
+
* it refuses to pose). `skins` is the Spine file's own list, in its order, which
|
|
661
|
+
* the document does not hold (`resolveSkinView`'s note in `./core/index.ts`).
|
|
662
|
+
*
|
|
663
|
+
* Measured against the runtime's reading (`skinRosterOf`, a fresh skeleton's
|
|
664
|
+
* `active` under each skin and under none) on every rigc build the tree
|
|
665
|
+
* carries: the same bones, under every skin. The document is read on the first
|
|
666
|
+
* question, so a render that frames nothing under a skin never parses it twice.
|
|
667
|
+
*/
|
|
668
|
+
export function coreSkinRoster(modelText: string, where: string, skins: readonly string[]): SkinRoster {
|
|
669
|
+
let doc: CompiledDocument | null = null;
|
|
670
|
+
return {
|
|
671
|
+
skins,
|
|
672
|
+
unposedUnder: (skin) => {
|
|
673
|
+
doc ??= readModel(modelText, where);
|
|
674
|
+
const view = skin === undefined ? noSkinView(doc) : underSkin(doc, skin);
|
|
675
|
+
const active = activeBones(view);
|
|
676
|
+
return unposedBones(view.bones.map((b) => ({ name: b.name, parent: b.parent ?? null, active: active.has(b.name) })));
|
|
677
|
+
},
|
|
678
|
+
};
|
|
679
|
+
}
|
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,19 @@
|
|
|
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. This file
|
|
37
|
+
* hands it the compiler's arithmetic (`COMPILER_ARITHMETIC`, below) and keeps
|
|
38
|
+
* what is the compiler's own: the coordinate contract, the refusals by name, the
|
|
39
|
+
* world rotation a region cancels, and the inverses.
|
|
33
40
|
*/
|
|
41
|
+
import type { ModelBone } from './model.ts';
|
|
42
|
+
import { modeMatrix, worldTransforms, type CoreWorld, type WorldArithmetic } from './core/world.ts';
|
|
43
|
+
|
|
34
44
|
/**
|
|
35
45
|
* What `computeWorldTransforms` reads off a bone — a structural type, so both a
|
|
36
46
|
* Spine bone (`SpineBone`, what spine-parts and every skeleton reader hold) and
|
|
@@ -70,6 +80,24 @@ export class TransformError extends Error {}
|
|
|
70
80
|
|
|
71
81
|
const DEG = Math.PI / 180;
|
|
72
82
|
|
|
83
|
+
/**
|
|
84
|
+
* The arithmetic every figure rigc emits was computed with, handed to the core's
|
|
85
|
+
* evaluator (issue #1015). It differs from the runtime's in the last bit and in
|
|
86
|
+
* no matrix: degrees to radians with `Math.PI` rather than the runtime's
|
|
87
|
+
* 3.1415927, radians back to degrees by dividing by that factor, and a bone with
|
|
88
|
+
* the default scale and no shear framed as `lb = -sin`, `ld = cos` rather than
|
|
89
|
+
* `cos`/`sin` of `rot + 90°`. One bit can move a float32 spelling in the file, so
|
|
90
|
+
* this is a parameter and not a rewrite: measured with the runtime's arithmetic
|
|
91
|
+
* instead, 2 of the 19 public recipes moved by at most 4e-5. Whether the compiler
|
|
92
|
+
* should bind with the runtime's constant is a question of its own, not settled
|
|
93
|
+
* here.
|
|
94
|
+
*/
|
|
95
|
+
const COMPILER_ARITHMETIC: WorldArithmetic = {
|
|
96
|
+
radiansPerDegree: DEG,
|
|
97
|
+
degreesOf: (radians) => radians / DEG,
|
|
98
|
+
rotationOnlyFrame: true,
|
|
99
|
+
};
|
|
100
|
+
|
|
73
101
|
/**
|
|
74
102
|
* Crop pixels (y down, origin top-left) -> Spine world (y up, origin at the
|
|
75
103
|
* bottom-left of the crop).
|
|
@@ -92,69 +120,46 @@ export function cropToSpineY(cropY: number, cropHeight: number): number {
|
|
|
92
120
|
* requires (it resolves `parent` by name against the bones already read), so a
|
|
93
121
|
* violation here is a violation there.
|
|
94
122
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* inheriting rig emits moves by a byte. The five `inherit` cases are the
|
|
102
|
-
* runtime's, transcribed.
|
|
123
|
+
* The matrices and origins are the core's (`worldTransforms`, under
|
|
124
|
+
* `COMPILER_ARITHMETIC`). What is decided here is what the core does not say:
|
|
125
|
+
* an undeclared parent and an `inherit` no mode answers to are refused by name
|
|
126
|
+
* as `TransformError`, a bone whose `parent` is empty is a root as it always was
|
|
127
|
+
* here, and `worldRotation` is read off the matrix — a root with the default
|
|
128
|
+
* scale and no shear keeps its stated rotation, unwrapped.
|
|
103
129
|
*/
|
|
104
130
|
export function computeWorldTransforms(bones: readonly PosableBone[]): Map<string, BoneTransform> {
|
|
105
|
-
const
|
|
131
|
+
const model: ModelBone[] = [];
|
|
132
|
+
const declared = new Set<string>();
|
|
106
133
|
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;
|
|
134
|
+
const parent = bone.parent ? bone.parent : undefined;
|
|
135
|
+
if (parent !== undefined && !declared.has(parent)) {
|
|
136
|
+
throw new TransformError(`bone "${bone.name}" names parent "${parent}", which is not declared before it`);
|
|
138
137
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
138
|
+
model.push({
|
|
139
|
+
name: bone.name,
|
|
140
|
+
parent,
|
|
141
|
+
x: bone.x,
|
|
142
|
+
y: bone.y,
|
|
143
|
+
rotation: bone.rotation,
|
|
144
|
+
scaleX: bone.scaleX,
|
|
145
|
+
scaleY: bone.scaleY,
|
|
146
|
+
shearX: bone.shearX,
|
|
147
|
+
shearY: bone.shearY,
|
|
148
|
+
inheritMode: parent === undefined ? undefined : inheritMode(bone),
|
|
150
149
|
});
|
|
150
|
+
declared.add(bone.name);
|
|
151
|
+
}
|
|
152
|
+
const worlds = worldTransforms(model, null, modeMatrix, COMPILER_ARITHMETIC);
|
|
153
|
+
const out = new Map<string, BoneTransform>();
|
|
154
|
+
for (const bone of model) {
|
|
155
|
+
const w = worlds.get(bone.name) as CoreWorld;
|
|
156
|
+
const plain = (bone.scaleX ?? 1) === 1 && (bone.scaleY ?? 1) === 1 && (bone.shearX ?? 0) === 0 && (bone.shearY ?? 0) === 0;
|
|
157
|
+
const worldRotation = bone.parent === undefined && plain ? (bone.rotation ?? 0) : (Math.atan2(w.c, w.a) / DEG + 360) % 360;
|
|
158
|
+
out.set(bone.name, { a: w.a, b: w.b, c: w.c, d: w.d, worldX: w.worldX, worldY: w.worldY, worldRotation });
|
|
151
159
|
}
|
|
152
160
|
return out;
|
|
153
161
|
}
|
|
154
162
|
|
|
155
|
-
/** `MathUtils.epsilon2` — the threshold `noRotationOrReflection` tests a degenerate parent against. */
|
|
156
|
-
const EPSILON2 = 0.00001 * 0.00001;
|
|
157
|
-
|
|
158
163
|
type InheritMode = 'normal' | 'onlyTranslation' | 'noRotationOrReflection' | 'noScale' | 'noScaleOrReflection';
|
|
159
164
|
|
|
160
165
|
const INHERIT_MODES: readonly InheritMode[] = ['normal', 'onlyTranslation', 'noRotationOrReflection', 'noScale', 'noScaleOrReflection'];
|
|
@@ -177,75 +182,6 @@ function inheritMode(bone: PosableBone): InheritMode {
|
|
|
177
182
|
return mode;
|
|
178
183
|
}
|
|
179
184
|
|
|
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
185
|
/**
|
|
250
186
|
* World point -> the bone's local space. Used for bind coordinates and offsets.
|
|
251
187
|
*
|
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
|
*
|
package/src/validate.ts
CHANGED
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
// rectangle on a shared page.
|
|
66
66
|
import { readPlate } from '../tools/plate.ts';
|
|
67
67
|
import { pageFootprint, pageGridSentence } from './atlas.ts';
|
|
68
|
+
import { frameRegionName } from './core/uvs.ts';
|
|
68
69
|
import {
|
|
69
70
|
BEZIER_POINTS,
|
|
70
71
|
curveStorage,
|
|
@@ -613,6 +614,9 @@ function setupBasisSays(rule: PhysicsPoseRule, poseValue: number): string {
|
|
|
613
614
|
return arm === undefined ? physicsOutsideSays(rule, poseValue) : physicsBasisSays(arm);
|
|
614
615
|
}
|
|
615
616
|
|
|
617
|
+
/** `physicsTimelineNames`' table, once it has been built. */
|
|
618
|
+
let physicsTimelineNamesTable: Record<number, string> | null = null;
|
|
619
|
+
|
|
616
620
|
/**
|
|
617
621
|
* The skeleton-JSON name of each physics timeline, keyed by the runtime's own
|
|
618
622
|
* `Property` id.
|
|
@@ -624,16 +628,25 @@ function setupBasisSays(rule: PhysicsPoseRule, poseValue: number): string {
|
|
|
624
628
|
* on the right are the ones `SkeletonJson`'s physics branch reads
|
|
625
629
|
* (`SkeletonJson.js:1063-1094`), which is also what a motion spec's `property`
|
|
626
630
|
* says.
|
|
631
|
+
*
|
|
632
|
+
* ⚠️ Built on first use, not while the module loads (issue #1014): the
|
|
633
|
+
* computed keys read the runtime's `Property` enum, and seven reads of it at
|
|
634
|
+
* load time were the only spine-core access a module made before a command
|
|
635
|
+
* ran — so `--help`, `render` and `check` on a rigc build all touched the
|
|
636
|
+
* runtime for a table only the gate's physics rules consult.
|
|
627
637
|
*/
|
|
628
|
-
export
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
638
|
+
export function physicsTimelineNames(): Record<number, string> {
|
|
639
|
+
physicsTimelineNamesTable ??= {
|
|
640
|
+
[Property.physicsConstraintInertia]: 'inertia',
|
|
641
|
+
[Property.physicsConstraintStrength]: 'strength',
|
|
642
|
+
[Property.physicsConstraintDamping]: 'damping',
|
|
643
|
+
[Property.physicsConstraintMass]: 'mass',
|
|
644
|
+
[Property.physicsConstraintWind]: 'wind',
|
|
645
|
+
[Property.physicsConstraintGravity]: 'gravity',
|
|
646
|
+
[Property.physicsConstraintMix]: 'mix',
|
|
647
|
+
};
|
|
648
|
+
return physicsTimelineNamesTable;
|
|
649
|
+
}
|
|
637
650
|
|
|
638
651
|
/**
|
|
639
652
|
* Which physics constraints a physics timeline that names NO constraint writes
|
|
@@ -733,20 +746,23 @@ function atStoredKey(time: number): number {
|
|
|
733
746
|
* The atlas region names one raw skin entry will make the loader look up — or
|
|
734
747
|
* `null` when the file states a sequence this walk cannot predict.
|
|
735
748
|
*
|
|
736
|
-
* ⚠️
|
|
749
|
+
* ⚠️ Measured against the loader rather than reasoned out, because a join that
|
|
737
750
|
* merely looks right is the thing A08 exists to refuse. `readSequence`
|
|
738
751
|
* (`dist/SkeletonJson.js:641-649`) turns an absent or null `sequence` into
|
|
739
752
|
* `new Sequence(1, false)` — one lookup, at the bare path — and a present one
|
|
740
753
|
* into `new Sequence(count ?? 0, true)`, so a sequence map with no `count`
|
|
741
|
-
* looks up nothing at all.
|
|
742
|
-
*
|
|
754
|
+
* looks up nothing at all. Each frame's name is the core's `frameRegionName`
|
|
755
|
+
* (`src/core/uvs.ts`): `start + i`, left-padded with zeros to `digits`, after
|
|
756
|
+
* the path (issue #1015). `PS127` records what the loader asks for and compares,
|
|
757
|
+
* and the core suite holds the frame names to `Sequence.getPath` over 7,680
|
|
758
|
+
* cases.
|
|
743
759
|
*
|
|
744
760
|
* Nothing in `examples/` carries a `sequence` (measured: 0 occurrences across
|
|
745
761
|
* all twelve editor exports), so without this the assertion would have read a
|
|
746
762
|
* sequence's base path as a region name and refused correct foreign data —
|
|
747
763
|
* `A21_MESH_RIM_PINNED`'s old `|| 'ring'` default, one file over.
|
|
748
764
|
*/
|
|
749
|
-
function attachmentRegionLookups(sequence: unknown, path: string): string[] | null {
|
|
765
|
+
export function attachmentRegionLookups(sequence: unknown, path: string): string[] | null {
|
|
750
766
|
if (sequence === undefined || sequence === null) return [path];
|
|
751
767
|
if (!isObj(sequence)) return null;
|
|
752
768
|
const whole = (value: unknown, fallback: number): number | null => {
|
|
@@ -757,11 +773,9 @@ function attachmentRegionLookups(sequence: unknown, path: string): string[] | nu
|
|
|
757
773
|
const start = whole(sequence.start, 1);
|
|
758
774
|
const digits = whole(sequence.digits, 0);
|
|
759
775
|
if (count === null || start === null || digits === null || count < 0) return null;
|
|
776
|
+
const series = { count, start, digits, setup: 0 };
|
|
760
777
|
const lookups: string[] = [];
|
|
761
|
-
for (let i = 0; i < count; i++)
|
|
762
|
-
const frame = String(start + i);
|
|
763
|
-
lookups.push(`${path}${'0'.repeat(Math.max(0, digits - frame.length))}${frame}`);
|
|
764
|
-
}
|
|
778
|
+
for (let i = 0; i < count; i++) lookups.push(frameRegionName(path, series, i));
|
|
765
779
|
return lookups;
|
|
766
780
|
}
|
|
767
781
|
|
|
@@ -870,11 +884,11 @@ interface RawLinkedMesh {
|
|
|
870
884
|
* `"<skin>\0<slot>\0<placeholder>" -> source` for every linked mesh the raw
|
|
871
885
|
* skeleton declares, with what the file says about each one (issues #691, #710).
|
|
872
886
|
*
|
|
873
|
-
* 🔑 The
|
|
874
|
-
* `type: "linkedmesh"` share one branch and a truthy `source` is
|
|
875
|
-
* between them (`SkeletonJson.js:568-569`, `:582`). An empty
|
|
876
|
-
* there, so it is not a link here either — that map is read
|
|
877
|
-
* which is exactly what the runtime does with it.
|
|
887
|
+
* 🔑 The format decides it, and not by `type`: `type: "mesh"` and
|
|
888
|
+
* `type: "linkedmesh"` share one branch of the reader and a truthy `source` is
|
|
889
|
+
* what decides between them (`SkeletonJson.js:568-569`, `:582`). An empty
|
|
890
|
+
* `source` is falsy there, so it is not a link here either — that map is read
|
|
891
|
+
* as an ordinary mesh, which is exactly what the runtime does with it.
|
|
878
892
|
*/
|
|
879
893
|
function rawLinkedMeshes(raw: unknown): Map<string, RawLinkedMesh> {
|
|
880
894
|
const links = new Map<string, RawLinkedMesh>();
|
|
@@ -3305,7 +3319,7 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
3305
3319
|
const reached = keyedLive(
|
|
3306
3320
|
(timeline): timeline is PhysicsConstraintTimeline =>
|
|
3307
3321
|
timeline instanceof PhysicsConstraintTimeline &&
|
|
3308
|
-
|
|
3322
|
+
physicsTimelineNames()[Number(timeline.getPropertyIds()[0].split('|')[0])] === rule.timeline,
|
|
3309
3323
|
(timeline, value) => {
|
|
3310
3324
|
timeline.set(raised, value);
|
|
3311
3325
|
return rule.poseOk(raised[rule.field]);
|
|
@@ -3388,7 +3402,7 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
3388
3402
|
// `ConstraintTimeline1` encodes its propertyId as `<Property>|<index>`,
|
|
3389
3403
|
// so the name comes from the runtime's own enum rather than from a
|
|
3390
3404
|
// table of strings this file would have to keep in step.
|
|
3391
|
-
const name =
|
|
3405
|
+
const name = physicsTimelineNames()[Number(timeline.getPropertyIds()[0].split('|')[0])];
|
|
3392
3406
|
if (name === undefined) continue;
|
|
3393
3407
|
const rule = physicsRuleFor(name);
|
|
3394
3408
|
// -1 is the global form: the timeline drives every physics constraint
|
|
@@ -5029,9 +5043,9 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
5029
5043
|
// reads each of those off the FILE and names the value. The second POSES
|
|
5030
5044
|
// every key at sampled times — mid-frame, so a float32 key time cannot land
|
|
5031
5045
|
// a sample on a frame boundary — and compares the region the slot shows
|
|
5032
|
-
// against the one the file's own statement gives: the frame
|
|
5033
|
-
// `
|
|
5034
|
-
// `
|
|
5046
|
+
// against the one the file's own statement gives: the frame the key's mode,
|
|
5047
|
+
// index and delay give (`frameOf` below), and the frame names
|
|
5048
|
+
// `attachmentRegionLookups` derives (the core's `frameRegionName`). Neither is
|
|
5035
5049
|
// read off the loaded timeline, so the check is not the runtime agreeing
|
|
5036
5050
|
// with itself.
|
|
5037
5051
|
//
|
|
@@ -5099,7 +5113,15 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
5099
5113
|
}
|
|
5100
5114
|
}
|
|
5101
5115
|
|
|
5102
|
-
/**
|
|
5116
|
+
/**
|
|
5117
|
+
* The frame the file's statement gives for one key at one elapsed time —
|
|
5118
|
+
* A46's prediction, not a call into the runtime. What holds it is the
|
|
5119
|
+
* comparison it feeds: every sample sets it against the frame the posed
|
|
5120
|
+
* slot shows, so a reading that disagreed with the runtime would fail A46
|
|
5121
|
+
* on a correct file. A46 passing with samples posed on the selftest's
|
|
5122
|
+
* series probes is that measurement for the modes they key; `M73` is its
|
|
5123
|
+
* red half.
|
|
5124
|
+
*/
|
|
5103
5125
|
const frameOf = (mode: string, index: number, elapsed: number, delay: number, count: number): number => {
|
|
5104
5126
|
if (mode === 'hold') return index;
|
|
5105
5127
|
let i = index + Math.trunc(elapsed / delay + 0.00001);
|
|
@@ -5436,7 +5458,7 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
5436
5458
|
// is about rigc's own packer never turning a region, which is a statement
|
|
5437
5459
|
// about what rigc WRITES. It stopped being a statement about what rigc can
|
|
5438
5460
|
// read in issue #570 — `extractRegion` now lifts a rotated region back off
|
|
5439
|
-
// its page
|
|
5461
|
+
// its page, measured against `MeshAttachment.computeUVs` (`PKR02`) — and the two
|
|
5440
5462
|
// must not be re-merged: an artifact under the renderer's own rulebook that
|
|
5441
5463
|
// rigc did not pack is still a foreign artifact, whatever rigc can measure.
|
|
5442
5464
|
const regionsPerPage = new Map<string, TextureAtlasRegion[]>();
|