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/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, opts)]
1097
- : data.animations.map((a) => sampleAnimation(data, a.name, FRAMING_FPS, opts));
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;