spine-rigc 1.1.0 → 1.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 +4 -3
- package/cli.ts +289 -46
- package/docs/AUTHORING.md +134 -7
- package/docs/MOTION.md +1 -0
- package/package.json +1 -1
- package/skills/rigc/SKILL.md +25 -2
- package/src/ballot.ts +9 -12
- package/src/check.ts +181 -5
- package/src/checkpics.ts +293 -0
- package/src/preview.ts +223 -32
- package/src/render.ts +140 -2
package/src/render.ts
CHANGED
|
@@ -198,6 +198,19 @@ export interface FramesSidecar {
|
|
|
198
198
|
* one it cannot (`skin` absent while the run asked for one).
|
|
199
199
|
*/
|
|
200
200
|
skin?: string;
|
|
201
|
+
/**
|
|
202
|
+
* The slots these frames draw, when `render --slot` narrowed them to a subset
|
|
203
|
+
* (issue #835) — in the skeleton's draw order, whatever order they were named in.
|
|
204
|
+
*
|
|
205
|
+
* ⭐ Absent on a render of every slot, for the reason `skin` is: that is what
|
|
206
|
+
* every frame set written before this field existed says too, so the whole-rig
|
|
207
|
+
* render stays byte-identical and the key's presence is the claim. A frame set
|
|
208
|
+
* carrying this or `hidden` is a picture of PART of the rig, and `check` refuses
|
|
209
|
+
* it as a reference by name rather than scoring a whole candidate against it.
|
|
210
|
+
*/
|
|
211
|
+
slots?: string[];
|
|
212
|
+
/** The slots these frames leave out, when `render --hide` named them — see `slots`. */
|
|
213
|
+
hidden?: string[];
|
|
201
214
|
/** The colour the frames were cleared to, straight RGBA 0..255. */
|
|
202
215
|
background: RGBA;
|
|
203
216
|
viewport: {
|
|
@@ -329,6 +342,118 @@ export interface PoseOptions {
|
|
|
329
342
|
* whole flag exists to remove.
|
|
330
343
|
*/
|
|
331
344
|
skin?: string;
|
|
345
|
+
/**
|
|
346
|
+
* Draw only these slots, by name (issue #835). `hidden` is the same statement
|
|
347
|
+
* the other way round, and the two together are refused.
|
|
348
|
+
*
|
|
349
|
+
* ⭐ **It is a filter on what is DRAWN, never on what is framed.**
|
|
350
|
+
* `framingViewport` takes both off before it samples, so a frame with `head`
|
|
351
|
+
* hidden sits on exactly the pixel grid of the frame with it and the two
|
|
352
|
+
* overlay — which is the whole use of the picture: *which part is this pixel*
|
|
353
|
+
* is answered by the difference between two frames of one grid, and a subset
|
|
354
|
+
* re-framed to its own extent would have no second frame to differ from.
|
|
355
|
+
*
|
|
356
|
+
* ⚠️ Applied in `piecesOf`, where the pieces are collected, and resolved there
|
|
357
|
+
* against the posed skeleton's own slots and skin — see `slotSubsetOf` — so a
|
|
358
|
+
* name that draws nothing is refused by name rather than quietly matching no
|
|
359
|
+
* piece.
|
|
360
|
+
*/
|
|
361
|
+
slots?: string[];
|
|
362
|
+
/** Draw every slot but these — see `slots`. */
|
|
363
|
+
hidden?: string[];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Why a slot subset cannot be drawn — a `--slot`/`--hide` naming no slot, a slot
|
|
368
|
+
* whose art only another skin carries, or both flags at once (issue #835).
|
|
369
|
+
*
|
|
370
|
+
* A class of its own so `cli.ts` can turn it into a usage refusal (exit 2,
|
|
371
|
+
* nothing written) without reading a message to decide what kind it is.
|
|
372
|
+
*/
|
|
373
|
+
export class SlotSubsetError extends Error {}
|
|
374
|
+
|
|
375
|
+
/** The flag spelling each half of a subset is refused under — the UI's, since that is who reads it. */
|
|
376
|
+
const SUBSET_FLAG = { slots: '--slot', hidden: '--hide' } as const;
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* A slot subset resolved against a skeleton: which half was asked for, and the
|
|
380
|
+
* names in the skeleton's **draw order** rather than the order they were typed.
|
|
381
|
+
*
|
|
382
|
+
* Draw order because the names are a set and the sidecar records them: `--hide
|
|
383
|
+
* b,a` and `--hide a,b` are one picture, and a sidecar whose bytes depended on
|
|
384
|
+
* the spelling would make two identical frame sets differ.
|
|
385
|
+
*/
|
|
386
|
+
export interface SlotSubset {
|
|
387
|
+
mode: 'slots' | 'hidden';
|
|
388
|
+
names: string[];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Resolve `slots` / `hidden` against `data` as posed under `skin`, or refuse by name.
|
|
393
|
+
*
|
|
394
|
+
* `undefined` when neither is set — the whole rig, which is the ordinary case and
|
|
395
|
+
* costs nothing. Refused, each naming what would have worked:
|
|
396
|
+
*
|
|
397
|
+
* - **both at once** — one statement two ways, as `--rig` with `--cut` is;
|
|
398
|
+
* - **a name the skeleton does not declare** — with every slot it does declare,
|
|
399
|
+
* in draw order, and how many;
|
|
400
|
+
* - **a slot whose attachments live only under skins this pose does not
|
|
401
|
+
* resolve through.** Every slot is declared at the skeleton's top level, so
|
|
402
|
+
* "a slot only a named skin declares" is not a shape the format has — what a
|
|
403
|
+
* skin declares is the slot's ART. A pose resolves an attachment through the
|
|
404
|
+
* skin it was set to and then the default skin (`Skeleton.getAttachment`), so
|
|
405
|
+
* a slot none of whose attachments is in either of those draws nothing in
|
|
406
|
+
* every frame, and `--slot` on it would be a blank picture that looks like an
|
|
407
|
+
* answer. The refusal names the skin(s) that do carry it.
|
|
408
|
+
*
|
|
409
|
+
* ⚠️ A declared slot with no attachment in ANY skin is accepted: it draws
|
|
410
|
+
* nothing under every skin, so there is no skin to name and no picture of it
|
|
411
|
+
* that a different invocation would produce.
|
|
412
|
+
*/
|
|
413
|
+
export function slotSubsetOf(
|
|
414
|
+
data: SkeletonData,
|
|
415
|
+
opts: Pick<PoseOptions, 'slots' | 'hidden'> | undefined,
|
|
416
|
+
skin: string | undefined,
|
|
417
|
+
): SlotSubset | undefined {
|
|
418
|
+
if (opts?.slots !== undefined && opts.hidden !== undefined) {
|
|
419
|
+
throw new SlotSubsetError(
|
|
420
|
+
'--slot and --hide are one statement two ways; name the slots to draw or the slots to hide, not both',
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
const mode = opts?.slots !== undefined ? 'slots' : opts?.hidden !== undefined ? 'hidden' : undefined;
|
|
424
|
+
if (mode === undefined) return undefined;
|
|
425
|
+
const asked = (mode === 'slots' ? opts?.slots : opts?.hidden) ?? [];
|
|
426
|
+
const flag = SUBSET_FLAG[mode];
|
|
427
|
+
const declared = data.slots.map((slot) => slot.name);
|
|
428
|
+
|
|
429
|
+
const unknown = asked.filter((name) => data.findSlot(name) === null);
|
|
430
|
+
if (unknown.length > 0 || asked.length === 0) {
|
|
431
|
+
const named =
|
|
432
|
+
unknown.length === 0
|
|
433
|
+
? 'was given no slot name'
|
|
434
|
+
: `${unknown.map((name) => JSON.stringify(name)).join(', ')} ${unknown.length === 1 ? 'names' : 'name'} no slot`;
|
|
435
|
+
throw new SlotSubsetError(
|
|
436
|
+
`${flag} ${named}; this skeleton declares, in draw order: ${declared.join(', ') || 'none'} (${declared.length})`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const resolving = new Set([skin ?? null, data.defaultSkin?.name ?? null]);
|
|
441
|
+
const underThisPose =
|
|
442
|
+
skin === undefined ? 'under no skin (the default skin alone)' : `under skin ${JSON.stringify(skin)}`;
|
|
443
|
+
for (const name of asked) {
|
|
444
|
+
const index = data.findSlot(name)?.index ?? -1;
|
|
445
|
+
const carriers = data.skins.filter((s) => s.getAttachments().some((entry) => entry.slotIndex === index));
|
|
446
|
+
if (carriers.length === 0 || carriers.some((s) => resolving.has(s.name))) continue;
|
|
447
|
+
const skins = carriers.map((s) => JSON.stringify(s.name));
|
|
448
|
+
throw new SlotSubsetError(
|
|
449
|
+
`${flag} ${JSON.stringify(name)} draws nothing ${underThisPose}: its attachments are declared only under ` +
|
|
450
|
+
`${skins.length === 1 ? 'skin' : 'skins'} ${skins.join(', ')} — pass --skin ${
|
|
451
|
+
skins.length === 1 ? skins[0] : 'with one of them'
|
|
452
|
+
}`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
const chosen = new Set(asked);
|
|
456
|
+
return { mode, names: declared.filter((name) => chosen.has(name)) };
|
|
332
457
|
}
|
|
333
458
|
|
|
334
459
|
/**
|
|
@@ -646,8 +771,14 @@ export function piecesOf(skeleton: Skeleton, opts?: PoseOptions): Piece[] {
|
|
|
646
771
|
'skeleton.setSkin(...) and skeleton.setupPose() before this.',
|
|
647
772
|
);
|
|
648
773
|
}
|
|
774
|
+
// Resolved against the skeleton as it was posed — its own slots and the skin
|
|
775
|
+
// it was set to — so a name that draws nothing is refused here, where the one
|
|
776
|
+
// application point is, rather than matching no piece in silence.
|
|
777
|
+
const subset = slotSubsetOf(skeleton.data, opts, skeleton.skin?.name);
|
|
778
|
+
const named = subset === undefined ? undefined : new Set(subset.names);
|
|
649
779
|
const pieces: Piece[] = [];
|
|
650
780
|
for (const slot of skeleton.drawOrder.appliedPose) {
|
|
781
|
+
if (subset !== undefined && named !== undefined && named.has(slot.data.name) !== (subset.mode === 'slots')) continue;
|
|
651
782
|
const pose = slot.appliedPose;
|
|
652
783
|
const attachment = pose.attachment;
|
|
653
784
|
if (!attachment) continue;
|
|
@@ -1091,10 +1222,17 @@ export function framingViewport(data: SkeletonData, maxSide: number, opts?: Pose
|
|
|
1091
1222
|
// attachments that POSE, and two skins fill a slot with art of different sizes
|
|
1092
1223
|
// in different places. Framing one skin's shot with another skin's box would
|
|
1093
1224
|
// put the difference between two skins into every measurement taken in it.
|
|
1225
|
+
//
|
|
1226
|
+
// ⭐ A slot subset is the opposite case, and is taken off (issue #835): what
|
|
1227
|
+
// `--slot`/`--hide` leave out still counts toward the box, so a frame with a
|
|
1228
|
+
// part hidden lands on the pixel grid of the frame with it and the two overlay.
|
|
1229
|
+
// A subset framed to its own extent would move every pixel it kept.
|
|
1230
|
+
const { slots: _drawn, hidden: _hidden, ...whole } = opts ?? {};
|
|
1231
|
+
const framed = opts === undefined ? undefined : whole;
|
|
1094
1232
|
const sets =
|
|
1095
1233
|
data.animations.length === 0
|
|
1096
|
-
? [sampleSetupPose(data,
|
|
1097
|
-
: data.animations.map((a) => sampleAnimation(data, a.name, FRAMING_FPS,
|
|
1234
|
+
? [sampleSetupPose(data, framed)]
|
|
1235
|
+
: data.animations.map((a) => sampleAnimation(data, a.name, FRAMING_FPS, framed));
|
|
1098
1236
|
const box = unionBounds(sets);
|
|
1099
1237
|
if (!Number.isFinite(box.minX)) return null;
|
|
1100
1238
|
const pad = Math.max(box.maxX - box.minX, box.maxY - box.minY) * PAD;
|