spine-rigc 2.11.1 → 2.12.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/validate.ts CHANGED
@@ -104,6 +104,8 @@ import { a38SkinMembersAreSkinRequired } from './assertions/bodies/a38.ts';
104
104
  import { a06AtlasPageSizeMatchesPng } from './assertions/bodies/a06.ts';
105
105
  import { a19OverlayPngsHaveAlpha } from './assertions/bodies/a19.ts';
106
106
  import { a49PackedFootprintsDoNotOverlap } from './assertions/bodies/a49.ts';
107
+ import { a50StageBoxIsTheStage } from './assertions/bodies/a50.ts';
108
+ import { SKIP_NO_STAGE_BOX, type StageBoxFacts } from './assertions/facts/stage_box.ts';
107
109
  import { a27RegionNameMatchesPageFilename } from './assertions/bodies/a27.ts';
108
110
  import type { MeshEntry as MeshAttachmentEntry, MeshFacts } from './assertions/facts/mesh_attachments.ts';
109
111
  import type { ClipEnd, PolygonEntry, PolygonFacts } from './assertions/facts/vertex_polygons.ts';
@@ -1270,6 +1272,61 @@ function documentStage(modelText: string | undefined): StageFacts | undefined {
1270
1272
  return undefined;
1271
1273
  }
1272
1274
 
1275
+ /**
1276
+ * The runtime's supply of `StageBoxFacts` (issue #1168): the box a
1277
+ * `rigc-compiled/3` document's `stage.box` asks for — nothing asks without
1278
+ * one, and an export has none — and what the loaded skeleton holds there: the
1279
+ * `default` skin's attachment of that name on that slot, its vertices as
1280
+ * loaded, and its world vertices on a fresh skeleton at the setup pose —
1281
+ * `setupPose()`, `updateWorldTransform(Physics.none)`, no skin set — which
1282
+ * is the pose `getBounds` reads for the header and the one a consumer that
1283
+ * loads the files and asks for the box sees.
1284
+ */
1285
+ export function spineStageBox(data: ReturnType<SkeletonJson['readSkeletonData']>, modelText?: string): StageBoxFacts {
1286
+ const asked = documentStageBox(modelText);
1287
+ if (typeof asked === 'string') return { asked: null, why: asked, slot: false, held: null };
1288
+ const slotData = data.findSlot(asked.slot);
1289
+ if (slotData === null) return { asked, why: '', slot: false, held: null };
1290
+ const attachment = data.defaultSkin?.getAttachment(slotData.index, asked.attachment) ?? null;
1291
+ if (attachment === null) return { asked, why: '', slot: true, held: null };
1292
+ if (!(attachment instanceof BoundingBoxAttachment)) {
1293
+ const type =
1294
+ attachment instanceof RegionAttachment ? 'region' : attachment instanceof MeshAttachment ? 'mesh' : attachment instanceof ClippingAttachment ? 'clipping' : attachment instanceof PathAttachment ? 'path' : 'point';
1295
+ return { asked, why: '', slot: true, held: { type, weighted: false, stored: [], world: '' } };
1296
+ }
1297
+ const weighted = attachment.bones !== null && attachment.bones !== undefined;
1298
+ const stored = weighted ? [] : [...attachment.vertices];
1299
+ const skeleton = new Skeleton(data);
1300
+ skeleton.setupPose();
1301
+ skeleton.updateWorldTransform(Physics.none);
1302
+ const slot = skeleton.slots[slotData.index];
1303
+ let world: number[] | string;
1304
+ if (!slot.bone.active) world = `its bone "${slot.bone.data.name}" is inactive with no skin set, so nothing poses it`;
1305
+ else {
1306
+ const out = new Array<number>(attachment.worldVerticesLength).fill(0);
1307
+ attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, out, 0, 2);
1308
+ world = out;
1309
+ }
1310
+ return { asked, why: '', slot: true, held: { type: 'boundingbox', weighted, stored, world } };
1311
+ }
1312
+
1313
+ /** The box a `rigc-compiled/3` document's stage asks for, with that stage — or the SKIP's reason where nothing asks (`spineStageBox`). */
1314
+ function documentStageBox(modelText: string | undefined): StageBoxFacts['asked'] & object | string {
1315
+ if (modelText === undefined) return SKIP_NO_STAGE_BOX;
1316
+ let doc: unknown;
1317
+ try {
1318
+ doc = JSON.parse(modelText);
1319
+ } catch {
1320
+ return SKIP_NO_STAGE_BOX;
1321
+ }
1322
+ if (!isObj(doc) || doc.spec !== MODEL_DOCUMENT_SPEC || !isObj(doc.stage)) return SKIP_NO_STAGE_BOX;
1323
+ const { x, y, width, height, box } = doc.stage;
1324
+ if (typeof x !== 'number' || typeof y !== 'number' || typeof width !== 'number' || typeof height !== 'number') return SKIP_NO_STAGE_BOX;
1325
+ if (!isObj(box)) return SKIP_NO_STAGE_BOX;
1326
+ if (typeof box.slot !== 'string' || typeof box.attachment !== 'string') return SKIP_NO_STAGE_BOX;
1327
+ return { slot: box.slot, attachment: box.attachment, stage: { x, y, width, height } };
1328
+ }
1329
+
1273
1330
  /**
1274
1331
  * The runtime's supply of `RegionJoinFacts` (issue #1025, cut 4c-1), read
1275
1332
  * before the round trip as A08 always read it: the region names of a
@@ -3242,6 +3299,10 @@ export function validate(input: ValidateInput): ValidateReport {
3242
3299
  // `./assertions/footprints.ts`. The meshes are the loaded skeleton's, and
3243
3300
  // where it did not load every footprint is the rectangle A06 read.
3244
3301
  check('A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP', () => a49PackedFootprintsDoNotOverlap(verdicts, { atlas }, spineRegionJoins(input.atlasText, raw), skeletonData === null ? null : loadedMeshFacts()));
3302
+ // The stage box a rig asked for (issue #1168): the slot's bounding box,
3303
+ // loaded and posed at setup by spine-core, against the stage the document
3304
+ // states. The argument and both readings are `./assertions/bodies/a50.ts`.
3305
+ check('A50_STAGE_BOX_IS_THE_STAGE', () => (skeletonData === null ? skip('A50_STAGE_BOX_IS_THE_STAGE', SKIP_NO_SKELETON) : a50StageBoxIsTheStage(verdicts, spineStageBox(skeletonData, input.modelText))));
3245
3306
 
3246
3307
  // -------------------------------------------------------------------------
3247
3308
  // Archetype assertions — the invariants the RIG declares about itself.