spine-rigc 0.31.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/docs/AUTHORING.md +205 -27
- package/docs/INGEST.md +8 -7
- package/docs/SPEC_COVERAGE.md +6 -5
- package/package.json +1 -1
- package/src/compile.ts +328 -10
- package/src/deformmeasure.ts +7 -2
- package/src/ingest.ts +154 -25
- package/src/motion.ts +82 -1
- package/src/rig.ts +145 -3
- package/src/timelines.ts +94 -6
- package/src/types.ts +63 -0
- package/src/validate.ts +440 -72
package/src/compile.ts
CHANGED
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
type RigMeshBinding,
|
|
64
64
|
type RigPathAttachment,
|
|
65
65
|
type RigRegionAttachment,
|
|
66
|
+
type RigSequence,
|
|
66
67
|
type RigSkinParts,
|
|
67
68
|
type RigSpec,
|
|
68
69
|
type RigVertexGeometry,
|
|
@@ -132,6 +133,7 @@ import type {
|
|
|
132
133
|
MotionEventKey,
|
|
133
134
|
MotionIkTrack,
|
|
134
135
|
MotionMemberValues,
|
|
136
|
+
MotionSequenceTrack,
|
|
135
137
|
MotionSpec,
|
|
136
138
|
MotionTrack,
|
|
137
139
|
MotionTransformTrack,
|
|
@@ -148,6 +150,7 @@ import type {
|
|
|
148
150
|
SpineMeshAttachment,
|
|
149
151
|
SpinePathAttachment,
|
|
150
152
|
SpineRegionAttachment,
|
|
153
|
+
SpineSequence,
|
|
151
154
|
SpineSkeletonJson,
|
|
152
155
|
SpineSlot,
|
|
153
156
|
SpineTimelineKey,
|
|
@@ -314,8 +317,14 @@ export function editorNamesInOrder(names: readonly string[], collection: 'animat
|
|
|
314
317
|
return [...names].sort((a, b) => verdicts.get(a)?.get(b) ?? 0);
|
|
315
318
|
}
|
|
316
319
|
|
|
317
|
-
/**
|
|
318
|
-
|
|
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';
|
|
319
328
|
|
|
320
329
|
/**
|
|
321
330
|
* The order the emitted `skins` array is written in: **`default` first, then the
|
|
@@ -1871,8 +1880,17 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
1871
1880
|
/** The pre-packed atlas `--atlas-in` named, parsed once, or null. */
|
|
1872
1881
|
const atlasIn = opts.atlasInPath === undefined ? null : readAtlasIn(resolve(opts.atlasInPath));
|
|
1873
1882
|
|
|
1874
|
-
const addImage = (
|
|
1875
|
-
|
|
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 => {
|
|
1876
1894
|
if (seenRegions.has(region)) {
|
|
1877
1895
|
throw new CompileError(`duplicate region name "${region}" (${relPath})`);
|
|
1878
1896
|
}
|
|
@@ -1883,7 +1901,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
1883
1901
|
: resolveFromAtlas(relPath, region, isBase, outDir, atlasIn);
|
|
1884
1902
|
seenRegions.add(region);
|
|
1885
1903
|
regionSource.set(region, loosePath);
|
|
1886
|
-
partDirs.add(
|
|
1904
|
+
if (partDir !== null) partDirs.add(partDir);
|
|
1887
1905
|
images.push(img);
|
|
1888
1906
|
return img;
|
|
1889
1907
|
};
|
|
@@ -1929,6 +1947,63 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
1929
1947
|
);
|
|
1930
1948
|
};
|
|
1931
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
|
+
|
|
1932
2007
|
// A manifest may name a part the cut does not carry. A formation can declare
|
|
1933
2008
|
// more slots than any one cut fills, and a cut that shares a sprite with the
|
|
1934
2009
|
// scene around it has no plate of its own to point at — the manifest then
|
|
@@ -2116,6 +2191,19 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2116
2191
|
if (typeof image === 'string') {
|
|
2117
2192
|
addSkinImage(image, `skin "${skinName}" slot "${slotName}" attachment "${placeholder}"`);
|
|
2118
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
|
+
}
|
|
2119
2207
|
}
|
|
2120
2208
|
rigAttachmentNames.set(slotName, names);
|
|
2121
2209
|
}
|
|
@@ -2847,6 +2935,43 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2847
2935
|
((deformTimelines[skinName] ??= {})[track.slot] ??= {})[track.attachment] = { deform: keys };
|
|
2848
2936
|
}
|
|
2849
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
|
+
|
|
2850
2975
|
const drawOrder = anim.drawOrder ? compileDrawOrder(anim.drawOrder, animName, anim.duration, slots) : null;
|
|
2851
2976
|
if (drawOrder) for (const key of drawOrder) compiledDuration = Math.max(compiledDuration, key.time as number);
|
|
2852
2977
|
|
|
@@ -3149,8 +3274,14 @@ function skinAttachmentName(skinName: string, placeholder: string): string {
|
|
|
3149
3274
|
* By the time either caller runs, a contested placeholder the **default** skin
|
|
3150
3275
|
* fills has already been refused — see `refuseDefaultSkinContest` — so every
|
|
3151
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.
|
|
3152
3283
|
*/
|
|
3153
|
-
function composeSkinAttachmentName(skinName: string, placeholder: string, contested: boolean): string | null {
|
|
3284
|
+
export function composeSkinAttachmentName(skinName: string, placeholder: string, contested: boolean): string | null {
|
|
3154
3285
|
return contested ? skinAttachmentName(skinName, placeholder) : null;
|
|
3155
3286
|
}
|
|
3156
3287
|
|
|
@@ -3956,12 +4087,120 @@ function attachmentPath(att: { path?: string; image?: string }, placeholder: str
|
|
|
3956
4087
|
return region === placeholder ? undefined : region;
|
|
3957
4088
|
}
|
|
3958
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
|
+
|
|
3959
4185
|
function buildRigRegion(
|
|
3960
4186
|
att: RigRegionAttachment,
|
|
3961
4187
|
placeholder: string,
|
|
3962
4188
|
where: string,
|
|
3963
4189
|
ctx: AttachmentContext,
|
|
3964
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
|
+
}
|
|
3965
4204
|
const img = att.image === undefined ? null : atlasedImage(att.image, where, ctx);
|
|
3966
4205
|
// ⭐ An IMPORTED region's size is not a default the spec may override. On the
|
|
3967
4206
|
// loose path `att.width` and the PNG's width are two legitimate numbers — "draw
|
|
@@ -4232,8 +4471,9 @@ function buildRigMesh(
|
|
|
4232
4471
|
// spec stated, and the editor shows whatever is written here as the image's
|
|
4233
4472
|
// dimensions (it showed 32x32, its missing-image placeholder, for a 0x0 mesh).
|
|
4234
4473
|
const img = att.image === undefined ? undefined : atlasedImage(att.image, where, ctx);
|
|
4235
|
-
const
|
|
4236
|
-
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);
|
|
4237
4477
|
if (width === undefined || height === undefined) {
|
|
4238
4478
|
throw new CompileError(
|
|
4239
4479
|
`${where}: a mesh needs width and height — give them, or give an "image" and rigc will measure the PNG`,
|
|
@@ -4252,6 +4492,7 @@ function buildRigMesh(
|
|
|
4252
4492
|
const path = attachmentPath(att, placeholder);
|
|
4253
4493
|
if (path !== undefined) out.path = path;
|
|
4254
4494
|
if (att.color !== undefined) out.color = att.color;
|
|
4495
|
+
if (att.sequence !== undefined) out.sequence = emitSequence(att.sequence);
|
|
4255
4496
|
// Register it as `authored`: geometry rigc did not build and whose topology it
|
|
4256
4497
|
// therefore gets to assume nothing about. The generator-topology assertions
|
|
4257
4498
|
// read this and skip rather than measuring a ring that was never a ring.
|
|
@@ -4348,8 +4589,9 @@ function buildRigLinkedMesh(
|
|
|
4348
4589
|
// The art side is a mesh's, unchanged: a link draws its OWN region, which is
|
|
4349
4590
|
// the reason the type exists — one triangulation, one outfit's pixels each.
|
|
4350
4591
|
const img = att.image === undefined ? undefined : atlasedImage(att.image, where, ctx);
|
|
4351
|
-
const
|
|
4352
|
-
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);
|
|
4353
4595
|
if (width === undefined || height === undefined) {
|
|
4354
4596
|
throw new CompileError(
|
|
4355
4597
|
`${where}: a linked mesh needs width and height — give them, or give an "image" and rigc will measure the PNG`,
|
|
@@ -4365,6 +4607,7 @@ function buildRigLinkedMesh(
|
|
|
4365
4607
|
if (att.skin !== undefined && att.skin !== 'default') out.skin = att.skin;
|
|
4366
4608
|
if (att.timelines === false) out.timelines = false;
|
|
4367
4609
|
if (att.color !== undefined) out.color = att.color;
|
|
4610
|
+
if (att.sequence !== undefined) out.sequence = emitSequence(att.sequence);
|
|
4368
4611
|
// 🚫 NOT registered in `ctx.meshes`, and that is a decision rather than an
|
|
4369
4612
|
// omission. `meshKinds` is keyed by SLOT and the commonest linked mesh shares
|
|
4370
4613
|
// its source's slot from another skin, so an entry here would overwrite the
|
|
@@ -6821,6 +7064,81 @@ function deformGeometryOf(
|
|
|
6821
7064
|
* nothing guessed. What survives is the one case the identity does not cover: a
|
|
6822
7065
|
* vertex whose weights do not close at 1.
|
|
6823
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
|
+
|
|
6824
7142
|
function compileDeformTrack(
|
|
6825
7143
|
track: MotionDeformTrack,
|
|
6826
7144
|
motion: MotionSpec,
|
package/src/deformmeasure.ts
CHANGED
|
@@ -94,6 +94,7 @@ import {
|
|
|
94
94
|
type Attachment,
|
|
95
95
|
AtlasAttachmentLoader,
|
|
96
96
|
type Bone,
|
|
97
|
+
type CurveTimeline,
|
|
97
98
|
DeformTimeline,
|
|
98
99
|
FromProperty,
|
|
99
100
|
FromRotate,
|
|
@@ -1937,7 +1938,7 @@ function curveLegs(timeline: DeformTimeline, frame: number): { kind: DeformSpanC
|
|
|
1937
1938
|
}
|
|
1938
1939
|
|
|
1939
1940
|
/** Points `CurveTimeline.setBezier` stores per curve — `BEZIER_SIZE / 2`. */
|
|
1940
|
-
const BEZIER_POINTS = 9;
|
|
1941
|
+
export const BEZIER_POINTS = 9;
|
|
1941
1942
|
|
|
1942
1943
|
/**
|
|
1943
1944
|
* `CurveTimeline.curves`, which is `protected` and is read anyway.
|
|
@@ -1956,8 +1957,12 @@ const BEZIER_POINTS = 9;
|
|
|
1956
1957
|
*
|
|
1957
1958
|
* `A05` already gates the emitted curve arrays, and `DW18` is the control that
|
|
1958
1959
|
* the reading here matches what the runtime does with them.
|
|
1960
|
+
*
|
|
1961
|
+
* Exported for `validate.ts`'s `curveChannelValues` (issue #752), which asks the
|
|
1962
|
+
* same array what values a constraint's `mix` timeline poses between its keys —
|
|
1963
|
+
* so the reach into protected storage stays one reach, in this file.
|
|
1959
1964
|
*/
|
|
1960
|
-
function curveStorage(timeline:
|
|
1965
|
+
export function curveStorage(timeline: CurveTimeline): ArrayLike<number> {
|
|
1961
1966
|
return (timeline as unknown as { curves: ArrayLike<number> }).curves;
|
|
1962
1967
|
}
|
|
1963
1968
|
|