spine-rigc 2.1.2 → 2.2.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/README.md +11 -3
- package/docs/AUTHORING.md +69 -22
- package/docs/INGEST.md +44 -25
- package/package.json +1 -1
- package/src/assertions/emitted/index.ts +28 -1
- package/src/assertions/facts/stage.ts +6 -3
- package/src/assertions/model/index.ts +41 -1
- package/src/cli/core_commands.ts +15 -8
- package/src/cli/shared.ts +70 -3
- package/src/cli/spine_commands.ts +13 -7
- package/src/compile.ts +100 -4
- package/src/core/raw.ts +101 -1
- package/src/diff.ts +71 -65
- package/src/emit_spine.ts +26 -12
- package/src/ingest.ts +38 -9
- package/src/model.ts +14 -10
- package/src/rig.ts +9 -2
- package/src/types.ts +4 -2
- package/src/validate.ts +41 -7
- package/tools/editor_roundtrip.ts +130 -42
package/src/diff.ts
CHANGED
|
@@ -1702,64 +1702,66 @@ function diffEvents(c: Json, r: Json): DiffSection {
|
|
|
1702
1702
|
// ---------------------------------------------------------------------------
|
|
1703
1703
|
|
|
1704
1704
|
/**
|
|
1705
|
-
* The
|
|
1706
|
-
*
|
|
1705
|
+
* The header's box: `x`, `y`, `width`, `height` of the setup-pose bounding box
|
|
1706
|
+
* — the box around the attachments at the setup pose, which is what the Spine
|
|
1707
|
+
* format says the four are — or the absence of all four.
|
|
1707
1708
|
*
|
|
1708
|
-
*
|
|
1709
|
-
*
|
|
1710
|
-
*
|
|
1711
|
-
*
|
|
1712
|
-
*
|
|
1713
|
-
*
|
|
1714
|
-
*
|
|
1709
|
+
* 🔁 **Renamed from `stage_present`/`stage_box` by issue #907, because the old
|
|
1710
|
+
* name had become a lie.** Until then `build` copied rigc's stage — the crop
|
|
1711
|
+
* the art was painted in — into these four fields, so on a rigc candidate the
|
|
1712
|
+
* measure read a stage and on an editor export it read a bounding box, under
|
|
1713
|
+
* one word. Since #907 `build` writes the setup-pose bounding box there too
|
|
1714
|
+
* (`headerBoundsOf` in `src/compile.ts`, held to spine-core's `getBounds`), so
|
|
1715
|
+
* both sides of every comparison carry the same kind of value, and the stage —
|
|
1716
|
+
* which a rig spec states and the model document carries — is not in the file
|
|
1717
|
+
* at all. A measure called `stage` would send a reader to change the stage,
|
|
1718
|
+
* which no longer moves this number.
|
|
1715
1719
|
*
|
|
1716
|
-
*
|
|
1717
|
-
*
|
|
1718
|
-
*
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1720
|
+
* 🔍 **Why this measure exists at all** (issue #578). The header box was the
|
|
1721
|
+
* one value `build` required and no instrument in this tree could see:
|
|
1722
|
+
* `validate`, `check` and `render` all ignore it — `render` frames from the
|
|
1723
|
+
* posed bounds — and `diff` had no header measure, so a sweep that handed a
|
|
1724
|
+
* deliberately absurd unit stage `0,0,1,1` to 37 real exports read **1.000 on
|
|
1725
|
+
* every measure** for 32 of them.
|
|
1726
|
+
*
|
|
1727
|
+
* ⭐ **`bounds_present` is 1/1 or 0/1 and never `0/0`.** Both sides always have
|
|
1728
|
+
* a presence to compare, including when both say "none" — that is agreement,
|
|
1729
|
+
* not an absence of data, and `total: 0` here would be the vacuous 1.000 this
|
|
1730
|
+
* file refuses. `bounds_box` is the one that goes vacuous, and only when there
|
|
1731
|
+
* are not two boxes to compare.
|
|
1721
1732
|
*/
|
|
1722
|
-
interface
|
|
1733
|
+
interface HeaderBox {
|
|
1723
1734
|
present: boolean;
|
|
1724
1735
|
/**
|
|
1725
1736
|
* The four fields as the header MEANS them: the extent exactly as stated, and
|
|
1726
|
-
* the origin of a
|
|
1727
|
-
* `
|
|
1737
|
+
* the origin of a stated box as stated or `0` where it is omitted. See
|
|
1738
|
+
* `headerBox` for why the second half is a reading and not a fallback.
|
|
1728
1739
|
*/
|
|
1729
1740
|
box: Array<number | null>;
|
|
1730
1741
|
}
|
|
1731
1742
|
|
|
1732
|
-
const
|
|
1743
|
+
const BOX_FIELDS = ['x', 'y', 'width', 'height'] as const;
|
|
1733
1744
|
|
|
1734
1745
|
/**
|
|
1735
|
-
* A
|
|
1746
|
+
* A header states a box by its EXTENT: a numeric `width` and `height`.
|
|
1736
1747
|
*
|
|
1737
1748
|
* `x`/`y` alone are an origin for a box that is not there — no export carries
|
|
1738
|
-
* that shape, and rigc
|
|
1739
|
-
* own. It is also what the compiler requires and what `A14_NO_FULL_FRAME_MESH`
|
|
1740
|
-
* and `A19_OVERLAY_PNGS_HAVE_ALPHA` measure against, so the three agree on the
|
|
1741
|
-
* word by construction rather than by memory.
|
|
1749
|
+
* that shape, and rigc never emits it — so they do not make a box on their own.
|
|
1742
1750
|
*
|
|
1743
|
-
* ⭐ **Inside a
|
|
1744
|
-
*
|
|
1745
|
-
* file lives or dies by — and
|
|
1746
|
-
* word for the convention:
|
|
1751
|
+
* ⭐ **Inside a stated box, an omitted `x`/`y` IS `0`** (issue #620). That is a
|
|
1752
|
+
* reading of the format, not a value invented for a gap — the distinction this
|
|
1753
|
+
* file lives or dies by — and three measurements carry it, none of them
|
|
1754
|
+
* anybody's word for the convention:
|
|
1747
1755
|
*
|
|
1748
|
-
* - **This tree had already decided it, one file over.** `compile.ts` assembles
|
|
1749
|
-
* the header as `header.x = rig.skeleton?.x ?? 0`, under the same guard: only
|
|
1750
|
-
* when the extent is there. So a rig spec that omits its origin emits `0`, and
|
|
1751
|
-
* reading the same omission in a file as absence made the compiler and the
|
|
1752
|
-
* comparison disagree about one value in one header. This measure is not
|
|
1753
|
-
* adopting the editor's convention so much as stopping contradicting the
|
|
1754
|
-
* emitter it is pointed at.
|
|
1755
1756
|
* - **The header's writer omits a field at its default.** All twelve exports
|
|
1756
1757
|
* under `examples/` omit `referenceScale`, whose default the parser itself
|
|
1757
1758
|
* spells two lines below the four raw assignments (`SkeletonJson.js:74`,
|
|
1758
1759
|
* `getValue(skeletonMap, "referenceScale", 100)`), and none of the 389 bone
|
|
1759
1760
|
* `x`/`y`/`rotation`/`shearX`/`shearY` values those same files DO write is an
|
|
1760
|
-
* explicit `0`. A
|
|
1761
|
+
* explicit `0`. A box at the origin therefore has no spelling but the
|
|
1761
1762
|
* omission, so reading the omission as absence reads a value the format cannot
|
|
1762
|
-
* express.
|
|
1763
|
+
* express. (An editor round trip of a header stating `"x": 0, "y": 0` wrote
|
|
1764
|
+
* neither back — measured through a licensed 4.3.26 editor for issue #907.)
|
|
1763
1765
|
* - **The binary reader supplies it unconditionally.** `SkeletonBinary.js:69-72`
|
|
1764
1766
|
* reads the four as four floats with no key to be missing, so one skeleton's
|
|
1765
1767
|
* origin is `0` in a `.skel` and absent in a `.json`. A measure that called
|
|
@@ -1772,13 +1774,13 @@ const STAGE_FIELDS = ['x', 'y', 'width', 'height'] as const;
|
|
|
1772
1774
|
* editor has never exported a frame rate.
|
|
1773
1775
|
*
|
|
1774
1776
|
* ⚠️ **The default is the ORIGIN's alone**, and the guard is the paragraph above
|
|
1775
|
-
* it: the extent is what
|
|
1776
|
-
*
|
|
1777
|
-
*
|
|
1777
|
+
* it: the extent is what states a box, so defaulting it would turn the box-less
|
|
1778
|
+
* header of issue #578 into a `0x0` box at `0,0` and answer the question
|
|
1779
|
+
* instead of reading it.
|
|
1778
1780
|
*/
|
|
1779
|
-
function
|
|
1781
|
+
function headerBox(root: Json): HeaderBox {
|
|
1780
1782
|
const header = isObj(root.skeleton) ? root.skeleton : {};
|
|
1781
|
-
const box =
|
|
1783
|
+
const box = BOX_FIELDS.map((k) => num(header[k]));
|
|
1782
1784
|
const present = box[2] !== null && box[3] !== null;
|
|
1783
1785
|
return { present, box: present ? [box[0] ?? 0, box[1] ?? 0, box[2], box[3]] : box };
|
|
1784
1786
|
}
|
|
@@ -1787,47 +1789,51 @@ function stageFacts(root: Json): StageFacts {
|
|
|
1787
1789
|
* The two header measures.
|
|
1788
1790
|
*
|
|
1789
1791
|
* ⚠️ **The box is compared EXACTLY, and that is a measurement rather than a
|
|
1790
|
-
* choice.** The
|
|
1791
|
-
* measures use"; the structural measures in this file have exactly one —
|
|
1792
|
+
* choice.** The structural measures in this file have exactly one tolerance —
|
|
1792
1793
|
* `FRAME`, one sixtieth of a second, used once, for `animations.duration` — and
|
|
1793
1794
|
* no spatial one anywhere, because they compare no position at all (that is
|
|
1794
|
-
* `bonedist.ts`).
|
|
1795
|
-
*
|
|
1796
|
-
*
|
|
1797
|
-
*
|
|
1795
|
+
* `bonedist.ts`). Each side's box is a number its writer computed: rigc's is
|
|
1796
|
+
* spine-core's `getBounds` on the header's 1e-6 grid at float32 (`headerBoxNumber`; `tools/core_gate.ts` holds it), and
|
|
1797
|
+
* the editor's is its own arithmetic, which on the twelve examples sits up to
|
|
1798
|
+
* 0.0071 units from `getBounds` on the same skeleton and agrees with its
|
|
1799
|
+
* float32 on 13 of the 48 numbers (issue #907's measurement). A rebuild and its
|
|
1800
|
+
* export therefore read below 4/4 here by the editor's arithmetic alone, and an
|
|
1801
|
+
* epsilon chosen to hide that would be a number nobody measured, in the file
|
|
1802
|
+
* whose whole job is to report measured ones. The measure is reported and gates
|
|
1803
|
+
* nothing on the ladder.
|
|
1798
1804
|
*
|
|
1799
1805
|
* ⭐ `diffSkeletonValues` (issue #615) is the second tolerance in the file and
|
|
1800
1806
|
* it does not weaken this one. It compares positions, so it needs one; both its
|
|
1801
1807
|
* terms are read off other code — the 1e-6 grid rigc's closed-form models are
|
|
1802
|
-
* evaluated on and the parser's float32 storage — rather than chosen here; and
|
|
1803
|
-
* one place the two overlap, where `
|
|
1804
|
-
* says so by staying exact.
|
|
1808
|
+
* evaluated on and the parser's float32 storage — rather than chosen here; and
|
|
1809
|
+
* the four numbers above are the one place the two overlap, where `bounds_box`
|
|
1810
|
+
* stays the stricter reading and says so by staying exact.
|
|
1805
1811
|
*/
|
|
1806
1812
|
function diffHeader(c: Json, r: Json): DiffReported {
|
|
1807
|
-
const a =
|
|
1808
|
-
const b =
|
|
1813
|
+
const a = headerBox(c);
|
|
1814
|
+
const b = headerBox(r);
|
|
1809
1815
|
const both = a.present && b.present;
|
|
1810
1816
|
const agreed = both ? a.box.filter((v, i) => v === b.box[i]).length : 0;
|
|
1811
|
-
const side = (f:
|
|
1817
|
+
const side = (f: HeaderBox): string => (f.present ? `${f.box[2]}x${f.box[3]} at ${f.box[0]},${f.box[1]}` : 'none');
|
|
1812
1818
|
return {
|
|
1813
1819
|
measures: [
|
|
1814
1820
|
measure(
|
|
1815
|
-
'skeleton.
|
|
1816
|
-
'both
|
|
1821
|
+
'skeleton.bounds_present',
|
|
1822
|
+
'both headers carry a setup-pose bounding box, or neither does',
|
|
1817
1823
|
a.present === b.present ? 1 : 0,
|
|
1818
1824
|
1,
|
|
1819
|
-
`candidate ${side(a)}; reference ${side(b)}. A
|
|
1825
|
+
`candidate ${side(a)}; reference ${side(b)}. A header carries a box by stating a width and a height`,
|
|
1820
1826
|
),
|
|
1821
1827
|
measure(
|
|
1822
|
-
'skeleton.
|
|
1823
|
-
'the
|
|
1828
|
+
'skeleton.bounds_box',
|
|
1829
|
+
'the header carries the same box (x, y, width, height — the extent as stated, an omitted origin as the 0 it means)',
|
|
1824
1830
|
agreed,
|
|
1825
|
-
both ?
|
|
1831
|
+
both ? BOX_FIELDS.length : 0,
|
|
1826
1832
|
both
|
|
1827
1833
|
? `candidate ${side(a)}; reference ${side(b)}`
|
|
1828
1834
|
: a.present === b.present
|
|
1829
|
-
? 'neither
|
|
1830
|
-
: 'only one
|
|
1835
|
+
? 'neither header carries a box, so there is no box to compare — `bounds_present` carries that'
|
|
1836
|
+
: 'only one header carries a box, so there is no second box to compare — `bounds_present` carries that',
|
|
1831
1837
|
),
|
|
1832
1838
|
],
|
|
1833
1839
|
};
|
|
@@ -1971,7 +1977,7 @@ function valuesAgree(a: number | string, b: number | string): boolean {
|
|
|
1971
1977
|
* distinction `DiffMeasure.total` draws everywhere else in this file.
|
|
1972
1978
|
*/
|
|
1973
1979
|
const VALUE_MEASURES: ReadonlyArray<{ id: string; what: string; prefix: string }> = [
|
|
1974
|
-
{ id: 'values.skeleton', what: 'the header and
|
|
1980
|
+
{ id: 'values.skeleton', what: 'the header and its setup-pose bounding box', prefix: 'skeleton/' },
|
|
1975
1981
|
{ id: 'values.bones', what: 'every bone setup pose, its length and its colour', prefix: 'bones/' },
|
|
1976
1982
|
{ id: 'values.slots', what: 'every slot colour, dark colour, blend mode and setup attachment', prefix: 'slots/' },
|
|
1977
1983
|
{ id: 'values.attachments', what: 'every attachment offset, size, vertex, weight, uv and triangle', prefix: 'skins/' },
|
|
@@ -2170,7 +2176,7 @@ export function diffLines(report: DiffReport, labels: { candidate: string; refer
|
|
|
2170
2176
|
// with `(no mean)` for the same reason a section's `(reported)` block has one.
|
|
2171
2177
|
lines.push(
|
|
2172
2178
|
` ${'skeleton (reported)'.padEnd(21)} (no mean) over ${report.header.measures.length} measures` +
|
|
2173
|
-
' — the
|
|
2179
|
+
' — the header\'s setup-pose bounding box, which no reading of the frames could decide',
|
|
2174
2180
|
);
|
|
2175
2181
|
lines.push(...measureLines(report.header.measures, 'skeleton.'.length));
|
|
2176
2182
|
lines.push('');
|
|
@@ -2232,10 +2238,10 @@ export function diffLines(report: DiffReport, labels: { candidate: string; refer
|
|
|
2232
2238
|
lines.push(' the block is absent rather than guessed — and its absence beside `names` 0.000');
|
|
2233
2239
|
lines.push(' is the report saying the candidate named its shots itself and nothing said how.');
|
|
2234
2240
|
lines.push('');
|
|
2235
|
-
lines.push(' `skeleton` is the file\'s own header block and reports two measures for
|
|
2241
|
+
lines.push(' `skeleton` is the file\'s own header block and reports two measures for its box.');
|
|
2236
2242
|
lines.push(' It has no mean for the reason a `(reported)` block never does, and it never');
|
|
2237
2243
|
lines.push(' gates for two: no reading of the frames recovers a setup-pose bounding box, and');
|
|
2238
|
-
lines.push('
|
|
2244
|
+
lines.push(' each side\'s box is its writer\'s own arithmetic.');
|
|
2239
2245
|
lines.push('');
|
|
2240
2246
|
lines.push(' A `(reported)` block has no mean because its measures have unlike units, and');
|
|
2241
2247
|
lines.push(' it stays out of the section mean above it for the same reason no clause may');
|
package/src/emit_spine.ts
CHANGED
|
@@ -755,19 +755,33 @@ export function emitAnimations(
|
|
|
755
755
|
* stated, `images` spelled relative to `--out` (`skeletonImagesPath`);
|
|
756
756
|
* each only when present.
|
|
757
757
|
*
|
|
758
|
+
* - `bounds` — the setup-pose bounding box, `x`, `y`, `width`, `height`,
|
|
759
|
+
* which is what the format says the header's four are (issue #907):
|
|
760
|
+
* computed by `compile` with rigc's core over the model
|
|
761
|
+
* (`headerBoundsOf`), each on the 1e-6 grid at float32 (`headerBoxNumber`), or `null` for no box — a rig that
|
|
762
|
+
* declares no stage (issue #578), a setup pose that draws nothing, a
|
|
763
|
+
* region with no atlas rectangle, or a setup pose the core leaves out
|
|
764
|
+
* (that function's header). Four fields or none.
|
|
765
|
+
*
|
|
758
766
|
* `referenceScale` is not here: wind and gravity act over it, so a posing
|
|
759
767
|
* core reads it, and the model holds it (`CompiledModel.referenceScale`,
|
|
760
768
|
* issue #958). The emitter writes the model's value into the header, where
|
|
761
|
-
* `withoutParserDefaults` drops it at the parser's 100.
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
769
|
+
* `withoutParserDefaults` drops it at the parser's 100.
|
|
770
|
+
*
|
|
771
|
+
* ⚠️ Nor is the stage (`CompiledModel.stage`, issue #1026) — and since issue
|
|
772
|
+
* #907 the header does not carry it at all. Until then the emitter copied the
|
|
773
|
+
* stage into the four box fields, so a reader that took the header for what
|
|
774
|
+
* the format says it is — the box around the figure at rest, for scaling and
|
|
775
|
+
* layout — got the crop the art was painted in. The stage stays the model's:
|
|
776
|
+
* the document states it, and everything that measures against it reads it
|
|
777
|
+
* there.
|
|
765
778
|
*/
|
|
766
779
|
export interface SkeletonHeader {
|
|
767
780
|
spine: string;
|
|
768
781
|
fps?: number;
|
|
769
782
|
images?: string;
|
|
770
783
|
audio?: string | null;
|
|
784
|
+
bounds: { x: number; y: number; width: number; height: number } | null;
|
|
771
785
|
}
|
|
772
786
|
|
|
773
787
|
/** The editor's orders the emitter applies, passed for the reason the header's 🔸 gives. */
|
|
@@ -776,7 +790,7 @@ export interface SkeletonOrder extends EditorOrder {
|
|
|
776
790
|
}
|
|
777
791
|
|
|
778
792
|
/** The model fields a skeleton is written from. */
|
|
779
|
-
export type SkeletonSource = Pick<CompiledModel, 'referenceScale' | '
|
|
793
|
+
export type SkeletonSource = Pick<CompiledModel, 'referenceScale' | 'bones' | 'slots' | 'skins' | 'constraints' | 'events' | 'animations'>;
|
|
780
794
|
|
|
781
795
|
/**
|
|
782
796
|
* The Spine 4.3 skeleton of a compiled model: the emitter's one entry, and
|
|
@@ -791,7 +805,7 @@ export type SkeletonSource = Pick<CompiledModel, 'referenceScale' | 'stage' | 'b
|
|
|
791
805
|
* between `skins` and `animations`, where the editor writes it), `constraints`
|
|
792
806
|
* only when the model holds one (assigned after, so it lands last until the
|
|
793
807
|
* key-order pass moves it). The header's keys: `spine, x, y, width, height,
|
|
794
|
-
* fps, referenceScale, images, audio`, the
|
|
808
|
+
* fps, referenceScale, images, audio`, the box's four only together.
|
|
795
809
|
*
|
|
796
810
|
* Then, on the finished object and once: `withoutParserDefaults` drops every
|
|
797
811
|
* key at the value the 4.3 parser reads in its absence, and `inEditorKeyOrder`
|
|
@@ -800,7 +814,7 @@ export type SkeletonSource = Pick<CompiledModel, 'referenceScale' | 'stage' | 'b
|
|
|
800
814
|
* throws, so a refusal raised here is raised by a section emitter, in the
|
|
801
815
|
* order above.
|
|
802
816
|
*
|
|
803
|
-
* The
|
|
817
|
+
* The box (`header.bounds`, issue #907) is written as `compile` computed it,
|
|
804
818
|
* its four fields together or none. `referenceScale` is the model's, written
|
|
805
819
|
* always and dropped by the parser-default pass at 100 — so a rig stating none and a rig stating 100
|
|
806
820
|
* write the same bytes, as they did when the header carried the stated value.
|
|
@@ -811,11 +825,11 @@ export type SkeletonSource = Pick<CompiledModel, 'referenceScale' | 'stage' | 'b
|
|
|
811
825
|
*/
|
|
812
826
|
export function emitSkeleton(model: SkeletonSource, header: SkeletonHeader, order: SkeletonOrder): SpineSkeletonJson {
|
|
813
827
|
const head: SpineSkeletonJson['skeleton'] = { spine: header.spine };
|
|
814
|
-
if (
|
|
815
|
-
head.x =
|
|
816
|
-
head.y =
|
|
817
|
-
head.width =
|
|
818
|
-
head.height =
|
|
828
|
+
if (header.bounds !== null) {
|
|
829
|
+
head.x = header.bounds.x;
|
|
830
|
+
head.y = header.bounds.y;
|
|
831
|
+
head.width = header.bounds.width;
|
|
832
|
+
head.height = header.bounds.height;
|
|
819
833
|
}
|
|
820
834
|
if (header.fps !== undefined) head.fps = header.fps;
|
|
821
835
|
head.referenceScale = model.referenceScale;
|
package/src/ingest.ts
CHANGED
|
@@ -209,6 +209,18 @@ export interface IngestOptions {
|
|
|
209
209
|
* refuses it rather than answering it quietly.
|
|
210
210
|
*/
|
|
211
211
|
stage?: IngestStage;
|
|
212
|
+
/**
|
|
213
|
+
* The stage a rigc build's model document states (issue #907): its `stage`,
|
|
214
|
+
* or `null` for a rig that declared none. Given, it is read IN PLACE OF the
|
|
215
|
+
* header's `x`, `y`, `width`, `height` — which on a rigc build since #907 are
|
|
216
|
+
* the setup-pose bounding box, not the stage — and everything below reads it
|
|
217
|
+
* as it would read a header stating it (`--stage` beside it is the same
|
|
218
|
+
* refusal). Absent, the header is read, as for an export: an export carries
|
|
219
|
+
* no other box. The caller reads it — `src/` reads no files —
|
|
220
|
+
* (`documentStageBeside` in `src/cli/shared.ts`: the `rigc-compiled/3`
|
|
221
|
+
* document beside the skeleton, only when its digest is that skeleton's).
|
|
222
|
+
*/
|
|
223
|
+
documentStage?: IngestStage | null;
|
|
212
224
|
/** The source file's basename, for the provenance note. No path: no leak. */
|
|
213
225
|
source: string;
|
|
214
226
|
/** rigc's own version, for the provenance note. Passed in — `src/` reads no files. */
|
|
@@ -1149,9 +1161,9 @@ function spellStage(x: unknown, y: unknown, width: unknown, height: unknown): st
|
|
|
1149
1161
|
/**
|
|
1150
1162
|
* The rig spec's `skeleton` block — and the one judgement in this module.
|
|
1151
1163
|
*
|
|
1152
|
-
* 🚨 **A skeleton JSON need not carry
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1164
|
+
* 🚨 **A skeleton JSON need not carry a box, and a stage cannot be derived.**
|
|
1165
|
+
* The stage is the working area the art was painted in; no pose of the rig
|
|
1166
|
+
* states it. So a file that declares none is written as declaring none
|
|
1155
1167
|
* — `"width": null, "height": null`, the rig spec's spelling for that claim since
|
|
1156
1168
|
* issue #578 — and `compile` then emits a header with none of the four fields,
|
|
1157
1169
|
* which is the file that was read, byte for byte. No finding: nothing was lost,
|
|
@@ -1173,9 +1185,22 @@ function spellStage(x: unknown, y: unknown, width: unknown, height: unknown): st
|
|
|
1173
1185
|
* that shape; the blocker names the fields it does state.
|
|
1174
1186
|
*
|
|
1175
1187
|
* ⭐ It is still the judgement that costs least to get wrong. `diff` does report
|
|
1176
|
-
* the box — `
|
|
1177
|
-
* the `(reported)` block that no rung consults
|
|
1178
|
-
*
|
|
1188
|
+
* the header's box — `bounds_present` and `bounds_box` (issue #578, renamed by
|
|
1189
|
+
* #907) — but they sit in the `(reported)` block that no rung consults.
|
|
1190
|
+
*
|
|
1191
|
+
* 🔁 **What the box becomes (issue #907).** A header's box is its setup-pose
|
|
1192
|
+
* bounding box — what the format says the four are, and what every editor
|
|
1193
|
+
* export carries there. It is the only box a file has, so it becomes the
|
|
1194
|
+
* rebuild's stage (`A14` and `A19` measure against it, as they did against the
|
|
1195
|
+
* export's header). The rebuild's own header is not carried from it: `build`
|
|
1196
|
+
* computes the setup-pose bounding box of what it draws (`headerBoundsOf` in
|
|
1197
|
+
* `src/compile.ts`), which for a rebuild drawing the source's vertices is
|
|
1198
|
+
* spine-core's `getBounds` over the source on the header's 1e-6 grid at float32 — not the editor's
|
|
1199
|
+
* arithmetic, which on the twelve examples sits up to 0.0071 units away. A
|
|
1200
|
+
* rigc build's own header is likewise its bounding box, not its stage: the
|
|
1201
|
+
* stage is in `skeleton.model.json`, which the caller reads and hands in as
|
|
1202
|
+
* `documentStage` when that document digests this skeleton — and then it is
|
|
1203
|
+
* read in place of the header's box.
|
|
1179
1204
|
*
|
|
1180
1205
|
* 🔇 **Both of its silences were here, and both were around the DECLARED branch
|
|
1181
1206
|
* rather than the missing one.** That branch used to be a bare early return, so
|
|
@@ -1186,7 +1211,10 @@ function spellStage(x: unknown, y: unknown, width: unknown, height: unknown): st
|
|
|
1186
1211
|
* a decompiler that is right for a reason it never states is a decompiler nobody
|
|
1187
1212
|
* can check.
|
|
1188
1213
|
*/
|
|
1189
|
-
function ingestHeader(
|
|
1214
|
+
function ingestHeader(source: JsonObject, opts: IngestOptions, note: Note): JsonObject {
|
|
1215
|
+
// A rigc build's stage is its model document's (issue #907, `IngestOptions.documentStage`): read in place of the header's box.
|
|
1216
|
+
const head: JsonObject = opts.documentStage === undefined ? source : Object.fromEntries(Object.entries(source).filter(([key]) => !(STAGE_FIELDS as readonly string[]).includes(key)));
|
|
1217
|
+
if (opts.documentStage) Object.assign(head, opts.documentStage);
|
|
1190
1218
|
// 🚨 Before a line of transcription, because a header the caller contradicted
|
|
1191
1219
|
// is not a header to start writing a spec from (issue #626).
|
|
1192
1220
|
if (declaresStage(head) && opts.stage !== undefined) {
|
|
@@ -1243,9 +1271,10 @@ function ingestHeader(head: JsonObject, opts: IngestOptions, note: Note): JsonOb
|
|
|
1243
1271
|
`skeleton.${omitted.join('/')}`,
|
|
1244
1272
|
`the source declares a ${String(head.width)}x${String(head.height)} stage and omits ` +
|
|
1245
1273
|
`${omitted.map((field) => `"${field}"`).join(' and ')}; inside a declared extent an omitted origin is 0, ` +
|
|
1246
|
-
'which is
|
|
1274
|
+
'which is how `compile` and `diff` read it (#620), so the rig spec states x=' +
|
|
1247
1275
|
`${String(out.x)}, y=${String(out.y)} rather than leaving the rebuild to a default in another module. ` +
|
|
1248
|
-
`⚠️ The
|
|
1276
|
+
`⚠️ The rebuilt header WILL spell ${omitted.length > 1 ? 'those fields' : 'that field'}: it carries the ` +
|
|
1277
|
+
'setup-pose bounding box `build` computes (#907), whose origin is wherever the art sits',
|
|
1249
1278
|
);
|
|
1250
1279
|
}
|
|
1251
1280
|
return out;
|
package/src/model.ts
CHANGED
|
@@ -572,18 +572,21 @@ export interface CompiledAnimation {
|
|
|
572
572
|
}
|
|
573
573
|
|
|
574
574
|
/**
|
|
575
|
-
* The
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
* (issue #578: four fields or none). `null` where the rig declares no stage.
|
|
575
|
+
* The stage the skeleton declares (issue #1026): its origin `x`, `y` and its
|
|
576
|
+
* extent `width`, `height` — the rig spec's `skeleton.width`/`height` (or the
|
|
577
|
+
* manifest's crop), and its `x`/`y` or 0 beside them (issue #578: four fields
|
|
578
|
+
* or none). `null` where the rig declares no stage.
|
|
580
579
|
*
|
|
581
580
|
* ⭐ **Why the model holds it.** `render` and `check` say whether a candidate
|
|
582
581
|
* declares a stage (issue #714), and `A14`/`A19` read its box; until this field
|
|
583
582
|
* the header of `skeleton.json` was the only place the value was written, so a
|
|
584
|
-
* reader of the document had to open the Spine file beside it.
|
|
585
|
-
*
|
|
586
|
-
*
|
|
583
|
+
* reader of the document had to open the Spine file beside it.
|
|
584
|
+
*
|
|
585
|
+
* 🔁 **And since issue #907 this is the only place it is written.** The Spine
|
|
586
|
+
* emitter used to copy it into the header's `x`, `y`, `width`, `height`,
|
|
587
|
+
* which the format defines as the setup-pose bounding box; the header now
|
|
588
|
+
* carries that box (`headerBoundsOf` in `src/compile.ts`), and every reader of
|
|
589
|
+
* the stage — `A14`, `A19`, `explain` — reads it here.
|
|
587
590
|
*/
|
|
588
591
|
export interface ModelStage {
|
|
589
592
|
x: number;
|
|
@@ -626,8 +629,9 @@ export interface ModelEditorOrder {
|
|
|
626
629
|
|
|
627
630
|
export interface CompiledModel extends CarriedFromCompileResult {
|
|
628
631
|
/**
|
|
629
|
-
* The
|
|
630
|
-
*
|
|
632
|
+
* The stage the skeleton declares, or `null` (issue #1026, `ModelStage`).
|
|
633
|
+
* Not in the Spine header since issue #907, which carries the setup-pose
|
|
634
|
+
* bounding box there.
|
|
631
635
|
*/
|
|
632
636
|
stage: ModelStage | null;
|
|
633
637
|
/**
|
package/src/rig.ts
CHANGED
|
@@ -73,7 +73,14 @@ export const RIG_SPEC_VERSION = 'rigc-rig/1';
|
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* The
|
|
76
|
+
* The stage and the runtime hints, all optional.
|
|
77
|
+
*
|
|
78
|
+
* 🔁 The four box fields here are rigc's **stage** — the working area the art
|
|
79
|
+
* was painted in, the frame the coordinate transform and `A14`/`A19` read —
|
|
80
|
+
* and not the Spine header's box of the same names, which the format defines
|
|
81
|
+
* as the setup-pose bounding box. Since issue #907 `build` writes that box
|
|
82
|
+
* into the header, computed from the rig (`headerBoundsOf` in
|
|
83
|
+
* `src/compile.ts`), and states the stage in `skeleton.model.json`.
|
|
77
84
|
*
|
|
78
85
|
* `x`/`y` default to 0 and `width`/`height` fall back to the cut manifest's crop
|
|
79
86
|
* when there is one. With neither a manifest nor a declaration here the compile
|
|
@@ -85,7 +92,7 @@ export const RIG_SPEC_VERSION = 'rigc-rig/1';
|
|
|
85
92
|
* stage** (issue #578). Omitting them is silence and stays a refusal by name;
|
|
86
93
|
* stating them `null` is a claim, and the emitted header then carries none of
|
|
87
94
|
* `x`/`y`/`width`/`height` — which is what an editor export of a skeleton whose
|
|
88
|
-
*
|
|
95
|
+
* bounds were never set looks like, and what a transcriber of one has to be able
|
|
89
96
|
* to write down. `null` is this spec's spelling for a stated absence everywhere
|
|
90
97
|
* else it has one (`RigSlot.attachment` = "show nothing", the cut manifest's
|
|
91
98
|
* `image` = "this cut does not carry the part"), so it is the spelling here too
|
package/src/types.ts
CHANGED
|
@@ -1167,10 +1167,12 @@ export interface SpineSkeletonJson {
|
|
|
1167
1167
|
skeleton: {
|
|
1168
1168
|
spine: string;
|
|
1169
1169
|
/**
|
|
1170
|
-
* The setup-pose bounding box
|
|
1170
|
+
* The setup-pose bounding box — and since issue #907 that is what `build`
|
|
1171
|
+
* writes here, computed by rigc's core (`headerBoundsOf` in `compile.ts`);
|
|
1172
|
+
* it used to copy the stage. All four together or none of them: a rig spec
|
|
1171
1173
|
* that declares no stage (`skeleton.width`/`height` stated `null` — see
|
|
1172
1174
|
* `RigSkeletonHeader`) emits a header without any of them, which is what an
|
|
1173
|
-
* export of a skeleton whose
|
|
1175
|
+
* export of a skeleton whose bounds were never set carries (issue #578).
|
|
1174
1176
|
*
|
|
1175
1177
|
* ⚠️ Optional here because the *runtime* leaves them `undefined` when they
|
|
1176
1178
|
* are absent, not 0. `SkeletonData` declares `x = 0 … height = 0`
|
package/src/validate.ts
CHANGED
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
type SpineGeneration,
|
|
69
69
|
} from './generation.ts';
|
|
70
70
|
import { posedNumbersOf } from './render.ts';
|
|
71
|
+
import { MODEL_DOCUMENT_SPEC } from './model.ts';
|
|
71
72
|
import { BONE_INHERIT_KNOWN } from './rig.ts';
|
|
72
73
|
import {
|
|
73
74
|
CHANNELS_BY_KIND,
|
|
@@ -860,7 +861,7 @@ export function spineSkinEntries(data: ReturnType<SkeletonJson['readSkeletonData
|
|
|
860
861
|
* line would hide a difference (an order nothing failed on, a name no line
|
|
861
862
|
* printed); `validate()` itself builds them from its own round trip.
|
|
862
863
|
*/
|
|
863
|
-
export function runtimeFacts(skeletonText: string, atlasText: string): {
|
|
864
|
+
export function runtimeFacts(skeletonText: string, atlasText: string, modelText?: string): {
|
|
864
865
|
skinEntries: SkinEntryFacts;
|
|
865
866
|
atlasPages: AtlasPageFacts;
|
|
866
867
|
slotColour: SlotColourFacts;
|
|
@@ -890,7 +891,7 @@ export function runtimeFacts(skeletonText: string, atlasText: string): {
|
|
|
890
891
|
skinMembers: data,
|
|
891
892
|
regionJoins: spineRegionJoins(atlasText, raw),
|
|
892
893
|
atlasRegions: { atlas },
|
|
893
|
-
stage: spineStage(data),
|
|
894
|
+
stage: spineStage(data, modelText),
|
|
894
895
|
skeletonRoster: rawSkeletonRoster(raw),
|
|
895
896
|
boneTimelines: rawBoneTimelines(raw),
|
|
896
897
|
eventKeys: rawEventKeys(raw),
|
|
@@ -1231,9 +1232,42 @@ export function spineAnimatedBones(raw: Json | null): AnimatedBoneFacts {
|
|
|
1231
1232
|
};
|
|
1232
1233
|
}
|
|
1233
1234
|
|
|
1234
|
-
/**
|
|
1235
|
-
|
|
1236
|
-
|
|
1235
|
+
/**
|
|
1236
|
+
* The runtime's supply of `StageFacts` (issue #1025, cut 4c-1): the stage A14
|
|
1237
|
+
* and A19 measure against.
|
|
1238
|
+
*
|
|
1239
|
+
* 🔸 **A rigc build's stage is its model document's, not its header's**
|
|
1240
|
+
* (issue #907). Since then the header of a rigc build carries the setup-pose
|
|
1241
|
+
* bounding box — what the format says it is — and the stage, the working
|
|
1242
|
+
* area the art was painted in, is stated by the `rigc-compiled/3` document
|
|
1243
|
+
* written beside the pair (`stage`, issue #1026). So where the caller hands a
|
|
1244
|
+
* `/3` document (`modelText`), its `stage` is read: the two extents, or both
|
|
1245
|
+
* `undefined` where it states `null`. Everywhere else — an editor export, a
|
|
1246
|
+
* bare directory with no document, a `/2` or `/1` document, written when the
|
|
1247
|
+
* header still was the stage — it is the loaded skeleton's `width` and
|
|
1248
|
+
* `height`, as spine-core holds them (`undefined` where the header states
|
|
1249
|
+
* none), or both absent with no skeleton. An export carries no other box, so
|
|
1250
|
+
* on an export the rules measure against its bounding box, as they always
|
|
1251
|
+
* did.
|
|
1252
|
+
*/
|
|
1253
|
+
export function spineStage(data: ReturnType<SkeletonJson['readSkeletonData']> | null, modelText?: string): StageFacts {
|
|
1254
|
+
return documentStage(modelText) ?? { width: data?.width, height: data?.height };
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/** The stage a `rigc-compiled/3` document states, or `undefined` where there is no such document (`spineStage`). */
|
|
1258
|
+
function documentStage(modelText: string | undefined): StageFacts | undefined {
|
|
1259
|
+
if (modelText === undefined) return undefined;
|
|
1260
|
+
let doc: unknown;
|
|
1261
|
+
try {
|
|
1262
|
+
doc = JSON.parse(modelText);
|
|
1263
|
+
} catch {
|
|
1264
|
+
return undefined;
|
|
1265
|
+
}
|
|
1266
|
+
if (!isObj(doc) || doc.spec !== MODEL_DOCUMENT_SPEC || !('stage' in doc)) return undefined;
|
|
1267
|
+
const stage = doc.stage;
|
|
1268
|
+
if (stage === null) return { width: undefined, height: undefined };
|
|
1269
|
+
if (isObj(stage) && typeof stage.width === 'number' && typeof stage.height === 'number') return { width: stage.width, height: stage.height };
|
|
1270
|
+
return undefined;
|
|
1237
1271
|
}
|
|
1238
1272
|
|
|
1239
1273
|
/**
|
|
@@ -2556,7 +2590,7 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
2556
2590
|
// somebody else's canvas. A rig that declares no budget has nothing to be
|
|
2557
2591
|
// measured against, and the assertion says so instead of inventing a wall.
|
|
2558
2592
|
check('A13_MESH_BUDGET', () => a13MeshBudget(verdicts, loadedMeshFacts(), input));
|
|
2559
|
-
check('A14_NO_FULL_FRAME_MESH', () => a14NoFullFrameMesh(verdicts, loadedMeshFacts(), spineStage(data)));
|
|
2593
|
+
check('A14_NO_FULL_FRAME_MESH', () => a14NoFullFrameMesh(verdicts, loadedMeshFacts(), spineStage(data, input.modelText)));
|
|
2560
2594
|
|
|
2561
2595
|
// --- A15: idle must not key a mesh-driving bone (dirty-skip lever) -----
|
|
2562
2596
|
//
|
|
@@ -3200,7 +3234,7 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
3200
3234
|
// fires, and it now has to name the profile it belongs to as well as the one
|
|
3201
3235
|
// that does not ask — the reader opted in, and the message is where they find
|
|
3202
3236
|
// out what they opted into.
|
|
3203
|
-
check('A19_OVERLAY_PNGS_HAVE_ALPHA', () => a19OverlayPngsHaveAlpha(verdicts, { atlas }, spineStage(skeletonData), { regionAttachments }, input));
|
|
3237
|
+
check('A19_OVERLAY_PNGS_HAVE_ALPHA', () => a19OverlayPngsHaveAlpha(verdicts, { atlas }, spineStage(skeletonData, input.modelText), { regionAttachments }, input));
|
|
3204
3238
|
// Two regions on one page whose rectangles overlap and whose footprints do
|
|
3205
3239
|
// too — a mesh's hull, else the rectangle (issue #1099). The clause A06 held
|
|
3206
3240
|
// about two rectangles over the same texels, read over what each region
|