spine-rigc 1.7.0 → 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 +87 -20
- package/docs/AUTHORING.md +18 -12
- 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/check.ts +33 -8
- package/src/compile.ts +59 -19
- package/src/core/index.ts +7 -5
- package/src/core/world.ts +21 -20
- package/src/deformmeasure.ts +377 -154
- package/src/deformreport.ts +3 -0
- package/src/deformstructure.ts +335 -0
- package/src/model.ts +5 -2
- package/src/render.ts +195 -47
- package/src/render_core.ts +68 -8
- package/src/transform.ts +79 -30
- package/src/validate.ts +149 -461
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `SlotColourFacts` (issue #1025, step 4c of #380).
|
|
3
|
+
*
|
|
4
|
+
* - **The slot timelines, in the file's order and spelling.** The document
|
|
5
|
+
* holds each animation's slot timelines in the order they are applied, with
|
|
6
|
+
* every key as the compiler built it; the Spine file keys the animations in
|
|
7
|
+
* the editor's order and passes every key through two passes. So the
|
|
8
|
+
* animations are put in order by `editorAnimationOrder` (`src/compile.ts`,
|
|
9
|
+
* the function the emitter is handed), and the timelines are laid out as the
|
|
10
|
+
* file lays them and handed to the emitter's own `withoutParserDefaults` and
|
|
11
|
+
* `inEditorKeyOrder` (`src/keyorder.ts`) — so a key field the emitter would
|
|
12
|
+
* leave out at the parser's default is left out here too, and a body
|
|
13
|
+
* printing `JSON.stringify(key.value)` prints what it prints over the file.
|
|
14
|
+
* Nothing here restates either rule.
|
|
15
|
+
* - **Whether an animation exists**: the document's own roster.
|
|
16
|
+
* - **A slot's posed colour**: the core's raw entry (`poseRawAnimation`,
|
|
17
|
+
* `src/core/raw.ts`) taking one step of the time on a fresh non-looping
|
|
18
|
+
* track, under the view the core poses with no skin set (`noSkinView`,
|
|
19
|
+
* `src/render_core.ts`) — the reading of what `validate()` does through
|
|
20
|
+
* spine-core (a fresh skeleton, no skin, setup pose, the track stepped to the
|
|
21
|
+
* time and applied). Its slot rows are the ones `CR03` holds bit for bit to
|
|
22
|
+
* spine-core on every frame of the nineteen recipes; that the two readings
|
|
23
|
+
* print the same A45 line is the selftest's supplier check, on every call
|
|
24
|
+
* that poses one (`VF02`). A channel the pose read as no number is NaN, as a
|
|
25
|
+
* non-finite channel reads on the runtime's side.
|
|
26
|
+
*
|
|
27
|
+
* Links nothing from the runtime.
|
|
28
|
+
*/
|
|
29
|
+
import { editorAnimationOrder } from '../../compile.ts';
|
|
30
|
+
import type { CompiledDocument } from '../../core/index.ts';
|
|
31
|
+
import { poseRawAnimation } from '../../core/raw.ts';
|
|
32
|
+
import { inEditorKeyOrder, withoutParserDefaults } from '../../keyorder.ts';
|
|
33
|
+
import { noSkinView } from '../../render_core.ts';
|
|
34
|
+
import type { SlotColourFacts, SlotTimelines } from '../facts/slot_colour.ts';
|
|
35
|
+
import { isObj, type Json } from '../values.ts';
|
|
36
|
+
import type { ReadDocument } from './parse.ts';
|
|
37
|
+
|
|
38
|
+
const list = (v: unknown): Json[] => (Array.isArray(v) ? v.filter(isObj) : []);
|
|
39
|
+
|
|
40
|
+
/** Every animation's slot timelines as the Spine file states them: `animations.<name>.slots.<slot>.<timeline> = keys`. */
|
|
41
|
+
export function fileSlotTimelines(read: ReadDocument): SlotTimelines[] {
|
|
42
|
+
const byName = new Map<string, Json>();
|
|
43
|
+
for (const anim of list(read.json.animations)) if (typeof anim.name === 'string') byName.set(anim.name, anim);
|
|
44
|
+
const animations: Json = {};
|
|
45
|
+
for (const name of editorAnimationOrder([...byName.keys()])) {
|
|
46
|
+
const slots: Json = {};
|
|
47
|
+
for (const slot of list((byName.get(name) as Json).slots)) {
|
|
48
|
+
if (typeof slot.name !== 'string') continue;
|
|
49
|
+
const timelines: Json = {};
|
|
50
|
+
for (const timeline of list(slot.timelines)) {
|
|
51
|
+
if (typeof timeline.name === 'string') timelines[timeline.name] = list(timeline.keys).map((key) => ({ ...key }));
|
|
52
|
+
}
|
|
53
|
+
slots[slot.name] = timelines;
|
|
54
|
+
}
|
|
55
|
+
// An animation keying no slot writes no `slots` group — `emitAnimation`'s `if (animation.slots.size)`.
|
|
56
|
+
animations[name] = Object.keys(slots).length > 0 ? { slots } : {};
|
|
57
|
+
}
|
|
58
|
+
const file = inEditorKeyOrder(withoutParserDefaults({ animations }));
|
|
59
|
+
const out: SlotTimelines[] = [];
|
|
60
|
+
for (const [animation, anim] of Object.entries(file.animations)) {
|
|
61
|
+
if (!isObj(anim) || !isObj(anim.slots)) continue;
|
|
62
|
+
for (const [slot, timelines] of Object.entries(anim.slots)) if (isObj(timelines)) out.push({ animation, slot, timelines });
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function modelSlotColour(read: ReadDocument): SlotColourFacts {
|
|
68
|
+
let view: CompiledDocument | null = null;
|
|
69
|
+
return {
|
|
70
|
+
slotTimelines: fileSlotTimelines(read),
|
|
71
|
+
hasAnimation: (name) => read.doc.animations.some((a) => a.name === name),
|
|
72
|
+
posedSlot: (animation, slot, time) => {
|
|
73
|
+
view ??= noSkinView(read.doc);
|
|
74
|
+
const row = poseRawAnimation(view, animation, [time], {}, 'animation')[1].slots.find((r) => r[0] === slot);
|
|
75
|
+
if (row === undefined) return undefined;
|
|
76
|
+
const channel = (v: number | null): number => (v === null ? Number.NaN : v);
|
|
77
|
+
return { color: { r: channel(row[2]), g: channel(row[3]), b: channel(row[4]), a: channel(row[5]) } };
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a SKIP says, by what the assertion was denied (issues #568, #580, #608).
|
|
3
|
+
*
|
|
4
|
+
* Moved here from `src/validate.ts` by issue #1025 (step 4c of #380), words
|
|
5
|
+
* unchanged: an assertion body now lives in a module that links nothing from
|
|
6
|
+
* the runtime (`./bodies/`), and the bodies print these sentences on both
|
|
7
|
+
* sides — over what spine-core loaded and over the model document. A sentence
|
|
8
|
+
* kept in the file that links spine-core could not be read by the second.
|
|
9
|
+
* `src/validate.ts` re-exports every one, so nothing that imported them from
|
|
10
|
+
* there moves.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* What a SKIP says when `A00_ROUNDTRIP_PARSE` handed an assertion nothing to
|
|
15
|
+
* look at.
|
|
16
|
+
*
|
|
17
|
+
* Two of them rather than one, because what an assertion was denied is the whole
|
|
18
|
+
* content of its SKIP: `A20` wanted the loaded skeleton and `A06` wanted the
|
|
19
|
+
* loaded atlas, and a reader who is told which can tell a rule that is waiting
|
|
20
|
+
* on the parse from one that is waiting on the art. Both point at A00 instead of
|
|
21
|
+
* restating its detail, which the FAIL row prints in full.
|
|
22
|
+
*
|
|
23
|
+
* Exported so a control compares against them rather than quoting them — the
|
|
24
|
+
* same reason `ASSERTION_NAMES` is exported.
|
|
25
|
+
*/
|
|
26
|
+
export const SKIP_NO_SKELETON = 'the round trip did not produce a skeleton to measure (A00 owns that failure)';
|
|
27
|
+
export const SKIP_NO_ATLAS = 'the round trip did not produce an atlas to measure (A00 owns that failure)';
|
|
28
|
+
/**
|
|
29
|
+
* The third of them, and it is NOT waiting on A00 (issue #608): the atlas parsed
|
|
30
|
+
* and it has no pages, which is what a compile that measured no art writes. The
|
|
31
|
+
* four rules whose only subject is a page — A06, A17, A19, A27 — have nothing to
|
|
32
|
+
* look at, and an empty loop walking out of `check()` is reported as a PASS.
|
|
33
|
+
*/
|
|
34
|
+
/*
|
|
35
|
+
* **An empty subject list: SKIP or PASS, and which one is derived (issue #580).**
|
|
36
|
+
*
|
|
37
|
+
* `check()` records a pass when a body runs to its end without a `fail()` or a
|
|
38
|
+
* `skip()`, so a body whose main construct is a loop over a list that is EMPTY
|
|
39
|
+
* passes having measured nothing — the vacuous green one ring out from the bare
|
|
40
|
+
* `return` guards issue #568 converted. Two different things can be true of such
|
|
41
|
+
* a loop, and the criterion decides between them by reading the rule's own name
|
|
42
|
+
* and its `fail()` sentences rather than by preference:
|
|
43
|
+
*
|
|
44
|
+
* * A name of the form ⟨subject⟩_⟨property⟩ — `REGION_WIDTH_HEIGHT_FINITE`,
|
|
45
|
+
* `MESH_TRIANGLES_AND_ENCODING`, `PHYSICS_CONSTRAINT_EFFECTIVE`,
|
|
46
|
+
* `ATLAS_PAGE_SIZE_MATCHES_PNG` — quantifies over the subject it names, and
|
|
47
|
+
* its `fail()` names a member of that subject, the value found and the value
|
|
48
|
+
* required. With no member, nothing was measured: it **SKIPs**, and the
|
|
49
|
+
* reason names the subject that was absent.
|
|
50
|
+
* * A name of the form NO_⟨construct⟩ — `NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS`,
|
|
51
|
+
* `NO_BONE_TRANSFORM_KEY`, `NO_CLIPPING_ATTACHMENTS`, `NO_DARK_COLOR`,
|
|
52
|
+
* `NO_FULL_FRAME_MESH` — quantifies over occurrences of something a correct
|
|
53
|
+
* artifact has NONE of, and its `fail()` reports that the construct is
|
|
54
|
+
* present rather than measuring a value on it. Zero occurrences IS the
|
|
55
|
+
* measurement, so it **PASSes**: the loop is a search of the artifact, and
|
|
56
|
+
* the artifact is what it measured.
|
|
57
|
+
* * A name carrying both halves takes each at its word.
|
|
58
|
+
* `A15_IDLE_NO_MESH_BONE_KEYS` is the pattern and already reads this way: no
|
|
59
|
+
* `idle` animation, or an `idle` with no bone timeline, is the named subject
|
|
60
|
+
* absent and SKIPs; no mesh-driving bone among the bones `idle` does key is
|
|
61
|
+
* the construct absent and passes. `A10_NO_NAN_AFTER_STEPPING` was the same
|
|
62
|
+
* shape until issue #902, and is the next bullet's now: #882 gave it a
|
|
63
|
+
* setup-pose clause whose subject is the bones, beside the stepping clause
|
|
64
|
+
* whose subject is the animations.
|
|
65
|
+
* * An assertion with more than one clause SKIPs only when EVERY clause had
|
|
66
|
+
* nothing to measure, which is the shape `A09`, `A33` and `A38` already
|
|
67
|
+
* carry (`polygons.length === 0 && endsChecked === 0`), and `A10` since #902.
|
|
68
|
+
* `A13_MESH_BUDGET` is why the clause is stated: a declared slot budget is
|
|
69
|
+
* measured against a count of mesh slots, and zero is a count, so that half
|
|
70
|
+
* passes on a rig with no mesh — while a rig that declares only a TRIANGLE
|
|
71
|
+
* budget has nothing left to measure and skips.
|
|
72
|
+
*
|
|
73
|
+
* ⚠️ What the criterion is not allowed to become is a table of assertions with
|
|
74
|
+
* their verdicts written beside them. Every row above is decided by reading the
|
|
75
|
+
* name and the sentences the assertion already prints, so a rule added tomorrow
|
|
76
|
+
* is decided by the same reading and nobody re-opens this question.
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* What a SKIP says when the artifact carries no member of the subject a rule
|
|
81
|
+
* quantifies over (issue #580), by subject.
|
|
82
|
+
*
|
|
83
|
+
* One constant per SUBJECT rather than one per assertion, for the reason the two
|
|
84
|
+
* above are two: what the rule was denied is the whole content of its SKIP, and
|
|
85
|
+
* three rules denied the same thing should say so in the same words. Exported so
|
|
86
|
+
* a control compares against them rather than quoting them.
|
|
87
|
+
*/
|
|
88
|
+
export const SKIP_NO_REGION_ATTACHMENT = 'the skeleton carries no region attachment';
|
|
89
|
+
export const SKIP_NO_MESH_ATTACHMENT = 'the skeleton carries no mesh attachment';
|
|
90
|
+
export const SKIP_NO_ANIMATION = 'the skeleton carries no animation';
|
|
91
|
+
/**
|
|
92
|
+
* A10's (issue #902): its setup clause reads the bones and its stepping clause
|
|
93
|
+
* the animations, so it skips only when the skeleton carries neither — and the
|
|
94
|
+
* sentence is `SKIP_NO_ANIMATION`'s with the second subject added, so the two
|
|
95
|
+
* cannot drift into different words for the same absence.
|
|
96
|
+
*/
|
|
97
|
+
export const SKIP_NO_POSE = `${SKIP_NO_ANIMATION} and no bone, so there is no setup pose to read and nothing to step`;
|
|
98
|
+
export const SKIP_NO_TIMELINE = 'no animation here carries a timeline';
|
|
99
|
+
export const SKIP_NO_PHYSICS_CONSTRAINT = 'the skeleton declares no physics constraint';
|
|
100
|
+
export const SKIP_NO_ATLAS_PAGE = 'the atlas declares no page';
|
|
101
|
+
export const SKIP_NO_ATLAS_REGION = 'the atlas declares no region';
|
|
102
|
+
export const SKIP_NO_ATTACHMENT_REGION_JOIN =
|
|
103
|
+
'no attachment names a region and the atlas declares none, so there is no attachment-to-region join to hold';
|
|
104
|
+
export const SKIP_NO_TWO_COLOR_TINT =
|
|
105
|
+
'no slot declares a "dark" colour and no animation keys an "rgba2" or "rgb2" timeline, so there is no two-colour tint to read back';
|
|
106
|
+
export const SKIP_NO_SEPARABLE_COLOR =
|
|
107
|
+
'no animation keys an "rgb" or "alpha" timeline, so there is no separable slot colour to read back';
|
|
108
|
+
export const SKIP_NO_LINKED_MESH = 'no attachment in this skeleton takes its geometry from another one';
|
|
109
|
+
export const SKIP_NO_SEQUENCE =
|
|
110
|
+
'no attachment carries a "sequence" block and no animation keys a "sequence" timeline, so there is no numbered series to read back';
|
|
111
|
+
/**
|
|
112
|
+
* A09's, which predates this list and joins it rather than being rewritten: it
|
|
113
|
+
* is the same fact about the same subject, and a control that compares against
|
|
114
|
+
* eight constants and quotes the ninth is a control with a hand-kept exception
|
|
115
|
+
* in it.
|
|
116
|
+
*/
|
|
117
|
+
export const SKIP_NO_DECLARED_DURATION =
|
|
118
|
+
'the motion spec declares no animations and the skeleton has none — a static rig has no duration to compare';
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The model side's `SKIP_NO_SKELETON` (issue #1025): the reader refused the
|
|
122
|
+
* document, or the document names a region its pages lack, so a body that
|
|
123
|
+
* reads the model has nothing to read. It names the two model-side parse
|
|
124
|
+
* rules rather than A00, because on this side there is no round trip — the
|
|
125
|
+
* reader and the region rule are what stand in its place (`./model/parse.ts`).
|
|
126
|
+
*/
|
|
127
|
+
export const SKIP_NO_MODEL = 'the model side read no document to measure (A00_MODEL_READ or A00_MODEL_REGIONS_ON_PAGES owns that failure)';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How an assertion body reads a value — the helpers the bodies under
|
|
3
|
+
* `./bodies/` share with the ones still in `src/validate.ts` (issue #1025,
|
|
4
|
+
* step 4c of #380), moved here unchanged so both read a value one way. Links
|
|
5
|
+
* nothing from the runtime.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A JSON object as `JSON.parse` returns it. */
|
|
9
|
+
export type Json = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
/** A JSON object, and not an array or `null`. */
|
|
12
|
+
export function isObj(v: unknown): v is Json {
|
|
13
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The time to pose a key at so that the runtime is AT it: the later of the
|
|
18
|
+
* file's number and that number as spine-core stores it (issue #771).
|
|
19
|
+
*
|
|
20
|
+
* 🚨 Every timeline keeps its key times in a `Float32Array`
|
|
21
|
+
* (`Utils.newFloatArray`), and a key time the float cannot hold exactly is
|
|
22
|
+
* stored at the nearest one — for `0.2`, `0.20000000298…`, which is LATER than
|
|
23
|
+
* the double `0.2`. Stepped to the file's own number, a timeline is then just
|
|
24
|
+
* BEFORE its key: before a first key it writes the setup value (`time <
|
|
25
|
+
* frames[0]`), and past a stepped key it still holds the one before
|
|
26
|
+
* (`frames[i] > time`). That is a pose one float step from the key, not the
|
|
27
|
+
* key's — measured on the selftest's own `ingest_probe`, whose `alpha` key at
|
|
28
|
+
* 0.2 posed the setup 1.0 and was refused as `the key states value 0.4`.
|
|
29
|
+
*
|
|
30
|
+
* ⚠️ The later of the two rather than `Math.fround` alone: where the float
|
|
31
|
+
* rounds DOWN, the file's number is already past the stored key, and a runtime
|
|
32
|
+
* built without typed arrays stores the double itself — in both, the file's
|
|
33
|
+
* number is the one at or after the key. What a key time rounds to is the
|
|
34
|
+
* runtime's storage and not the file's statement, so a rule judging what a KEY
|
|
35
|
+
* states poses at the key; the rounding itself is nothing an author can repair.
|
|
36
|
+
*/
|
|
37
|
+
export function atStoredKey(time: number): number {
|
|
38
|
+
return Math.max(time, Math.fround(time));
|
|
39
|
+
}
|
package/src/check.ts
CHANGED
|
@@ -878,8 +878,12 @@ export interface TextureFromReport {
|
|
|
878
878
|
atlas: string;
|
|
879
879
|
/** Every `scale:` its text declares — the line that says a pack is coarser. */
|
|
880
880
|
scales: number[];
|
|
881
|
-
/**
|
|
882
|
-
|
|
881
|
+
/**
|
|
882
|
+
* The same for the CANDIDATE's own atlas, so the two are read side by side —
|
|
883
|
+
* `null` when the candidate has no atlas beside it to read them off (issue
|
|
884
|
+
* #1020), which is not the same claim as declaring none.
|
|
885
|
+
*/
|
|
886
|
+
candidateScales: number[] | null;
|
|
883
887
|
/**
|
|
884
888
|
* Regions the substituting atlas does not have, or the candidate packs rotated
|
|
885
889
|
* so its own corner order cannot be inverted — see `artUvsOf`. Those pieces kept
|
|
@@ -987,7 +991,13 @@ export interface CheckReport {
|
|
|
987
991
|
|
|
988
992
|
export interface CheckOptions {
|
|
989
993
|
skeletonText: string;
|
|
990
|
-
|
|
994
|
+
/**
|
|
995
|
+
* The candidate's atlas, or `null` when there is no file (issue #1020): a
|
|
996
|
+
* rigc build the core poses from a `rigc-compiled/2` document is drawn
|
|
997
|
+
* without one, and anything else is refused naming the file
|
|
998
|
+
* (`CandidateAtlasError`).
|
|
999
|
+
*/
|
|
1000
|
+
atlasText: string | null;
|
|
991
1001
|
/** Where the atlas's page paths resolve from. */
|
|
992
1002
|
atlasDir: string;
|
|
993
1003
|
framesDir: string;
|
|
@@ -1161,8 +1171,13 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
|
|
|
1161
1171
|
// The substitution is loaded before anything is posed, because posing has to
|
|
1162
1172
|
// record each piece's original-art UVs for it and that is the one thing about
|
|
1163
1173
|
// this measure that cannot be added afterwards — see `PieceTexture`.
|
|
1174
|
+
//
|
|
1175
|
+
// Read by rigc's own atlas reader when the core poses the candidate, and by
|
|
1176
|
+
// spine-core's when the runtime does (issue #1020, `SubstitutionReader`): a
|
|
1177
|
+
// rigc build's `--texture-from` reads nothing through the runtime, and an
|
|
1178
|
+
// export's reading is the one it always had.
|
|
1164
1179
|
const substitution = options.textureFrom
|
|
1165
|
-
? textureSubstitutionFromText(options.textureFrom.atlasText, options.textureFrom.atlasDir)
|
|
1180
|
+
? textureSubstitutionFromText(options.textureFrom.atlasText, options.textureFrom.atlasDir, choice.core === null ? 'spine' : 'rigc')
|
|
1166
1181
|
: null;
|
|
1167
1182
|
// The skin is refused here rather than deeper in the sampler, for the reason
|
|
1168
1183
|
// every miss in this project is refused where the names are: the skeleton is
|
|
@@ -1600,7 +1615,11 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
|
|
|
1600
1615
|
);
|
|
1601
1616
|
}
|
|
1602
1617
|
|
|
1603
|
-
|
|
1618
|
+
// `null` when the candidate has no atlas to read them off (issue #1020): the
|
|
1619
|
+
// model document a build is drawn from without one states where each region
|
|
1620
|
+
// sits, not the `scale:` line, so the report says it was not read rather than
|
|
1621
|
+
// that none was declared.
|
|
1622
|
+
const candidateScales = options.atlasText === null ? null : atlasScales(options.atlasText);
|
|
1604
1623
|
if (substitution === null) {
|
|
1605
1624
|
// ⭐ Named at the top of every report rather than only when it is measured.
|
|
1606
1625
|
// Issue #171's finding was not that the floor was mis-measured; it was that
|
|
@@ -1610,7 +1629,13 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
|
|
|
1610
1629
|
notes.push(
|
|
1611
1630
|
'part of every MAE below is TEXTURE, not animation, and it is not attributed here. The frames were rendered ' +
|
|
1612
1631
|
"through the reference's own atlas; this candidate samples its own" +
|
|
1613
|
-
`${
|
|
1632
|
+
`${
|
|
1633
|
+
candidateScales === null
|
|
1634
|
+
? ' (its atlas is not beside it, so whether that declares a scale: line is not read — the model document does not state one)'
|
|
1635
|
+
: candidateScales.length > 0
|
|
1636
|
+
? ` (declared at scale: ${[...new Set(candidateScales)].join(', ')})`
|
|
1637
|
+
: ''
|
|
1638
|
+
}` +
|
|
1614
1639
|
', and if the two were packed at different scales every edge of every part is filtered from a different ' +
|
|
1615
1640
|
'source in every frame. That difference is a constant no key can move and is invisible to the content box, ' +
|
|
1616
1641
|
'the fit residual and the whole-pixel refinement. Pass --texture-from <the atlas the frames were rendered ' +
|
|
@@ -3540,8 +3565,8 @@ export function checkLines(report: CheckReport, opts?: { allFrames?: boolean }):
|
|
|
3540
3565
|
}
|
|
3541
3566
|
if (report.textureFrom) {
|
|
3542
3567
|
const t = report.textureFrom;
|
|
3543
|
-
const scale = (values: number[]): string =>
|
|
3544
|
-
values.length === 0 ? 'no scale: line' : `scale: ${[...new Set(values)].join(', ')}`;
|
|
3568
|
+
const scale = (values: number[] | null): string =>
|
|
3569
|
+
values === null ? 'no atlas beside it to read a scale: line from' : values.length === 0 ? 'no scale: line' : `scale: ${[...new Set(values)].join(', ')}`;
|
|
3545
3570
|
lines.push(
|
|
3546
3571
|
` texture from ${t.atlas} (${scale(t.scales)}) against the candidate's own (${scale(t.candidateScales)})` +
|
|
3547
3572
|
`${t.unmatched.length === 0 ? '' : ` ⚠️ ${t.unmatched.length} region(s) not substituted`}`,
|
package/src/compile.ts
CHANGED
|
@@ -117,6 +117,7 @@ import {
|
|
|
117
117
|
import { evaluateDeformTransform } from './deformgen.ts';
|
|
118
118
|
import { evaluateTrackDerive, TRACK_DERIVE_PROJECTIONS, type TrackDeriveMember } from './trackgen.ts';
|
|
119
119
|
import {
|
|
120
|
+
computeExactFrameTransforms,
|
|
120
121
|
computeWorldTransforms,
|
|
121
122
|
cropToSpineY,
|
|
122
123
|
normaliseDegrees,
|
|
@@ -2564,8 +2565,13 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2564
2565
|
}
|
|
2565
2566
|
const boneNames = new Set(bones.map((b) => b.name));
|
|
2566
2567
|
let transforms: Map<string, BoneTransform>;
|
|
2568
|
+
// The frames a depth turn ceiling is measured in — the same bones without the
|
|
2569
|
+
// runtime's last bits (`computeExactFrameTransforms`, issue #1021). Nothing is
|
|
2570
|
+
// bound through them.
|
|
2571
|
+
let exactFrames: Map<string, BoneTransform>;
|
|
2567
2572
|
try {
|
|
2568
2573
|
transforms = computeWorldTransforms(bones);
|
|
2574
|
+
exactFrames = computeExactFrameTransforms(bones);
|
|
2569
2575
|
} catch (err) {
|
|
2570
2576
|
if (err instanceof TransformError) throw new CompileError(err.message);
|
|
2571
2577
|
throw err;
|
|
@@ -2743,7 +2749,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2743
2749
|
|
|
2744
2750
|
if (part) {
|
|
2745
2751
|
const perSlot: Record<string, SkinTableEntry> = {};
|
|
2746
|
-
const mesh = part.mesh ? buildMesh(part, manifest!, bones, transforms, rigSlot.bone) : null;
|
|
2752
|
+
const mesh = part.mesh ? buildMesh(part, manifest!, bones, transforms, exactFrames, rigSlot.bone) : null;
|
|
2747
2753
|
for (const name of names) {
|
|
2748
2754
|
const img = images.find((im) => im.region === name);
|
|
2749
2755
|
if (!img) throw new CompileError(`internal: no image for attachment ${name}`);
|
|
@@ -2783,6 +2789,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2783
2789
|
images,
|
|
2784
2790
|
bones,
|
|
2785
2791
|
transforms,
|
|
2792
|
+
exactFrames,
|
|
2786
2793
|
meshBones,
|
|
2787
2794
|
meshes,
|
|
2788
2795
|
slotName: rigSlot.name,
|
|
@@ -3770,6 +3777,8 @@ interface AttachmentContext {
|
|
|
3770
3777
|
images: CompiledImage[];
|
|
3771
3778
|
bones: ModelBone[];
|
|
3772
3779
|
transforms: Map<string, BoneTransform>;
|
|
3780
|
+
/** The same bones in `computeExactFrameTransforms`' frames: what a depth turn ceiling is measured in, and nothing else (issue #1021). */
|
|
3781
|
+
exactFrames: Map<string, BoneTransform>;
|
|
3773
3782
|
meshBones: Set<string>;
|
|
3774
3783
|
meshes: CompileResult['meshes'];
|
|
3775
3784
|
slotName: string;
|
|
@@ -5215,9 +5224,11 @@ function buildGeneratedMesh(
|
|
|
5215
5224
|
// refusals, because the ring's control angles need the transform and the bind
|
|
5216
5225
|
// needs the bone — and the two must refuse a bone the rig lacks in the same
|
|
5217
5226
|
// words whichever of them asks for it first.
|
|
5218
|
-
|
|
5227
|
+
// Two readings of one bone (issue #1021): the runtime's matrices to bind
|
|
5228
|
+
// through, and the exact frame to measure the control angles in.
|
|
5229
|
+
const resolve = (name: string, frames: Map<string, BoneTransform> = ctx.transforms): BoneTransform => {
|
|
5219
5230
|
if (!ctx.bones.some((b) => b.name === name)) throw new CompileError(`${where}: mesh bone "${name}" is not in the rig's bone list`);
|
|
5220
|
-
const transform =
|
|
5231
|
+
const transform = frames.get(name);
|
|
5221
5232
|
if (!transform) throw new CompileError(`${where}: no setup transform for mesh bone "${name}"`);
|
|
5222
5233
|
return transform;
|
|
5223
5234
|
};
|
|
@@ -5234,7 +5245,11 @@ function buildGeneratedMesh(
|
|
|
5234
5245
|
const anchor = ctx.transforms.get(ctx.anchorBone);
|
|
5235
5246
|
if (!anchor) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
|
|
5236
5247
|
const place = (px: number, py: number): [number, number] => [anchor.worldX + px - w / 2, anchor.worldY + h / 2 - py];
|
|
5237
|
-
|
|
5248
|
+
// The inverse a control angle is measured through, in the exact frame: the
|
|
5249
|
+
// anchor's and the control's positions as the spec put them, not as the
|
|
5250
|
+
// runtime's last bits put them (issue #1021).
|
|
5251
|
+
const exactAnchor = resolve(ctx.anchorBone, ctx.exactFrames);
|
|
5252
|
+
const toPartLocal = (wx: number, wy: number): [number, number] => [wx - exactAnchor.worldX + w / 2, exactAnchor.worldY + h / 2 - wy];
|
|
5238
5253
|
let geometry;
|
|
5239
5254
|
try {
|
|
5240
5255
|
geometry =
|
|
@@ -5247,7 +5262,7 @@ function buildGeneratedMesh(
|
|
|
5247
5262
|
size: generator.size,
|
|
5248
5263
|
bias: generator.bias,
|
|
5249
5264
|
controlAngles: ringControlAngles(controls, generator.center, (name) => {
|
|
5250
|
-
const transform = resolve(name);
|
|
5265
|
+
const transform = resolve(name, ctx.exactFrames);
|
|
5251
5266
|
return toPartLocal(transform.worldX, transform.worldY);
|
|
5252
5267
|
}),
|
|
5253
5268
|
});
|
|
@@ -5356,7 +5371,12 @@ function sampleMeshDepth(
|
|
|
5356
5371
|
/**
|
|
5357
5372
|
* A part-local pixel to the BIND space this mesh's vertices are emitted in —
|
|
5358
5373
|
* the same composition `bindWeightedVertices` applies, handed in rather than
|
|
5359
|
-
* rebuilt so the two cannot diverge
|
|
5374
|
+
* rebuilt so the two cannot diverge — taken in the slot bone's exact frame
|
|
5375
|
+
* (`computeExactFrameTransforms`) rather than the runtime's. The two differ
|
|
5376
|
+
* in the last bits only, and those bits are the runtime's π rather than the
|
|
5377
|
+
* mesh: an unrotated bone reads `b = −2.3e-8` there, which turns the exact
|
|
5378
|
+
* zero this ceiling reads as "this axis cannot fold" into a fold at
|
|
5379
|
+
* 89.99997° (issue #1021; `TC03` and `TC06` are that claim).
|
|
5360
5380
|
*
|
|
5361
5381
|
* 🚨 The ceiling has to be taken in bind space and in no other. A deform
|
|
5362
5382
|
* offset is authored there, so a rotated slot bone MIXES the two axes: a
|
|
@@ -5785,7 +5805,8 @@ function buildGridAttachment(
|
|
|
5785
5805
|
throw new CompileError(`${where}: the spec says height ${att.height} and "${att.image}" measures ${h}`);
|
|
5786
5806
|
}
|
|
5787
5807
|
const anchor = ctx.transforms.get(ctx.anchorBone);
|
|
5788
|
-
|
|
5808
|
+
const frame = ctx.exactFrames.get(ctx.anchorBone);
|
|
5809
|
+
if (!anchor || !frame) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
|
|
5789
5810
|
if (!ctx.bones.some((b) => b.name === ctx.anchorBone)) {
|
|
5790
5811
|
throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" is not in the rig's bone list`);
|
|
5791
5812
|
}
|
|
@@ -5800,7 +5821,7 @@ function buildGridAttachment(
|
|
|
5800
5821
|
'grid',
|
|
5801
5822
|
geometry.points,
|
|
5802
5823
|
geometry.triangles,
|
|
5803
|
-
(px, py) => toBoneLocal(
|
|
5824
|
+
(px, py) => toBoneLocal(frame, frame.worldX + px * toArt - w / 2, frame.worldY + h / 2 - py * toArt),
|
|
5804
5825
|
// The lattice's geometry takes nothing off the texels — its window is
|
|
5805
5826
|
// the plate's size, which is the atlas's own `offsets` — so a page
|
|
5806
5827
|
// whose file is not its declared size costs a grid only this one
|
|
@@ -5985,7 +6006,11 @@ function buildSegmentsAttachment(
|
|
|
5985
6006
|
|
|
5986
6007
|
// -- the placement ---------------------------------------------------------
|
|
5987
6008
|
const slotBone = ctx.transforms.get(ctx.anchorBone);
|
|
5988
|
-
|
|
6009
|
+
// The exact frame (issue #1021) is what the segments and the vertices are
|
|
6010
|
+
// MEASURED in — distances, falloff, `minWeight`, the shared-origin refusal —
|
|
6011
|
+
// and the runtime's is what the vertices are BOUND through (`place`).
|
|
6012
|
+
const exactSlotBone = ctx.exactFrames.get(ctx.anchorBone);
|
|
6013
|
+
if (!slotBone || !exactSlotBone) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
|
|
5989
6014
|
let anchorAt: [number, number] = [w / 2, h / 2];
|
|
5990
6015
|
if (generator.anchor !== undefined) {
|
|
5991
6016
|
const stated = finitePair(generator.anchor);
|
|
@@ -5998,10 +6023,12 @@ function buildSegmentsAttachment(
|
|
|
5998
6023
|
anchorAt = stated;
|
|
5999
6024
|
}
|
|
6000
6025
|
const [ax, ay] = anchorAt;
|
|
6001
|
-
const
|
|
6002
|
-
|
|
6003
|
-
|
|
6026
|
+
const placeIn = (bone: BoneTransform) => (px: number, py: number): [number, number] => [
|
|
6027
|
+
bone.worldX + (px - ax),
|
|
6028
|
+
bone.worldY + (cropToSpineY(py, h) - cropToSpineY(ay, h)),
|
|
6004
6029
|
];
|
|
6030
|
+
const place = placeIn(slotBone);
|
|
6031
|
+
const measuredAt = placeIn(exactSlotBone);
|
|
6005
6032
|
|
|
6006
6033
|
// -- the segments ----------------------------------------------------------
|
|
6007
6034
|
if (!Array.isArray(generator.bones) || generator.bones.length === 0) {
|
|
@@ -6025,7 +6052,7 @@ function buildSegmentsAttachment(
|
|
|
6025
6052
|
named.push(name);
|
|
6026
6053
|
refs.push(meshBoneRef(name, where, ctx));
|
|
6027
6054
|
}
|
|
6028
|
-
return { index, transform: ctx.
|
|
6055
|
+
return { index, transform: ctx.exactFrames.get(name)! };
|
|
6029
6056
|
};
|
|
6030
6057
|
const tipOf = (name: string, transform: BoneTransform, at: string, chained: boolean): [number, number] => {
|
|
6031
6058
|
const length = ctx.bones.find((b) => b.name === name)?.length ?? 0;
|
|
@@ -6093,7 +6120,7 @@ function buildSegmentsAttachment(
|
|
|
6093
6120
|
`${JSON.stringify(from === null ? span.from : span.to)}; both ends are [x, y] in the part's pixels, y down`,
|
|
6094
6121
|
);
|
|
6095
6122
|
}
|
|
6096
|
-
segments.push({ bone: index, a:
|
|
6123
|
+
segments.push({ bone: index, a: measuredAt(from[0], from[1]), b: measuredAt(to[0], to[1]) });
|
|
6097
6124
|
return;
|
|
6098
6125
|
}
|
|
6099
6126
|
throw new CompileError(
|
|
@@ -6105,7 +6132,7 @@ function buildSegmentsAttachment(
|
|
|
6105
6132
|
// -- the weights -----------------------------------------------------------
|
|
6106
6133
|
const falloff = { power, radius: f.radius, maxBones, minWeight };
|
|
6107
6134
|
const weights: MeshVertexWeight[][] = lattice.points.map(([px, py], v) => {
|
|
6108
|
-
const got = segmentShares(
|
|
6135
|
+
const got = segmentShares(measuredAt(px, py), segments, falloff);
|
|
6109
6136
|
if ('weights' in got) return got.weights;
|
|
6110
6137
|
throw new CompileError(
|
|
6111
6138
|
`${where}: vertex ${v}, at (${px}, ${py}) in the part's pixels, keeps no bone — the ${got.dropped.length} ` +
|
|
@@ -6284,7 +6311,8 @@ function buildContourAttachment(
|
|
|
6284
6311
|
throw new CompileError(`${where}: the spec says height ${att.height} and "${att.image}" measures ${h}`);
|
|
6285
6312
|
}
|
|
6286
6313
|
const anchor = ctx.transforms.get(ctx.anchorBone);
|
|
6287
|
-
|
|
6314
|
+
const frame = ctx.exactFrames.get(ctx.anchorBone);
|
|
6315
|
+
if (!anchor || !frame) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
|
|
6288
6316
|
if (!ctx.bones.some((b) => b.name === ctx.anchorBone)) {
|
|
6289
6317
|
throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" is not in the rig's bone list`);
|
|
6290
6318
|
}
|
|
@@ -6308,7 +6336,7 @@ function buildContourAttachment(
|
|
|
6308
6336
|
'contour',
|
|
6309
6337
|
geometry.points,
|
|
6310
6338
|
geometry.triangles,
|
|
6311
|
-
(px, py) => toBoneLocal(
|
|
6339
|
+
(px, py) => toBoneLocal(frame, frame.worldX + px * toArt - w / 2, frame.worldY + h / 2 - py * toArt),
|
|
6312
6340
|
alpha,
|
|
6313
6341
|
sheetGridOf(img, plate),
|
|
6314
6342
|
where,
|
|
@@ -7120,6 +7148,8 @@ function buildMesh(
|
|
|
7120
7148
|
manifest: FaceManifest,
|
|
7121
7149
|
bones: ModelBone[],
|
|
7122
7150
|
transforms: Map<string, BoneTransform>,
|
|
7151
|
+
/** The same bones in the exact frame (`computeExactFrameTransforms`): what the control angles are measured in. */
|
|
7152
|
+
exactFrames: Map<string, BoneTransform>,
|
|
7123
7153
|
anchorName: string,
|
|
7124
7154
|
): { attachment: ModelMeshAttachment; kind: 'ring' | 'ribbon' } {
|
|
7125
7155
|
const spec = part.mesh!;
|
|
@@ -7152,9 +7182,12 @@ function buildMesh(
|
|
|
7152
7182
|
// measures in crop pixels, y down, and Spine world is that crop flipped, so
|
|
7153
7183
|
// `cropH - worldY` is the whole of it. `spec.center` is in the same crop
|
|
7154
7184
|
// pixels, which is why the centre passed here is not the window-local
|
|
7155
|
-
// `centre` two lines up.
|
|
7185
|
+
// `centre` two lines up. The positions are read in the exact frame
|
|
7186
|
+
// (issue #1021): an angle is a statement about where the spec put the
|
|
7187
|
+
// bone, it orders the split and refuses a tie, and the runtime's last
|
|
7188
|
+
// bits turned an authored 90 into 89.99999734089101 in that refusal.
|
|
7156
7189
|
const controlAngles = ringControlAngles(controls, spec.center!, (name) => {
|
|
7157
|
-
const m =
|
|
7190
|
+
const m = exactFrames.get(name);
|
|
7158
7191
|
if (!m) throw new CompileError(`internal: no setup transform for control bone "${name}"`);
|
|
7159
7192
|
return [m.worldX, cropH - m.worldY];
|
|
7160
7193
|
});
|
|
@@ -7894,6 +7927,13 @@ export function deformGeometryOf(
|
|
|
7894
7927
|
// what makes that provable — the alternative, folding it into the world path
|
|
7895
7928
|
// below, would re-evaluate every existing `transform` key at world
|
|
7896
7929
|
// coordinates and emit a different file for a spec nobody edited.
|
|
7930
|
+
//
|
|
7931
|
+
// 🔸 Issue #1021: this reads the runtime's frame, not the exact one. The
|
|
7932
|
+
// offsets are added by the runtime to exactly these numbers, so the model is
|
|
7933
|
+
// evaluated on what the file holds; a compiler-bound vertex therefore carries
|
|
7934
|
+
// the runtime's last bits (`b = −2.3e-8` on an unrotated bone) into its
|
|
7935
|
+
// offset, about 1e-6 on gallery/look's hair locks. Evaluating on the authored
|
|
7936
|
+
// positions instead would need them carried here from the binder.
|
|
7897
7937
|
if (boneCounts.every((n) => n === 1) && bones.size === 1) {
|
|
7898
7938
|
return { ...common, setup: bindSpace, influenceBones: null, setupWhy: null };
|
|
7899
7939
|
}
|
package/src/core/index.ts
CHANGED
|
@@ -154,11 +154,13 @@
|
|
|
154
154
|
* World transforms come from the core's own evaluator, `worldTransforms` in
|
|
155
155
|
* `./world.ts`, written from what a bone's fields mean and from measurement
|
|
156
156
|
* against the runtime's dump; its header states each measured choice. It is
|
|
157
|
-
* not `src/transform.ts`'s `computeWorldTransforms
|
|
158
|
-
* the compiler's
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
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
|
|
162
164
|
* `CorePlant` — the evaluator, the skin resolution, the colour and blend readings — so
|
|
163
165
|
* the suite's plants can pass a copy of one of them; nothing else passes one.
|
|
164
166
|
*
|
package/src/core/world.ts
CHANGED
|
@@ -5,21 +5,22 @@
|
|
|
5
5
|
* (`tools/pose_oracle.ts dump`, spine-core 4.3.13), and held to it by the core
|
|
6
6
|
* suite's equivalence controls.
|
|
7
7
|
*
|
|
8
|
-
* ⭐ **The compiler's setup transforms are this evaluator too** (issue #1015)
|
|
9
|
-
* `src/transform.ts` held a second
|
|
10
|
-
*
|
|
11
|
-
* degree factor (`Math.PI / 180`
|
|
12
|
-
* frame of a bone with scale 1 and
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* multiplied by `180 / pi` here).
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* compiler's
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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.
|
|
23
24
|
*
|
|
24
25
|
* ## What each measured choice is
|
|
25
26
|
*
|
|
@@ -115,9 +116,9 @@ export const COLLAPSED_X_AXIS_SQ = 0.00001 * 0.00001;
|
|
|
115
116
|
|
|
116
117
|
/**
|
|
117
118
|
* The three places an evaluator's arithmetic may differ in the last bit while
|
|
118
|
-
* computing the same matrix (issue #1015) — see the header. The core poses
|
|
119
|
-
* `RUNTIME_ARITHMETIC
|
|
120
|
-
*
|
|
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.
|
|
121
122
|
*/
|
|
122
123
|
export interface WorldArithmetic {
|
|
123
124
|
/** Degrees to radians: every angle is multiplied by it. */
|
|
@@ -254,8 +255,8 @@ function modeOf(bone: ModelBone): CoreInheritMode {
|
|
|
254
255
|
* posed from those zeros like any child. `inherit` replaces the mode
|
|
255
256
|
* computation — the core suite's plant passes a copy with one mode's sign
|
|
256
257
|
* flipped, and nothing else does. `arithmetic` is the runtime's unless a
|
|
257
|
-
* caller states its own: `src/transform.ts`
|
|
258
|
-
* #
|
|
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.
|
|
259
260
|
*/
|
|
260
261
|
export function worldTransforms(
|
|
261
262
|
bones: readonly ModelBone[],
|