spine-rigc 1.6.1 → 1.6.2

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/docs/AUTHORING.md CHANGED
@@ -235,7 +235,10 @@ spine-core's own `SkeletonClipping`, in spine-webgl's call order — so a slot f
235
235
  the clip's through its `end` slot draws only what falls inside the polygon, in
236
236
  `render`, `check` and every other picture rigc's own rasteriser draws (`preview`
237
237
  plays in Spine's player, which always clipped); the framing box still counts
238
- what the clip removes, so adding or keying a mask moves no pixel it leaves drawn. Three commands take `--out`: a directory for `render`
238
+ what the clip removes, so adding or keying a mask moves no pixel it leaves drawn,
239
+ and it never counts a slot whose bone the posed skin leaves unposed (a `"skin": true`
240
+ bone the skin does not name, or any bone below one, §3.4.1), which draws no pixel;
241
+ when every drawn slot is such a slot, the render is refused by name (§3.4). Three commands take `--out`: a directory for `render`
239
242
  (default `render/`), the `.html` file for `preview` (default `preview.html`) and
240
243
  for `vote` (default `ballot.html`).
241
244
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data, round-trips it through spine-core and named assertions, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/render.ts CHANGED
@@ -2147,7 +2147,8 @@ export interface SkinRoster {
2147
2147
  * The skin roster of a parsed Spine file, as the runtime flags it —
2148
2148
  * `unposedBones` over a fresh skeleton's `active` under each skin, so Spine's
2149
2149
  * activation rule is the runtime's and is not restated here. Nothing is posed
2150
- * until a question is asked, and only the refusal asks one.
2150
+ * until a question is asked: the framing asks one (the skin it frames under,
2151
+ * `slotsOnUnposedBones`) and the refusal asks one per skin.
2151
2152
  */
2152
2153
  export function skinRosterOf(data: SkeletonData): SkinRoster {
2153
2154
  return {
@@ -2164,6 +2165,28 @@ function rosterBehind(source: PoseSource, roster: SkinRoster | undefined): SkinR
2164
2165
  return source instanceof SkeletonData ? skinRosterOf(source) : roster;
2165
2166
  }
2166
2167
 
2168
+ /**
2169
+ * ⭐ **The one derivation of "a drawn slot on an unposed bone"** (issues #997,
2170
+ * #1000): the names of the slots of `slots` whose bone `roster` says `skin`
2171
+ * leaves unposed — inactive itself, or below an inactive bone. Both posers draw
2172
+ * such a slot through the zero matrix, so it draws no pixel and every vertex of
2173
+ * it sits at the origin. `framingViewport` takes these slots off the box
2174
+ * (#1000) and `unframeableSentence` refuses a pose that drew nothing else
2175
+ * (#997), from this one set.
2176
+ *
2177
+ * Empty without a roster: a bare `Poser` carries no skin structure, and what
2178
+ * cannot be read is not guessed — every slot then counts, as it did before.
2179
+ */
2180
+ export function slotsOnUnposedBones(
2181
+ slots: ReadonlyArray<{ name: string; bone: string }>,
2182
+ skin: string | undefined,
2183
+ roster: SkinRoster | undefined,
2184
+ ): Set<string> {
2185
+ if (roster === undefined) return new Set();
2186
+ const unposed = roster.unposedUnder(skin);
2187
+ return new Set(slots.filter((slot) => unposed.has(slot.bone)).map((slot) => slot.name));
2188
+ }
2189
+
2167
2190
  /** How many drawn slots a refusal names before it says how many more there are. */
2168
2191
  const UNFRAMEABLE_NAMED = 3;
2169
2192
 
@@ -2185,9 +2208,10 @@ const UNFRAMEABLE_NAMED = 3;
2185
2208
  * collapsed their attachments (a world scale of 0 does).
2186
2209
  *
2187
2210
  * `roster` is the skin structure of the Spine file the poser poses
2188
- * (`skinRosterOf`), which is what says whether a bone is unposed and which skins
2189
- * pose it — read only on this refusal's path. Without it the first sentence
2190
- * cannot be told from the second, and the second is what is said.
2211
+ * (`skinRosterOf`), which is what says whether a bone is unposed
2212
+ * (`slotsOnUnposedBones`, the set the framing box leaves out) and which skins
2213
+ * pose it. Without it the first sentence cannot be told from the second, and
2214
+ * the second is what is said.
2191
2215
  *
2192
2216
  * Distinct from *nothing to draw*: that is a skeleton that posed no vertex at
2193
2217
  * all, and its fix is art; this one posed vertices, and they have no place.
@@ -2213,8 +2237,8 @@ export function unframeableSentence(
2213
2237
  const named = (describe: (slot: { name: string; bone: string }) => string): string =>
2214
2238
  drawn.slice(0, UNFRAMEABLE_NAMED).map(describe).join('; ') + more;
2215
2239
  if (roster !== undefined) {
2216
- const unposed = roster.unposedUnder(skin);
2217
- if (drawn.length > 0 && drawn.every((slot) => unposed.has(slot.bone))) {
2240
+ const offBox = slotsOnUnposedBones(slots, skin, roster);
2241
+ if (drawn.length > 0 && drawn.every((slot) => offBox.has(slot.name))) {
2218
2242
  const bySkin = roster.skins.map((name) => ({ name, unposed: roster.unposedUnder(name) }));
2219
2243
  const posers = (bone: string): string => {
2220
2244
  const names = bySkin.filter((k) => !k.unposed.has(bone)).map((k) => JSON.stringify(k.name));
@@ -2250,11 +2274,21 @@ export function unframeableSentence(
2250
2274
  * every vertex sits at one point is refused by an `UnframeablePoseError`
2251
2275
  * (`unframeableSentence`, issue #997), never framed at a scale of Infinity.
2252
2276
  *
2277
+ * The box is over the slots that POSE (issue #1000): a drawn slot whose bone
2278
+ * the skin leaves unposed (`slotsOnUnposedBones`) draws no pixel — both posers
2279
+ * put it through the zero matrix — so its vertices at the origin are not part
2280
+ * of the shot, and counting them framed every frame of a rig that carries one
2281
+ * to a point nothing is drawn at (256x94 where the posed slot alone gives
2282
+ * 256x55). When every drawn slot is such a slot there is no posed box at all,
2283
+ * and that is #997's refusal, never an empty or invented one.
2284
+ *
2253
2285
  * `roster` is the skin roster behind a `Poser` source (`skinRosterOf`) — what
2254
- * lets that refusal name the skins that pose an unposed bone. Spine data as the
2255
- * source is its own.
2286
+ * says which bones the skin leaves unposed, and lets the refusal name the skins
2287
+ * that pose one. Spine data as the source is its own. A bare `Poser` with no
2288
+ * roster cannot tell an unposed bone from a posed one, so every drawn slot
2289
+ * counts there; every CLI caller passes the roster, so both posers frame alike.
2256
2290
  */
2257
- export function framingViewport(source: PoseSource, maxSide: number, opts?: PoseOptions, roster?: SkinRoster): Viewport | null {
2291
+ export function framingViewport(source: PoseSource, maxSide: number, opts?: PoseOptions, rosterGiven?: SkinRoster): Viewport | null {
2258
2292
  const poser = poserOf(source);
2259
2293
  // The skin belongs here as much as in the frames: the union box is over the
2260
2294
  // attachments that POSE, and two skins fill a slot with art of different sizes
@@ -2285,9 +2319,20 @@ export function framingViewport(source: PoseSource, maxSide: number, opts?: Pose
2285
2319
  // this, the first case's `null` covered both, and a single overflowing bone
2286
2320
  // among finite ones reached neither: its box was finite on one side, and
2287
2321
  // `render` wrote a NaN-by-NaN frame set with exit 0.
2288
- const posedAny = sets.some((frames) => frames.some((frame) => frame.pieces.some((piece) => piece.world.length > 0)));
2289
- if (!posedAny) return null;
2290
- const box = unionBounds(sets);
2322
+ const drewAny = (frameSets: ReadonlyArray<readonly Frame[]>): boolean =>
2323
+ frameSets.some((frames) => frames.some((frame) => frame.pieces.some((piece) => piece.world.length > 0)));
2324
+ if (!drewAny(sets)) return null;
2325
+ // ⭐ The box is over the slots that pose (issue #1000). A drawn slot on a bone
2326
+ // the skin leaves unposed is taken off — it draws no pixel — unless nothing
2327
+ // else drew, in which case every set is kept whole so the pose reaches #997's
2328
+ // refusal below exactly as it did before this: the same inputs, the same
2329
+ // sentence, never an empty box framed to something.
2330
+ const roster = rosterBehind(source, rosterGiven);
2331
+ const offBox = slotsOnUnposedBones(poser.slots, framed.skin, roster);
2332
+ const posedSets =
2333
+ offBox.size === 0 ? sets : sets.map((frames) => frames.map((frame) => ({ ...frame, pieces: frame.pieces.filter((piece) => !offBox.has(piece.slot)) })));
2334
+ const boxed = drewAny(posedSets) ? posedSets : sets;
2335
+ const box = unionBounds(boxed);
2291
2336
  if (![box.minX, box.minY, box.maxX, box.maxY].every(Number.isFinite)) {
2292
2337
  const found = nonFinitePoseOf(
2293
2338
  poser,
@@ -2309,7 +2354,7 @@ export function framingViewport(source: PoseSource, maxSide: number, opts?: Pose
2309
2354
  // A finite box over one point (issue #997): every drawn slot on a bone the
2310
2355
  // skin leaves unposed, or every vertex collapsed. Framed, it is a scale of
2311
2356
  // Infinity and a frame of NaN by NaN pixels, written as 0x0 with exit 0.
2312
- const unframeable = unframeableSentence(sets, poser.slots, framed.skin, rosterBehind(source, roster));
2357
+ const unframeable = unframeableSentence(boxed, poser.slots, framed.skin, roster);
2313
2358
  if (unframeable !== null) throw new UnframeablePoseError(unframeable);
2314
2359
  const pad = Math.max(box.maxX - box.minX, box.maxY - box.minY) * PAD;
2315
2360
  return viewportFor(box.minX - pad, box.minY - pad, box.maxX + pad, box.maxY + pad, maxSide);