spine-rigc 0.30.0 → 0.32.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 +4 -4
- package/cli.ts +39 -1
- package/docs/AUTHORING.md +309 -48
- package/docs/INGEST.md +12 -10
- package/docs/SPEC_COVERAGE.md +6 -5
- package/package.json +1 -1
- package/src/atlas.ts +180 -0
- package/src/compile.ts +486 -21
- package/src/deformmeasure.ts +7 -2
- package/src/ingest.ts +254 -29
- package/src/motion.ts +120 -2
- package/src/rig.ts +179 -5
- package/src/timelines.ts +94 -6
- package/src/types.ts +106 -3
- package/src/validate.ts +618 -221
package/src/compile.ts
CHANGED
|
@@ -39,8 +39,9 @@ import { parseJsonWithPosition } from './json-position.ts';
|
|
|
39
39
|
// of it — the same search serves `refuseUnknownKeys`, and a second copy here with
|
|
40
40
|
// a threshold edited is how such a pair drifts apart.
|
|
41
41
|
import { nearMisses } from './keys.ts';
|
|
42
|
-
import { parseMotionSpec } from './motion.ts';
|
|
42
|
+
import { EVERY_GLOBAL_PHYSICS, parseMotionSpec } from './motion.ts';
|
|
43
43
|
import {
|
|
44
|
+
BONE_INHERIT_KNOWN,
|
|
44
45
|
constraintAt,
|
|
45
46
|
declaresNoStage,
|
|
46
47
|
parseRigSpec,
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
RIG_PATH_SPACING_MODES,
|
|
51
52
|
RIG_SCALE_Y_MODES,
|
|
52
53
|
RIG_SKIN_CONSTRAINT_KEYS,
|
|
54
|
+
resolveBoneInherit,
|
|
53
55
|
splitRigSkin,
|
|
54
56
|
type RigAttachment,
|
|
55
57
|
type RigBone,
|
|
@@ -61,6 +63,7 @@ import {
|
|
|
61
63
|
type RigMeshBinding,
|
|
62
64
|
type RigPathAttachment,
|
|
63
65
|
type RigRegionAttachment,
|
|
66
|
+
type RigSequence,
|
|
64
67
|
type RigSkinParts,
|
|
65
68
|
type RigSpec,
|
|
66
69
|
type RigVertexGeometry,
|
|
@@ -89,6 +92,8 @@ import { Plate, readPlate } from '../tools/plate.ts';
|
|
|
89
92
|
import {
|
|
90
93
|
extractRegion,
|
|
91
94
|
pageFootprint,
|
|
95
|
+
pageGridSaid,
|
|
96
|
+
pageGridSentence,
|
|
92
97
|
parseAtlasText,
|
|
93
98
|
rewritePageNames,
|
|
94
99
|
writeAtlasText,
|
|
@@ -128,6 +133,7 @@ import type {
|
|
|
128
133
|
MotionEventKey,
|
|
129
134
|
MotionIkTrack,
|
|
130
135
|
MotionMemberValues,
|
|
136
|
+
MotionSequenceTrack,
|
|
131
137
|
MotionSpec,
|
|
132
138
|
MotionTrack,
|
|
133
139
|
MotionTransformTrack,
|
|
@@ -144,6 +150,7 @@ import type {
|
|
|
144
150
|
SpineMeshAttachment,
|
|
145
151
|
SpinePathAttachment,
|
|
146
152
|
SpineRegionAttachment,
|
|
153
|
+
SpineSequence,
|
|
147
154
|
SpineSkeletonJson,
|
|
148
155
|
SpineSlot,
|
|
149
156
|
SpineTimelineKey,
|
|
@@ -310,8 +317,14 @@ export function editorNamesInOrder(names: readonly string[], collection: 'animat
|
|
|
310
317
|
return [...names].sort((a, b) => verdicts.get(a)?.get(b) ?? 0);
|
|
311
318
|
}
|
|
312
319
|
|
|
313
|
-
/**
|
|
314
|
-
|
|
320
|
+
/**
|
|
321
|
+
* The skin the editor keeps at index 0 whatever its name sorts as.
|
|
322
|
+
*
|
|
323
|
+
* Exported for `ingest`, which has to know that a contested placeholder the
|
|
324
|
+
* default skin fills is refused rather than named (`refuseDefaultSkinContest`),
|
|
325
|
+
* and has no business spelling the name a second time (issue #746).
|
|
326
|
+
*/
|
|
327
|
+
export const DEFAULT_SKIN = 'default';
|
|
315
328
|
|
|
316
329
|
/**
|
|
317
330
|
* The order the emitted `skins` array is written in: **`default` first, then the
|
|
@@ -864,6 +877,17 @@ interface ValueTrackShape {
|
|
|
864
877
|
fields: string[];
|
|
865
878
|
identity: number[];
|
|
866
879
|
bounds?: Array<PhysicsPoseRule | null>;
|
|
880
|
+
/**
|
|
881
|
+
* A timeline whose key holds one NAMED value instead of numbers — the JSON
|
|
882
|
+
* field it is written as, and the resolver that turns a spelling into the
|
|
883
|
+
* name the file carries (or `undefined` for one the runtime cannot resolve).
|
|
884
|
+
*
|
|
885
|
+
* ⚠️ Such a timeline is stepped by the format rather than by choice: its
|
|
886
|
+
* reader builds no curve at all, so an `ease` or a `curve` on its key would be
|
|
887
|
+
* accepted by the parser and never read. `compileValueTrack` refuses both.
|
|
888
|
+
* One entry has it — the bone's `inherit` (issue #733).
|
|
889
|
+
*/
|
|
890
|
+
named?: { field: string; resolve: (value: unknown) => string | undefined; known: string };
|
|
867
891
|
}
|
|
868
892
|
|
|
869
893
|
/**
|
|
@@ -898,6 +922,12 @@ const BONE_TRACKS: Record<string, ValueTrackShape> = {
|
|
|
898
922
|
shearx: { fields: ['value'], identity: [0] },
|
|
899
923
|
sheary: { fields: ['value'], identity: [0] },
|
|
900
924
|
rotate: { fields: ['value'], identity: [0] },
|
|
925
|
+
// The eleventh case of the runtime's bone switch, and the one that keys no
|
|
926
|
+
// number: `InheritTimeline` sets `pose.inherit` at the key and
|
|
927
|
+
// `updateWorldTransform` reads it that frame. Resolved by the SAME call the
|
|
928
|
+
// setup field is (`resolveBoneInherit`), so the five a key may name are the
|
|
929
|
+
// five a bone may rest in and a refusal prints the one list (issue #733).
|
|
930
|
+
inherit: { fields: [], identity: [], named: { field: 'inherit', resolve: resolveBoneInherit, known: BONE_INHERIT_KNOWN } },
|
|
901
931
|
};
|
|
902
932
|
|
|
903
933
|
/**
|
|
@@ -1505,6 +1535,11 @@ interface AtlasSource {
|
|
|
1505
1535
|
parsed: ParsedAtlas;
|
|
1506
1536
|
/** Trimmed region name -> the region, first occurrence wins (as `findRegion` does). */
|
|
1507
1537
|
byName: Map<string, { region: AtlasRegion; page: ParsedAtlas['pages'][number] }>;
|
|
1538
|
+
/**
|
|
1539
|
+
* Page name -> what its file is against what the atlas declares, for every
|
|
1540
|
+
* page on disk whose PNG is not the declared size, in page order (issue #750).
|
|
1541
|
+
*/
|
|
1542
|
+
grids: Map<string, NonNullable<CompiledImage['pageGrid']>>;
|
|
1508
1543
|
}
|
|
1509
1544
|
|
|
1510
1545
|
function readAtlasIn(path: string): AtlasSource {
|
|
@@ -1527,12 +1562,28 @@ function readAtlasIn(path: string): AtlasSource {
|
|
|
1527
1562
|
// one sentence, rather than one region at a time or at `A06` after the whole
|
|
1528
1563
|
// compile. A page that is not on disk is left to the region that sits on it,
|
|
1529
1564
|
// which already names it; saying so twice would be two refusals of one fact.
|
|
1565
|
+
//
|
|
1566
|
+
// 📐 **What each page MEASURES, read in the same pass and not refused here**
|
|
1567
|
+
// (issue #750). A page whose PNG is not the size the atlas declares is a
|
|
1568
|
+
// file every runtime still draws — the region mapping is a fraction of the
|
|
1569
|
+
// declared size — and the only readers it breaks are the ones that address
|
|
1570
|
+
// it in texels. Refusing it here would refuse every pack that has one,
|
|
1571
|
+
// including a pack of region attachments nothing reads a texel of, and on
|
|
1572
|
+
// `build` it would replace `A06`'s verdict with a compile error for the same
|
|
1573
|
+
// fact: `A06` already refuses the pack, names the ratio and prints the
|
|
1574
|
+
// `scale:` repair, before anything is written. So the page is RECORDED, as
|
|
1575
|
+
// `pageGridSentence` states it, and the readers that would take a figure off
|
|
1576
|
+
// its texels decline to on the part that asks (`partTexels` below).
|
|
1530
1577
|
const unread: string[] = [];
|
|
1578
|
+
const grids: AtlasSource['grids'] = new Map();
|
|
1531
1579
|
for (const page of parsed.pages) {
|
|
1532
1580
|
const abs = resolve(dirname(path), page.name);
|
|
1533
1581
|
if (!existsSync(abs)) continue;
|
|
1534
|
-
const { problem } = readPngHeader(abs);
|
|
1582
|
+
const { info, problem } = readPngHeader(abs);
|
|
1535
1583
|
if (problem !== null) unread.push(`page "${page.name}": ${problem}`);
|
|
1584
|
+
if (info === null) continue;
|
|
1585
|
+
const sentence = pageGridSentence(page, info, page.regions);
|
|
1586
|
+
if (sentence !== null) grids.set(page.name, { said: pageGridSaid(page, info), sentence });
|
|
1536
1587
|
}
|
|
1537
1588
|
if (unread.length > 0) {
|
|
1538
1589
|
throw new CompileError(
|
|
@@ -1540,7 +1591,7 @@ function readAtlasIn(path: string): AtlasSource {
|
|
|
1540
1591
|
`nothing was compiled against the pack — ${unread.join('; ')}`,
|
|
1541
1592
|
);
|
|
1542
1593
|
}
|
|
1543
|
-
return { path, dir: dirname(path), parsed, byName };
|
|
1594
|
+
return { path, dir: dirname(path), parsed, byName, grids };
|
|
1544
1595
|
}
|
|
1545
1596
|
|
|
1546
1597
|
/**
|
|
@@ -1634,6 +1685,7 @@ function resolveFromAtlas(
|
|
|
1634
1685
|
const info = readPngInfo(absPath);
|
|
1635
1686
|
const page = relative(outDir, absPath).split('\\').join('/');
|
|
1636
1687
|
const scale = found.page.scale;
|
|
1688
|
+
const grid = atlas.grids.get(found.page.name);
|
|
1637
1689
|
return {
|
|
1638
1690
|
region,
|
|
1639
1691
|
page,
|
|
@@ -1647,6 +1699,7 @@ function resolveFromAtlas(
|
|
|
1647
1699
|
isBase,
|
|
1648
1700
|
atlas: found.region,
|
|
1649
1701
|
...(scale === 1 ? {} : { atlasScale: scale }),
|
|
1702
|
+
...(grid === undefined ? {} : { pageGrid: grid }),
|
|
1650
1703
|
};
|
|
1651
1704
|
}
|
|
1652
1705
|
|
|
@@ -1827,8 +1880,17 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
1827
1880
|
/** The pre-packed atlas `--atlas-in` named, parsed once, or null. */
|
|
1828
1881
|
const atlasIn = opts.atlasInPath === undefined ? null : readAtlasIn(resolve(opts.atlasInPath));
|
|
1829
1882
|
|
|
1830
|
-
const addImage = (
|
|
1831
|
-
|
|
1883
|
+
const addImage = (
|
|
1884
|
+
relPath: string,
|
|
1885
|
+
baseDir: string,
|
|
1886
|
+
isBase: boolean,
|
|
1887
|
+
region: string = basename(relPath, '.png'),
|
|
1888
|
+
/**
|
|
1889
|
+
* The directory this part counts as named in, for `skeletonImagesPath`, or
|
|
1890
|
+
* `null` for none. Default: the file's own directory, as it always was.
|
|
1891
|
+
*/
|
|
1892
|
+
partDir: string | null = dirname(resolve(baseDir, relPath)),
|
|
1893
|
+
): CompiledImage => {
|
|
1832
1894
|
if (seenRegions.has(region)) {
|
|
1833
1895
|
throw new CompileError(`duplicate region name "${region}" (${relPath})`);
|
|
1834
1896
|
}
|
|
@@ -1839,7 +1901,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
1839
1901
|
: resolveFromAtlas(relPath, region, isBase, outDir, atlasIn);
|
|
1840
1902
|
seenRegions.add(region);
|
|
1841
1903
|
regionSource.set(region, loosePath);
|
|
1842
|
-
partDirs.add(
|
|
1904
|
+
if (partDir !== null) partDirs.add(partDir);
|
|
1843
1905
|
images.push(img);
|
|
1844
1906
|
return img;
|
|
1845
1907
|
};
|
|
@@ -1885,6 +1947,63 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
1885
1947
|
);
|
|
1886
1948
|
};
|
|
1887
1949
|
|
|
1950
|
+
/**
|
|
1951
|
+
* Measure and atlas every frame of one attachment's `sequence` — by NAME, the
|
|
1952
|
+
* way the loader will ask for them (issue #729).
|
|
1953
|
+
*
|
|
1954
|
+
* Frame `i` is the region `sequenceFrameRegion(stem, …, i)`; on the loose
|
|
1955
|
+
* route its pixels are the PNG of that name under the images directory, and
|
|
1956
|
+
* under `--atlas-in` they are the pack's region of that name. A frame that is
|
|
1957
|
+
* not there is refused with its number and the name looked for: the loader's
|
|
1958
|
+
* own miss is `Region not found in atlas: <name> (attachment: <name>)`, which
|
|
1959
|
+
* says neither that the region was frame 3 of a series nor which series.
|
|
1960
|
+
*
|
|
1961
|
+
* ⚠️ The region is the frame's FULL name, not a basename — an editor names a
|
|
1962
|
+
* series in an images subfolder `fx/flame_0001`, and that string is what the
|
|
1963
|
+
* loader looks up. A frame another attachment already atlased from the same
|
|
1964
|
+
* file is the same region on purpose, as `addSkinImage` allows for one PNG.
|
|
1965
|
+
*/
|
|
1966
|
+
const addSequenceFrames = (stem: string, seq: RigSequence, where: string): void => {
|
|
1967
|
+
for (let i = 0; i < seq.count; i++) {
|
|
1968
|
+
const region = sequenceFrameRegion(stem, seq, i);
|
|
1969
|
+
const relPath = `${region}.png`;
|
|
1970
|
+
const wanted = resolve(imagesDir, relPath);
|
|
1971
|
+
const frame = `frame ${i} of ${seq.count} (number ${(seq.start ?? 1) + i})`;
|
|
1972
|
+
if (atlasIn === null && !existsSync(wanted)) {
|
|
1973
|
+
throw new CompileError(
|
|
1974
|
+
`${where}: sequence ${frame} is the region "${region}", and there is no PNG for it at ${wanted}. A ` +
|
|
1975
|
+
`sequence's frames are the regions "<stem><start + i>", zero-padded to "digits" — here stem ` +
|
|
1976
|
+
`${JSON.stringify(stem)}, start ${seq.start ?? 1}, digits ${seq.digits ?? 0} — and the compiler draws ` +
|
|
1977
|
+
'no frame in place of another. Add the file, or state the "count" the series really has.',
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
if (atlasIn !== null && !atlasIn.byName.has(region)) {
|
|
1981
|
+
const near = nearMisses(region, atlasIn.byName.keys());
|
|
1982
|
+
throw new CompileError(
|
|
1983
|
+
`${where}: sequence ${frame} is the region "${region}", which the atlas at ${atlasIn.path} does not have. ` +
|
|
1984
|
+
(near.length ? `Did you mean ${near.map((n) => JSON.stringify(n)).join(', ')}? ` : '') +
|
|
1985
|
+
`A sequence's frames are the regions "<stem><start + i>", zero-padded to "digits" — here stem ` +
|
|
1986
|
+
`${JSON.stringify(stem)}, start ${seq.start ?? 1}, digits ${seq.digits ?? 0} — and the compiler draws ` +
|
|
1987
|
+
'no frame in place of another.',
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
const already = regionSource.get(region);
|
|
1991
|
+
if (already === wanted) continue;
|
|
1992
|
+
if (already !== undefined) {
|
|
1993
|
+
throw new CompileError(
|
|
1994
|
+
`${where}: sequence ${frame} is the region "${region}", and the art already atlased under that name is ` +
|
|
1995
|
+
`${already}, not ${wanted}. One region name is one set of pixels, so this frame would draw the other ` +
|
|
1996
|
+
"file's. Rename one of them.",
|
|
1997
|
+
);
|
|
1998
|
+
}
|
|
1999
|
+
// The directory the frame NAMES are relative to — the images root, not
|
|
2000
|
+
// the frame's own subfolder, because the region name carries the folder —
|
|
2001
|
+
// and only on the loose route: under `--atlas-in` the spec names no file
|
|
2002
|
+
// for a frame, where an `image` is still a file the spec names.
|
|
2003
|
+
addImage(relPath, imagesDir, false, region, atlasIn === null ? imagesDir : null);
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
|
|
1888
2007
|
// A manifest may name a part the cut does not carry. A formation can declare
|
|
1889
2008
|
// more slots than any one cut fills, and a cut that shares a sprite with the
|
|
1890
2009
|
// scene around it has no plate of its own to point at — the manifest then
|
|
@@ -2072,6 +2191,19 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2072
2191
|
if (typeof image === 'string') {
|
|
2073
2192
|
addSkinImage(image, `skin "${skinName}" slot "${slotName}" attachment "${placeholder}"`);
|
|
2074
2193
|
}
|
|
2194
|
+
// `parseRigSpec` has refused a sequence on any kind but these three and
|
|
2195
|
+
// proved its shape, so what is left is whether its frames exist. The stem
|
|
2196
|
+
// is the region `path` the attachment resolves through, which with no
|
|
2197
|
+
// `path` stated is the placeholder (`nameSkinAttachment` pins exactly that
|
|
2198
|
+
// where a placeholder is contested).
|
|
2199
|
+
const sequence = (att as RigRegionAttachment).sequence;
|
|
2200
|
+
if (sequence !== undefined) {
|
|
2201
|
+
addSequenceFrames(
|
|
2202
|
+
(att as RigRegionAttachment).path ?? placeholder,
|
|
2203
|
+
sequence,
|
|
2204
|
+
`skin "${skinName}" slot "${slotName}" attachment "${placeholder}"`,
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2075
2207
|
}
|
|
2076
2208
|
rigAttachmentNames.set(slotName, names);
|
|
2077
2209
|
}
|
|
@@ -2395,6 +2527,16 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2395
2527
|
/** kind -> the names it declares, for the "the rig declares: …" half of one. */
|
|
2396
2528
|
const constraintNamesOfKind = new Map<string, string[]>();
|
|
2397
2529
|
const declareConstraint = (name: string, type: string): void => {
|
|
2530
|
+
// Reserved, because a track's `"physics": "*"` is the timeline that names no
|
|
2531
|
+
// constraint (issue #726): a physics constraint of that name is one no track
|
|
2532
|
+
// could key by name without meaning every global one instead.
|
|
2533
|
+
if (type === 'physics' && name === EVERY_GLOBAL_PHYSICS) {
|
|
2534
|
+
throw new CompileError(
|
|
2535
|
+
`physics constraint "${EVERY_GLOBAL_PHYSICS}": the name is reserved — a motion spec's \`"physics": ` +
|
|
2536
|
+
`"${EVERY_GLOBAL_PHYSICS}"\` is the timeline that names no constraint and drives every physics constraint ` +
|
|
2537
|
+
'declaring the keyed property global, so a constraint called that could not be keyed by name. Rename it',
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2398
2540
|
constraintDeclared.add(constraintAt(type, name));
|
|
2399
2541
|
constraintKinds.set(name, [...(constraintKinds.get(name) ?? []), type]);
|
|
2400
2542
|
constraintNamesOfKind.set(type, [...(constraintNamesOfKind.get(type) ?? []), name]);
|
|
@@ -2568,7 +2710,40 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2568
2710
|
const perMember = resolveMemberTrack(track, animName, targets, bones, trackDerivations);
|
|
2569
2711
|
targets.forEach((target, index) => {
|
|
2570
2712
|
const resolved = perMember.get(target)!;
|
|
2571
|
-
|
|
2713
|
+
// The timeline that names no constraint (issue #726). Resolved against
|
|
2714
|
+
// the constraints' own `…Global` flags rather than a name, and refused
|
|
2715
|
+
// when it reaches none — the runtime would walk every physics
|
|
2716
|
+
// constraint, skip each one, and the file would parse and do nothing.
|
|
2717
|
+
const everyGlobal = family === 'physics' && target === EVERY_GLOBAL_PHYSICS;
|
|
2718
|
+
if (everyGlobal && track.physics === undefined) {
|
|
2719
|
+
throw new CompileError(
|
|
2720
|
+
`animation "${animName}": group "${String(track.group)}" lists "${EVERY_GLOBAL_PHYSICS}", which is not a ` +
|
|
2721
|
+
`constraint but the target that names none — write it as the track's \`"physics": "${EVERY_GLOBAL_PHYSICS}"\``,
|
|
2722
|
+
);
|
|
2723
|
+
}
|
|
2724
|
+
if (everyGlobal) {
|
|
2725
|
+
const physics = constraints.filter((one) => one.type === 'physics');
|
|
2726
|
+
const flag = `${track.property}Global`;
|
|
2727
|
+
const reached =
|
|
2728
|
+
track.property === 'reset'
|
|
2729
|
+
? physics
|
|
2730
|
+
: physics.filter((one) => one[flag] === true);
|
|
2731
|
+
if (reached.length === 0) {
|
|
2732
|
+
throw new CompileError(
|
|
2733
|
+
`animation "${animName}" keys physics "${EVERY_GLOBAL_PHYSICS}" ${track.property}, the timeline that ` +
|
|
2734
|
+
`names no constraint and drives every physics constraint ` +
|
|
2735
|
+
(track.property === 'reset' ? 'the rig has' : `declaring "${flag}": true`) +
|
|
2736
|
+
', and ' +
|
|
2737
|
+
(physics.length === 0
|
|
2738
|
+
? 'the rig declares no physics constraint'
|
|
2739
|
+
: `none of ${physics.map((one) => `"${one.name}"`).join(', ')} does`) +
|
|
2740
|
+
' — it would parse and move nothing. ' +
|
|
2741
|
+
(track.property === 'reset'
|
|
2742
|
+
? 'Declare a physics constraint, or remove the track'
|
|
2743
|
+
: `Set "${flag}": true on the constraints it is for, or key one by name`),
|
|
2744
|
+
);
|
|
2745
|
+
}
|
|
2746
|
+
} else if (family !== null) {
|
|
2572
2747
|
const label = CONSTRAINT_TRACK_FAMILIES[family].label;
|
|
2573
2748
|
// Resolved by name AND by type in the parser
|
|
2574
2749
|
// (`findConstraint(name, PathConstraintData)`), which returns null on
|
|
@@ -2650,7 +2825,8 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2650
2825
|
? compileValueTrack(resolved, motion, animName, anim.duration, target, shift, BONE_TRACKS, 'bone')
|
|
2651
2826
|
: compileTrack(resolved, motion, animName, anim.duration, target, shift, attachmentIndex, darkSlots);
|
|
2652
2827
|
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time as number);
|
|
2653
|
-
|
|
2828
|
+
// Under the empty name, which is the file's own spelling of it.
|
|
2829
|
+
if (family !== null) (familyTimelines[family][everyGlobal ? '' : target] ??= {})[track.property] = keys;
|
|
2654
2830
|
else if (isBoneTrack) (boneTimelines[target] ??= {})[track.property] = keys;
|
|
2655
2831
|
else (slotTimelines[target] ??= {})[track.property] = keys;
|
|
2656
2832
|
});
|
|
@@ -2759,6 +2935,43 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2759
2935
|
((deformTimelines[skinName] ??= {})[track.slot] ??= {})[track.attachment] = { deform: keys };
|
|
2760
2936
|
}
|
|
2761
2937
|
|
|
2938
|
+
// -- sequence timelines: the other attachment timeline (issue #729) -----
|
|
2939
|
+
// Same triple, same table: `readAnimation` reads both names out of one
|
|
2940
|
+
// attachment map, and one attachment may carry both. `deform` is written
|
|
2941
|
+
// first where both exist, which is the order the parser tests them in.
|
|
2942
|
+
const sequenceTracks: MotionSequenceTrack[] = anim.sequence ?? [];
|
|
2943
|
+
for (const track of sequenceTracks) {
|
|
2944
|
+
const skinName = track.skin ?? 'default';
|
|
2945
|
+
const at = `animation "${animName}" sequence ${skinName}/${String(track.slot)}/${String(track.attachment)}`;
|
|
2946
|
+
const table = skinTables.get(skinName);
|
|
2947
|
+
if (!table) {
|
|
2948
|
+
throw new CompileError(
|
|
2949
|
+
`${at}: this rig emits no skin called "${skinName}" (it emits: ${[...skinTables.keys()].join(', ')})`,
|
|
2950
|
+
);
|
|
2951
|
+
}
|
|
2952
|
+
const perSlot = table[track.slot];
|
|
2953
|
+
if (!perSlot) {
|
|
2954
|
+
throw new CompileError(
|
|
2955
|
+
`${at}: skin "${skinName}" gives slot "${String(track.slot)}" no attachments` +
|
|
2956
|
+
(slotNames.has(track.slot) ? '' : ', and this rig does not declare that slot at all'),
|
|
2957
|
+
);
|
|
2958
|
+
}
|
|
2959
|
+
const attachment = perSlot[track.attachment];
|
|
2960
|
+
if (!attachment) {
|
|
2961
|
+
throw new CompileError(
|
|
2962
|
+
`${at}: slot "${track.slot}" in skin "${skinName}" has no attachment "${String(track.attachment)}" ` +
|
|
2963
|
+
`(it has: ${Object.keys(perSlot).join(', ')})`,
|
|
2964
|
+
);
|
|
2965
|
+
}
|
|
2966
|
+
const keys = compileSequenceTrack(track, anim.duration, attachment, at);
|
|
2967
|
+
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time as number);
|
|
2968
|
+
const slot = ((deformTimelines[skinName] ??= {})[track.slot] ??= {});
|
|
2969
|
+
if (slot[track.attachment]?.sequence) {
|
|
2970
|
+
throw new CompileError(`${at}: two sequence timelines on one attachment; merge them into one`);
|
|
2971
|
+
}
|
|
2972
|
+
slot[track.attachment] = { ...(slot[track.attachment] ?? {}), sequence: keys };
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2762
2975
|
const drawOrder = anim.drawOrder ? compileDrawOrder(anim.drawOrder, animName, anim.duration, slots) : null;
|
|
2763
2976
|
if (drawOrder) for (const key of drawOrder) compiledDuration = Math.max(compiledDuration, key.time as number);
|
|
2764
2977
|
|
|
@@ -2887,6 +3100,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2887
3100
|
skeletonText: `${JSON.stringify(skeleton, null, 2)}\n`,
|
|
2888
3101
|
atlasText,
|
|
2889
3102
|
images,
|
|
3103
|
+
pageGrids: atlasIn === null ? [] : [...atlasIn.grids].map(([page, grid]) => ({ page, said: grid.said })),
|
|
2890
3104
|
droppedStates,
|
|
2891
3105
|
absentParts,
|
|
2892
3106
|
declaredDurations,
|
|
@@ -3060,8 +3274,14 @@ function skinAttachmentName(skinName: string, placeholder: string): string {
|
|
|
3060
3274
|
* By the time either caller runs, a contested placeholder the **default** skin
|
|
3061
3275
|
* fills has already been refused — see `refuseDefaultSkinContest` — so every
|
|
3062
3276
|
* entry this composes for is a named skin's.
|
|
3277
|
+
*
|
|
3278
|
+
* 🔒 A third caller reads it from outside: `ingest` compares the name a source
|
|
3279
|
+
* states against the one this returns, and reports the rename where the two
|
|
3280
|
+
* differ (issue #746). It calls this rather than `skinAttachmentName` so that
|
|
3281
|
+
* WHETHER a name is composed is read off the same line as WHAT it is — the
|
|
3282
|
+
* separator and the contest test are each stated once, here.
|
|
3063
3283
|
*/
|
|
3064
|
-
function composeSkinAttachmentName(skinName: string, placeholder: string, contested: boolean): string | null {
|
|
3284
|
+
export function composeSkinAttachmentName(skinName: string, placeholder: string, contested: boolean): string | null {
|
|
3065
3285
|
return contested ? skinAttachmentName(skinName, placeholder) : null;
|
|
3066
3286
|
}
|
|
3067
3287
|
|
|
@@ -3867,12 +4087,120 @@ function attachmentPath(att: { path?: string; image?: string }, placeholder: str
|
|
|
3867
4087
|
return region === placeholder ? undefined : region;
|
|
3868
4088
|
}
|
|
3869
4089
|
|
|
4090
|
+
/**
|
|
4091
|
+
* The atlas region frame `i` of a sequence resolves to — `Sequence.getPath`
|
|
4092
|
+
* (`Sequence.js:124-132`) transcribed: the stem, then `start + i` left-padded
|
|
4093
|
+
* with zeros to `digits`. `start` and `digits` take the parser's own defaults
|
|
4094
|
+
* (`readSequence`: 1 and 0), which are the format's, not a guess.
|
|
4095
|
+
*
|
|
4096
|
+
* ⚠️ A second transcription of the rule exists in `validate.ts`
|
|
4097
|
+
* (`attachmentRegionLookups`, the walk `A08` joins the atlas with), and that is
|
|
4098
|
+
* deliberate: this module links no runtime, and the two are held to the loader
|
|
4099
|
+
* independently — the one by `A08`, both by `A00`'s round trip, which asks the
|
|
4100
|
+
* runtime's own `getPath` for every frame.
|
|
4101
|
+
*/
|
|
4102
|
+
function sequenceFrameRegion(stem: string, seq: RigSequence, i: number): string {
|
|
4103
|
+
const frame = String((seq.start ?? 1) + i);
|
|
4104
|
+
return `${stem}${'0'.repeat(Math.max(0, (seq.digits ?? 0) - frame.length))}${frame}`;
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
/** The emitted `sequence` block: the four fields exactly as the spec stated them. */
|
|
4108
|
+
function emitSequence(seq: RigSequence): SpineSequence {
|
|
4109
|
+
const out: SpineSequence = { count: seq.count };
|
|
4110
|
+
if (seq.start !== undefined) out.start = seq.start;
|
|
4111
|
+
if (seq.digits !== undefined) out.digits = seq.digits;
|
|
4112
|
+
if (seq.setup !== undefined) out.setup = seq.setup;
|
|
4113
|
+
return out;
|
|
4114
|
+
}
|
|
4115
|
+
|
|
4116
|
+
/**
|
|
4117
|
+
* What a sequence attachment's frames measure, for the `width`/`height` a spec
|
|
4118
|
+
* leaves out — or the refusal when they do not agree.
|
|
4119
|
+
*
|
|
4120
|
+
* An attachment has ONE size and every frame is mapped into it
|
|
4121
|
+
* (`RegionAttachment.computeUVs(regions[i], … width, height …)` for each `i` in
|
|
4122
|
+
* `Sequence.update`), so an omitted size is derivable only when every frame
|
|
4123
|
+
* measures the same: that is the one number the frames state. Frames of
|
|
4124
|
+
* different sizes state several, and picking one — the first, the setup frame,
|
|
4125
|
+
* the largest — would be the compiler choosing a value the spec did not.
|
|
4126
|
+
* Under `--atlas-in` a stated size that disagrees with a frame is refused as it
|
|
4127
|
+
* is for a single region, for the same reason: the pack's rectangle is fixed.
|
|
4128
|
+
*/
|
|
4129
|
+
function sequenceFrameSize(
|
|
4130
|
+
frames: readonly CompiledImage[],
|
|
4131
|
+
stated: { width?: number; height?: number },
|
|
4132
|
+
where: string,
|
|
4133
|
+
): { width?: number; height?: number } {
|
|
4134
|
+
const out: { width?: number; height?: number } = {};
|
|
4135
|
+
for (const field of ['width', 'height'] as const) {
|
|
4136
|
+
const sizes = [...new Set(frames.map((img) => img[field]))];
|
|
4137
|
+
if (stated[field] !== undefined) {
|
|
4138
|
+
const packed = frames.find((img) => img.atlas !== undefined && img[field] !== stated[field]);
|
|
4139
|
+
if (packed !== undefined) {
|
|
4140
|
+
throw new CompileError(
|
|
4141
|
+
`${where}: the spec says ${field} ${stated[field]} and sequence frame "${packed.region}" of the imported ` +
|
|
4142
|
+
`atlas is ${packed[field]}; a packed frame's rectangle is fixed, so the two would produce a quad the ` +
|
|
4143
|
+
'pack cannot fill',
|
|
4144
|
+
);
|
|
4145
|
+
}
|
|
4146
|
+
out[field] = stated[field];
|
|
4147
|
+
} else if (sizes.length === 1) {
|
|
4148
|
+
out[field] = sizes[0];
|
|
4149
|
+
} else {
|
|
4150
|
+
throw new CompileError(
|
|
4151
|
+
`${where}: the ${frames.length} frames of this sequence measure ${sizes.join(', ')} in ${field} ` +
|
|
4152
|
+
`(${frames.map((img) => `"${img.region}" ${img[field]}`).join(', ')}), and an attachment has one ` +
|
|
4153
|
+
`${field} every frame is drawn into. State "${field}" — the frames do not agree on one`,
|
|
4154
|
+
);
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
return out;
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
/** Every frame of `att`'s sequence, already atlased by the gather pass. */
|
|
4161
|
+
function sequenceFrames(att: { path?: string; sequence?: RigSequence }, placeholder: string, where: string, ctx: AttachmentContext): CompiledImage[] {
|
|
4162
|
+
const seq = att.sequence!;
|
|
4163
|
+
const stem = att.path ?? placeholder;
|
|
4164
|
+
const frames: CompiledImage[] = [];
|
|
4165
|
+
for (let i = 0; i < seq.count; i++) {
|
|
4166
|
+
// By the frame's FULL region name — `atlasedImage` takes a basename, and a
|
|
4167
|
+
// series in an images subfolder is named `fx/flame_0001`.
|
|
4168
|
+
const region = sequenceFrameRegion(stem, seq, i);
|
|
4169
|
+
const img = ctx.images.find((im) => im.region === region);
|
|
4170
|
+
if (img === undefined) {
|
|
4171
|
+
// Unreachable while the gather pass atlases every frame of every sequence
|
|
4172
|
+
// or refuses the missing one by name; stated so that a gather that stops
|
|
4173
|
+
// doing so is a sentence rather than a quad with no region.
|
|
4174
|
+
throw new CompileError(
|
|
4175
|
+
`${where}: sequence frame ${i} is the region "${region}", which was never added to the atlas — every frame ` +
|
|
4176
|
+
'of a sequence is atlased or refused by name before any attachment is built, so reaching this means rigc ' +
|
|
4177
|
+
'skipped one',
|
|
4178
|
+
);
|
|
4179
|
+
}
|
|
4180
|
+
frames.push(img);
|
|
4181
|
+
}
|
|
4182
|
+
return frames;
|
|
4183
|
+
}
|
|
4184
|
+
|
|
3870
4185
|
function buildRigRegion(
|
|
3871
4186
|
att: RigRegionAttachment,
|
|
3872
4187
|
placeholder: string,
|
|
3873
4188
|
where: string,
|
|
3874
4189
|
ctx: AttachmentContext,
|
|
3875
4190
|
): SpineRegionAttachment {
|
|
4191
|
+
if (att.sequence !== undefined) {
|
|
4192
|
+
const size = sequenceFrameSize(sequenceFrames(att, placeholder, where, ctx), att, where);
|
|
4193
|
+
const out: SpineRegionAttachment = { width: r6(size.width!), height: r6(size.height!) };
|
|
4194
|
+
if (att.path !== undefined) out.path = att.path;
|
|
4195
|
+
if (att.x !== undefined) out.x = r6(att.x);
|
|
4196
|
+
if (att.y !== undefined) out.y = r6(att.y);
|
|
4197
|
+
if (att.rotation !== undefined) out.rotation = r6(att.rotation);
|
|
4198
|
+
if (att.scaleX !== undefined) out.scaleX = r6(att.scaleX);
|
|
4199
|
+
if (att.scaleY !== undefined) out.scaleY = r6(att.scaleY);
|
|
4200
|
+
if (att.color !== undefined) out.color = att.color;
|
|
4201
|
+
out.sequence = emitSequence(att.sequence);
|
|
4202
|
+
return out;
|
|
4203
|
+
}
|
|
3876
4204
|
const img = att.image === undefined ? null : atlasedImage(att.image, where, ctx);
|
|
3877
4205
|
// ⭐ An IMPORTED region's size is not a default the spec may override. On the
|
|
3878
4206
|
// loose path `att.width` and the PNG's width are two legitimate numbers — "draw
|
|
@@ -3986,11 +4314,20 @@ function encodeNamedWeights(weights: RigMeshBinding[][], where: string, ctx: Att
|
|
|
3986
4314
|
* what `extractRegion` does, so there is no unmeasurable case left for this
|
|
3987
4315
|
* function to report and no catch here to keep reachable.
|
|
3988
4316
|
*/
|
|
3989
|
-
function measureAuthoredFit(
|
|
4317
|
+
function measureAuthoredFit(
|
|
4318
|
+
att: RigMeshAttachment,
|
|
4319
|
+
ctx: AttachmentContext,
|
|
4320
|
+
): MeshFitReport | { withheld: string } | null {
|
|
3990
4321
|
if (att.image === undefined || att.uvs === undefined || att.triangles === undefined) return null;
|
|
3991
4322
|
const region = basename(att.image, '.png');
|
|
3992
4323
|
const img = ctx.images.find((im) => im.region === region);
|
|
3993
4324
|
if (!img) return null;
|
|
4325
|
+
// A third case, and neither of the two above (issue #750): there IS art to
|
|
4326
|
+
// compare against, and its texels cannot be located, because its page's file
|
|
4327
|
+
// is not the size the atlas declares. Withheld and said, rather than measured
|
|
4328
|
+
// over whatever the declared coordinates land on — on a half-resolution pack
|
|
4329
|
+
// that read 68.49% where the page it was packed from reads 100.00%.
|
|
4330
|
+
if (img.pageGrid !== undefined) return { withheld: img.pageGrid.said };
|
|
3994
4331
|
const plate = partPlate(img);
|
|
3995
4332
|
const alpha = new Uint8Array(plate.width * plate.height);
|
|
3996
4333
|
for (let i = 0; i < alpha.length; i++) alpha[i] = plate.data[i * 4 + 3];
|
|
@@ -4134,8 +4471,9 @@ function buildRigMesh(
|
|
|
4134
4471
|
// spec stated, and the editor shows whatever is written here as the image's
|
|
4135
4472
|
// dimensions (it showed 32x32, its missing-image placeholder, for a 0x0 mesh).
|
|
4136
4473
|
const img = att.image === undefined ? undefined : atlasedImage(att.image, where, ctx);
|
|
4137
|
-
const
|
|
4138
|
-
const
|
|
4474
|
+
const seqSize = att.sequence === undefined ? null : sequenceFrameSize(sequenceFrames(att, placeholder, where, ctx), att, where);
|
|
4475
|
+
const width = seqSize !== null ? seqSize.width : (att.width ?? img?.width);
|
|
4476
|
+
const height = seqSize !== null ? seqSize.height : (att.height ?? img?.height);
|
|
4139
4477
|
if (width === undefined || height === undefined) {
|
|
4140
4478
|
throw new CompileError(
|
|
4141
4479
|
`${where}: a mesh needs width and height — give them, or give an "image" and rigc will measure the PNG`,
|
|
@@ -4154,12 +4492,14 @@ function buildRigMesh(
|
|
|
4154
4492
|
const path = attachmentPath(att, placeholder);
|
|
4155
4493
|
if (path !== undefined) out.path = path;
|
|
4156
4494
|
if (att.color !== undefined) out.color = att.color;
|
|
4495
|
+
if (att.sequence !== undefined) out.sequence = emitSequence(att.sequence);
|
|
4157
4496
|
// Register it as `authored`: geometry rigc did not build and whose topology it
|
|
4158
4497
|
// therefore gets to assume nothing about. The generator-topology assertions
|
|
4159
4498
|
// read this and skip rather than measuring a ring that was never a ring.
|
|
4160
4499
|
ctx.meshBones.add(ctx.anchorBone);
|
|
4161
4500
|
for (const name of boundBones) ctx.meshBones.add(name);
|
|
4162
|
-
const
|
|
4501
|
+
const measured = measureAuthoredFit(att, ctx);
|
|
4502
|
+
const fit = measured === null || 'withheld' in measured ? null : measured;
|
|
4163
4503
|
ctx.meshes.push({
|
|
4164
4504
|
slot: ctx.slotName,
|
|
4165
4505
|
kind: 'authored',
|
|
@@ -4169,6 +4509,7 @@ function buildRigMesh(
|
|
|
4169
4509
|
bones: boundBones.length ? boundBones : [ctx.anchorBone],
|
|
4170
4510
|
coverage: fit === null ? undefined : r6(fit.coverage),
|
|
4171
4511
|
overshoot: fit?.overshoot,
|
|
4512
|
+
...(measured !== null && 'withheld' in measured ? { fitWithheld: measured.withheld } : {}),
|
|
4172
4513
|
});
|
|
4173
4514
|
return out;
|
|
4174
4515
|
}
|
|
@@ -4248,8 +4589,9 @@ function buildRigLinkedMesh(
|
|
|
4248
4589
|
// The art side is a mesh's, unchanged: a link draws its OWN region, which is
|
|
4249
4590
|
// the reason the type exists — one triangulation, one outfit's pixels each.
|
|
4250
4591
|
const img = att.image === undefined ? undefined : atlasedImage(att.image, where, ctx);
|
|
4251
|
-
const
|
|
4252
|
-
const
|
|
4592
|
+
const seqSize = att.sequence === undefined ? null : sequenceFrameSize(sequenceFrames(att, placeholder, where, ctx), att, where);
|
|
4593
|
+
const width = seqSize !== null ? seqSize.width : (att.width ?? img?.width);
|
|
4594
|
+
const height = seqSize !== null ? seqSize.height : (att.height ?? img?.height);
|
|
4253
4595
|
if (width === undefined || height === undefined) {
|
|
4254
4596
|
throw new CompileError(
|
|
4255
4597
|
`${where}: a linked mesh needs width and height — give them, or give an "image" and rigc will measure the PNG`,
|
|
@@ -4265,6 +4607,7 @@ function buildRigLinkedMesh(
|
|
|
4265
4607
|
if (att.skin !== undefined && att.skin !== 'default') out.skin = att.skin;
|
|
4266
4608
|
if (att.timelines === false) out.timelines = false;
|
|
4267
4609
|
if (att.color !== undefined) out.color = att.color;
|
|
4610
|
+
if (att.sequence !== undefined) out.sequence = emitSequence(att.sequence);
|
|
4268
4611
|
// 🚫 NOT registered in `ctx.meshes`, and that is a decision rather than an
|
|
4269
4612
|
// omission. `meshKinds` is keyed by SLOT and the commonest linked mesh shares
|
|
4270
4613
|
// its source's slot from another skin, so an entry here would overwrite the
|
|
@@ -4526,8 +4869,13 @@ function sampleMeshDepth(
|
|
|
4526
4869
|
*
|
|
4527
4870
|
* The two are different questions and the header says why. This one has no
|
|
4528
4871
|
* refusal behind it: it is counted, reported, and left to the author.
|
|
4872
|
+
*
|
|
4873
|
+
* `unlocated` in its place when the part's texels cannot be located — a
|
|
4874
|
+
* region lifted off a page whose file is not its declared size (issue #750),
|
|
4875
|
+
* carrying `pageGridSaid`'s clause — and the count is then `null` with that
|
|
4876
|
+
* clause beside it: withheld, rather than taken off another part of the page.
|
|
4529
4877
|
*/
|
|
4530
|
-
partAlpha: Uint8Array,
|
|
4878
|
+
partAlpha: Uint8Array | { unlocated: string },
|
|
4531
4879
|
partWidth: number,
|
|
4532
4880
|
partHeight: number,
|
|
4533
4881
|
where: string,
|
|
@@ -4587,7 +4935,7 @@ function sampleMeshDepth(
|
|
|
4587
4935
|
// Zero, not a threshold: "the part image draws nothing here" is a fact
|
|
4588
4936
|
// about the file, and any other cut-off would be rigc deciding how faint
|
|
4589
4937
|
// a texel has to be before it stops counting as art.
|
|
4590
|
-
if (partAlpha[at] === 0) drawn = false;
|
|
4938
|
+
if (partAlpha instanceof Uint8Array && partAlpha[at] === 0) drawn = false;
|
|
4591
4939
|
}
|
|
4592
4940
|
if (!opaqueEverywhere && !covered) uncovered.push(v);
|
|
4593
4941
|
if (!drawn) undrawn++;
|
|
@@ -4632,7 +4980,7 @@ function sampleMeshDepth(
|
|
|
4632
4980
|
zScale: spec.zScale,
|
|
4633
4981
|
tone,
|
|
4634
4982
|
range: [r6(lo), r6(hi)],
|
|
4635
|
-
undrawn,
|
|
4983
|
+
...(partAlpha instanceof Uint8Array ? { undrawn } : { undrawn: null, unlocated: partAlpha.unlocated }),
|
|
4636
4984
|
ceiling: turnCeiling(
|
|
4637
4985
|
points.map(([px, py]) => toBind(px, py)),
|
|
4638
4986
|
z,
|
|
@@ -4870,7 +5218,11 @@ function buildGridAttachment(
|
|
|
4870
5218
|
geometry.points,
|
|
4871
5219
|
geometry.triangles,
|
|
4872
5220
|
(px, py) => toBoneLocal(anchor, anchor.worldX + px * toArt - w / 2, anchor.worldY + h / 2 - py * toArt),
|
|
4873
|
-
|
|
5221
|
+
// The lattice's geometry takes nothing off the texels — its window is
|
|
5222
|
+
// the plate's size, which is the atlas's own `offsets` — so a page
|
|
5223
|
+
// whose file is not its declared size costs a grid only this one
|
|
5224
|
+
// count, and the count is what is withheld (issue #750).
|
|
5225
|
+
img.pageGrid === undefined ? plateAlpha(plate) : { unlocated: img.pageGrid.said },
|
|
4874
5226
|
plate.width,
|
|
4875
5227
|
plate.height,
|
|
4876
5228
|
where,
|
|
@@ -4959,6 +5311,24 @@ function buildContourAttachment(
|
|
|
4959
5311
|
);
|
|
4960
5312
|
}
|
|
4961
5313
|
const img = atlasedImage(att.image, where, ctx);
|
|
5314
|
+
// 🔒 **A trace needs the part's texels, and on a page that is not its
|
|
5315
|
+
// declared size the lift does not have them** (issue #750). `extractRegion`
|
|
5316
|
+
// addresses the page at the coordinates the atlas states; on a file of any
|
|
5317
|
+
// other size those are a different part of the picture — measured on a
|
|
5318
|
+
// half-resolution pack, the lift came back with no pixel above alpha 0 and
|
|
5319
|
+
// the build refused with "there is no silhouette to trace", a true sentence
|
|
5320
|
+
// about the wrong texels. Unlike an authored mesh's fit, which is a
|
|
5321
|
+
// measurement and is withheld, this outline IS the geometry: there is no mesh
|
|
5322
|
+
// to build without it, and the compiler invents none. So it is refused, with
|
|
5323
|
+
// `A06`'s own sentence, which names the ratio and the repair — on `build`
|
|
5324
|
+
// this refusal arrives before the gate, where `A06` would have said it.
|
|
5325
|
+
if (img.pageGrid !== undefined) {
|
|
5326
|
+
throw new CompileError(
|
|
5327
|
+
`${where}: a "contour" generator traces the part's own alpha, and "${att.image}" is lifted off a packed ` +
|
|
5328
|
+
'page at the coordinates the atlas states, which on this file are not where its texels are — so there is ' +
|
|
5329
|
+
`no silhouette here to trace, only another part of the page. ${img.pageGrid.sentence}`,
|
|
5330
|
+
);
|
|
5331
|
+
}
|
|
4962
5332
|
// ⚠️ Nothing here reads the PNG's colour type. `hasAlpha` answers "where does
|
|
4963
5333
|
// this file keep its alpha", not "is any pixel of it transparent" — a tRNS
|
|
4964
5334
|
// chunk is real transparency (issue #215) and an all-255 alpha channel is
|
|
@@ -5968,6 +6338,26 @@ function compileValueTrack(
|
|
|
5968
6338
|
throw new CompileError(`${where}: key times must strictly increase (at t=${key.t})`);
|
|
5969
6339
|
}
|
|
5970
6340
|
checkKeyTime(where, time, key.t, duration);
|
|
6341
|
+
// A named-value timeline (`inherit`): the key's `v` is one name, stepped by
|
|
6342
|
+
// the format. The name is resolved rather than copied, and what is WRITTEN
|
|
6343
|
+
// is the resolver's spelling — so `NoScale`, which the runtime also reads,
|
|
6344
|
+
// emits as the `noScale` an editor writes, and a spelling the runtime
|
|
6345
|
+
// cannot resolve never reaches the file at all.
|
|
6346
|
+
if (shape.named !== undefined) {
|
|
6347
|
+
if (key.ease !== undefined || key.curve !== undefined) {
|
|
6348
|
+
throw new CompileError(
|
|
6349
|
+
`${where} key at t=${key.t} carries ${key.ease !== undefined ? `an easing ("${key.ease}")` : 'a curve'}, and ` +
|
|
6350
|
+
`this timeline is stepped by the format — its reader builds no curve, so the mode changes AT the key and ` +
|
|
6351
|
+
'holds until the next one. Remove it',
|
|
6352
|
+
);
|
|
6353
|
+
}
|
|
6354
|
+
const mode = shape.named.resolve(key.v);
|
|
6355
|
+
if (mode === undefined) {
|
|
6356
|
+
throw new CompileError(`${where} key at t=${key.t} names mode ${JSON.stringify(key.v)}; ${shape.named.known}`);
|
|
6357
|
+
}
|
|
6358
|
+
out.push({ time, [shape.named.field]: mode });
|
|
6359
|
+
continue;
|
|
6360
|
+
}
|
|
5971
6361
|
// A no-field timeline (`reset`) is an event: the key IS the value, so it
|
|
5972
6362
|
// carries none. Anything else must match the field count exactly.
|
|
5973
6363
|
if (shape.fields.length === 0) {
|
|
@@ -6674,6 +7064,81 @@ function deformGeometryOf(
|
|
|
6674
7064
|
* nothing guessed. What survives is the one case the identity does not cover: a
|
|
6675
7065
|
* vertex whose weights do not close at 1.
|
|
6676
7066
|
*/
|
|
7067
|
+
/**
|
|
7068
|
+
* One sequence timeline — `animations.<a>.attachments.<skin>.<slot>.<attachment>.sequence`
|
|
7069
|
+
* — keyed on an attachment that carries a numbered series (issue #729).
|
|
7070
|
+
*
|
|
7071
|
+
* The key's own shape (`mode` among the seven, a whole `index`, a delay the
|
|
7072
|
+
* parser will not divide by zero) is `parseMotionSpec`'s; what is refused here
|
|
7073
|
+
* needs the emitted attachment:
|
|
7074
|
+
*
|
|
7075
|
+
* - an attachment with no `sequence` block. The parser gives every region and
|
|
7076
|
+
* mesh a one-region series (`readSequence(null)` is `new Sequence(1,
|
|
7077
|
+
* false)`), so the timeline loads and every mode shows that one region —
|
|
7078
|
+
* measured: a `loop` key on a plain region showed it at every time.
|
|
7079
|
+
* - an `index` at or past the series' `count`. `Sequence.resolveIndex` clamps
|
|
7080
|
+
* it to the last frame (measured: `hold` at index 5 of 4 showed frame 4).
|
|
7081
|
+
* - a linked mesh that plays its source's timelines (`timelines` absent or
|
|
7082
|
+
* true). Its `timelineAttachment` is the SOURCE (`SkeletonJson.js:437-448`),
|
|
7083
|
+
* and `SequenceTimeline.applyToSlot` returns unless the slot's attachment's
|
|
7084
|
+
* `timelineAttachment` is the one the timeline was built for — so a key
|
|
7085
|
+
* aimed at the link itself is applied to nothing.
|
|
7086
|
+
*
|
|
7087
|
+
* Fields are emitted exactly as stated: an omitted `mode` is the parser's
|
|
7088
|
+
* `"hold"`, an omitted `index` its 0, an omitted `delay` the previous key's —
|
|
7089
|
+
* none of which the compiler writes for the spec.
|
|
7090
|
+
*/
|
|
7091
|
+
function compileSequenceTrack(
|
|
7092
|
+
track: MotionSequenceTrack,
|
|
7093
|
+
duration: number,
|
|
7094
|
+
attachment: SpineAttachment,
|
|
7095
|
+
where: string,
|
|
7096
|
+
): SpineTimelineKey[] {
|
|
7097
|
+
const type = (attachment as { type?: string }).type ?? 'region';
|
|
7098
|
+
const series = (attachment as { sequence?: SpineSequence }).sequence;
|
|
7099
|
+
if (series === undefined) {
|
|
7100
|
+
throw new CompileError(
|
|
7101
|
+
`${where}: attachment "${track.attachment}" carries no "sequence" block, so there is no series to step. The ` +
|
|
7102
|
+
'parser gives every region and mesh a series of ONE region (`readSequence(null)` is `new Sequence(1, ' +
|
|
7103
|
+
'false)`), so this timeline would load and show that region under every mode at every time. Give the ' +
|
|
7104
|
+
'attachment a "sequence" in the rig spec, or remove the track.',
|
|
7105
|
+
);
|
|
7106
|
+
}
|
|
7107
|
+
if (type === 'linkedmesh' && (attachment as SpineLinkedMeshAttachment).timelines !== false) {
|
|
7108
|
+
const source = (attachment as SpineLinkedMeshAttachment).source;
|
|
7109
|
+
throw new CompileError(
|
|
7110
|
+
`${where}: attachment "${track.attachment}" is a linked mesh that plays its source's timelines ("timelines" ` +
|
|
7111
|
+
`is not false), so its \`timelineAttachment\` is "${source}" (\`SkeletonJson.js:437-448\`) and ` +
|
|
7112
|
+
'`SequenceTimeline` applies only where the slot\'s attachment\'s `timelineAttachment` is the one it was ' +
|
|
7113
|
+
`built for — a key aimed at the link is applied to nothing. Key "${source}" instead (the link steps its own ` +
|
|
7114
|
+
'series by it), or set "timelines": false on the link.',
|
|
7115
|
+
);
|
|
7116
|
+
}
|
|
7117
|
+
if (track.keys.length === 0) throw new CompileError(`${where}: no keys`);
|
|
7118
|
+
const out: SpineTimelineKey[] = [];
|
|
7119
|
+
for (let i = 0; i < track.keys.length; i++) {
|
|
7120
|
+
const key = track.keys[i];
|
|
7121
|
+
const time = keyTime(key.t);
|
|
7122
|
+
if (i > 0 && time <= (out[i - 1].time as number)) {
|
|
7123
|
+
throw new CompileError(`${where}: key times must strictly increase (at t=${key.t})`);
|
|
7124
|
+
}
|
|
7125
|
+
checkKeyTime(where, time, key.t, duration);
|
|
7126
|
+
if (key.index !== undefined && key.index >= series.count) {
|
|
7127
|
+
throw new CompileError(
|
|
7128
|
+
`${where} (t=${key.t}): index ${key.index} is past the end of a ${series.count}-frame series (frames 0 to ` +
|
|
7129
|
+
`${series.count - 1}). \`Sequence.resolveIndex\` clamps it to the last frame, so the key would start on a ` +
|
|
7130
|
+
'frame it does not name. `index` is 0-based.',
|
|
7131
|
+
);
|
|
7132
|
+
}
|
|
7133
|
+
const entry: SpineTimelineKey = { time };
|
|
7134
|
+
if (key.mode !== undefined) entry.mode = key.mode;
|
|
7135
|
+
if (key.index !== undefined) entry.index = key.index;
|
|
7136
|
+
if (key.delay !== undefined) entry.delay = r6(key.delay);
|
|
7137
|
+
out.push(entry);
|
|
7138
|
+
}
|
|
7139
|
+
return out;
|
|
7140
|
+
}
|
|
7141
|
+
|
|
6677
7142
|
function compileDeformTrack(
|
|
6678
7143
|
track: MotionDeformTrack,
|
|
6679
7144
|
motion: MotionSpec,
|