spine-rigc 0.35.0 → 0.36.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/docs/AUTHORING.md +161 -97
- package/docs/FACE.md +3 -1
- package/docs/INGEST.md +10 -13
- package/docs/SPEC_COVERAGE.md +15 -14
- package/package.json +1 -1
- package/src/compile.ts +224 -236
- package/src/diff.ts +59 -0
- package/src/ingest.ts +26 -148
- package/src/rig.ts +48 -8
- package/src/timelines.ts +23 -8
- package/src/types.ts +14 -13
- package/src/validate.ts +9 -3
- package/tools/editor_roundtrip.ts +13 -4
package/src/diff.ts
CHANGED
|
@@ -683,6 +683,12 @@ interface AttachmentFact {
|
|
|
683
683
|
edgesPresent: boolean | null;
|
|
684
684
|
/** `<width>x<height>` as stated, or `unstated` — see `attachments.region_size`. */
|
|
685
685
|
size: string;
|
|
686
|
+
/**
|
|
687
|
+
* The name the runtime gives the attachment: its stated `name`, else its
|
|
688
|
+
* placeholder key (`getValue(map, "name", placeholder)`, `SkeletonJson.js:526`)
|
|
689
|
+
* — see `attachments.runtime_name`.
|
|
690
|
+
*/
|
|
691
|
+
runtimeName: string;
|
|
686
692
|
}
|
|
687
693
|
|
|
688
694
|
function attachmentFacts(root: Json): { skins: Set<string>; byKey: Map<string, AttachmentFact> } {
|
|
@@ -718,6 +724,7 @@ function attachmentFacts(root: Json): { skins: Set<string>; byKey: Map<string, A
|
|
|
718
724
|
// "has edges" means.
|
|
719
725
|
edgesPresent: type === 'mesh' ? 'edges' in att : null,
|
|
720
726
|
size: num(att.width) !== null && num(att.height) !== null ? `${num(att.width)}x${num(att.height)}` : 'unstated',
|
|
727
|
+
runtimeName: str(att.name) ?? attName,
|
|
721
728
|
});
|
|
722
729
|
}
|
|
723
730
|
}
|
|
@@ -820,9 +827,61 @@ function diffAttachments(c: Json, r: Json): DiffSection {
|
|
|
820
827
|
bm,
|
|
821
828
|
(x, y) => x.edgesPresent === y.edgesPresent,
|
|
822
829
|
),
|
|
830
|
+
// ── issue #796 ───────────────────────────────────────────────────────────
|
|
831
|
+
//
|
|
832
|
+
// The name the RUNTIME gives each attachment — `name` if stated, else the
|
|
833
|
+
// placeholder key — agreed per skin/slot/placeholder. Every measure above is
|
|
834
|
+
// keyed by the placeholder, so a rebuild that renamed every attachment while
|
|
835
|
+
// keeping every key read 1.000 across this whole section: that is what a
|
|
836
|
+
// rebuild did on two production rigs (a stated name respelled as `path`, and
|
|
837
|
+
// `<skin>/<placeholder>` composed over a name the file never stated), and it
|
|
838
|
+
// took a pose oracle comparing `slot.attachment.name` to see it.
|
|
839
|
+
//
|
|
840
|
+
// 🚫 Reported rather than in the mean, by the same test as `mesh_edges`: a
|
|
841
|
+
// name draws no pixel, so no reading of the frames could decide it. It is
|
|
842
|
+
// still a value a consumer reads — `slot.attachment.name` — which is why it
|
|
843
|
+
// is measured at all.
|
|
844
|
+
//
|
|
845
|
+
// ⚠️ Over the keys BOTH sides hold, not over the larger roster, and that is
|
|
846
|
+
// the one departure from `agreement` here. A key one side lacks is already
|
|
847
|
+
// `attachments.names` and `attachments.count`; scored again here it would
|
|
848
|
+
// move this measure on every rename of a key and every dropped attachment,
|
|
849
|
+
// and "the same key answers to another name" — the only thing nothing else
|
|
850
|
+
// reads — would be one term among many. The denominator this reports is the
|
|
851
|
+
// number of keys compared, so a report over few shared keys says so.
|
|
852
|
+
runtimeNames(a.byKey, b.byKey),
|
|
823
853
|
]);
|
|
824
854
|
}
|
|
825
855
|
|
|
856
|
+
/**
|
|
857
|
+
* `attachments.runtime_name`: of the skin/slot/placeholder keys both sides
|
|
858
|
+
* hold, how many answer to the same runtime name — see the note at its call.
|
|
859
|
+
*/
|
|
860
|
+
function runtimeNames(a: Map<string, AttachmentFact>, b: Map<string, AttachmentFact>): DiffMeasure {
|
|
861
|
+
let shared = 0;
|
|
862
|
+
let agree = 0;
|
|
863
|
+
const differ: string[] = [];
|
|
864
|
+
for (const [key, fact] of a) {
|
|
865
|
+
const other = b.get(key);
|
|
866
|
+
if (other === undefined) continue;
|
|
867
|
+
shared++;
|
|
868
|
+
if (fact.runtimeName === other.runtimeName) agree++;
|
|
869
|
+
else if (differ.length < 3) differ.push(`${key} "${fact.runtimeName}" vs "${other.runtimeName}"`);
|
|
870
|
+
}
|
|
871
|
+
const missed = shared - agree;
|
|
872
|
+
return measure(
|
|
873
|
+
'attachments.runtime_name',
|
|
874
|
+
'each attachment both sides hold answers to the same name at runtime (`name` if stated, else its placeholder)',
|
|
875
|
+
agree,
|
|
876
|
+
shared,
|
|
877
|
+
shared === 0
|
|
878
|
+
? 'no skin/slot/placeholder key is on both sides, so no name was compared'
|
|
879
|
+
: missed === 0
|
|
880
|
+
? undefined
|
|
881
|
+
: `${missed} renamed: ${differ.join('; ')}${missed > differ.length ? `; …and ${missed - differ.length} more` : ''}`,
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
|
|
826
885
|
interface ConstraintFact {
|
|
827
886
|
type: string;
|
|
828
887
|
/** Every bone or slot the constraint names, sorted — its wiring. */
|
package/src/ingest.ts
CHANGED
|
@@ -47,8 +47,6 @@
|
|
|
47
47
|
* spec.
|
|
48
48
|
*/
|
|
49
49
|
import {
|
|
50
|
-
composeSkinAttachmentName,
|
|
51
|
-
DEFAULT_SKIN,
|
|
52
50
|
PHYSICS_COMPONENTS,
|
|
53
51
|
SLOT_TRACKS as EMITTED_SLOT_TRACKS,
|
|
54
52
|
SPINE_VERSION,
|
|
@@ -681,34 +679,6 @@ function hexToRgb(hex: string): number[] {
|
|
|
681
679
|
return [0, 2, 4].map((i) => Number.parseInt(hex.slice(i, i + 2), 16) / 255);
|
|
682
680
|
}
|
|
683
681
|
|
|
684
|
-
/**
|
|
685
|
-
* Which placeholders more than one skin fills, per slot.
|
|
686
|
-
*
|
|
687
|
-
* 🔒 Inverts `contestedPlaceholders` + `nameSkinAttachment`: rigc writes an
|
|
688
|
-
* attachment `name` exactly where a placeholder is contested, and the rig spec
|
|
689
|
-
* has no field for one. So the name is DROPPED and re-derived — which is right
|
|
690
|
-
* where the contest survives the round trip, and a loss anywhere else. This is
|
|
691
|
-
* what lets that difference be reported rather than assumed.
|
|
692
|
-
*/
|
|
693
|
-
function contestedPlaceholders(skins: readonly unknown[]): Map<string, Set<string>> {
|
|
694
|
-
const fillers = new Map<string, Map<string, number>>();
|
|
695
|
-
for (const skin of skins) {
|
|
696
|
-
for (const [slot, placeholders] of objEntries(obj(skin).attachments)) {
|
|
697
|
-
const perSlot = fillers.get(slot) ?? new Map<string, number>();
|
|
698
|
-
for (const placeholder of Object.keys(placeholders)) {
|
|
699
|
-
perSlot.set(placeholder, (perSlot.get(placeholder) ?? 0) + 1);
|
|
700
|
-
}
|
|
701
|
-
fillers.set(slot, perSlot);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
const contested = new Map<string, Set<string>>();
|
|
705
|
-
for (const [slot, perSlot] of fillers) {
|
|
706
|
-
const shared = new Set([...perSlot].filter(([, count]) => count > 1).map(([placeholder]) => placeholder));
|
|
707
|
-
if (shared.size) contested.set(slot, shared);
|
|
708
|
-
}
|
|
709
|
-
return contested;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
682
|
/**
|
|
713
683
|
* Read a skeleton, write the two specs that rebuild it.
|
|
714
684
|
*
|
|
@@ -791,7 +761,6 @@ export function ingest(skeleton: unknown, opts: IngestOptions): IngestResult {
|
|
|
791
761
|
});
|
|
792
762
|
|
|
793
763
|
// -- skins and attachments ------------------------------------------------
|
|
794
|
-
const contested = contestedPlaceholders(arr(root.skins));
|
|
795
764
|
const skins: JsonObject = {};
|
|
796
765
|
for (const skin of arr(root.skins)) {
|
|
797
766
|
const entry = obj(skin);
|
|
@@ -801,8 +770,6 @@ export function ingest(skeleton: unknown, opts: IngestOptions): IngestResult {
|
|
|
801
770
|
for (const [placeholder, att] of Object.entries(placeholders)) {
|
|
802
771
|
perSlot[placeholder] = ingestAttachment(obj(att), placeholder, boneNames, opts, note, {
|
|
803
772
|
where: `skin "${nameOf(entry)}" slot "${slot}" attachment "${placeholder}"`,
|
|
804
|
-
contested: contested.get(slot)?.has(placeholder) === true,
|
|
805
|
-
skin: nameOf(entry),
|
|
806
773
|
});
|
|
807
774
|
}
|
|
808
775
|
table[slot] = perSlot;
|
|
@@ -1331,7 +1298,7 @@ function ingestAttachment(
|
|
|
1331
1298
|
boneNames: readonly string[],
|
|
1332
1299
|
opts: IngestOptions,
|
|
1333
1300
|
note: Note,
|
|
1334
|
-
at: { where: string
|
|
1301
|
+
at: { where: string },
|
|
1335
1302
|
): JsonObject {
|
|
1336
1303
|
// `readAttachment` reads no `type` as `region` (`SkeletonJson:539`), and so
|
|
1337
1304
|
// does `checkRigSpecKeys`. Both defaults are the parser's, not a guess.
|
|
@@ -1347,125 +1314,26 @@ function ingestAttachment(
|
|
|
1347
1314
|
// construct the file does not contain (issue #691).
|
|
1348
1315
|
const linked = (type === 'mesh' || type === 'linkedmesh') && typeof att.source === 'string' && att.source.length > 0;
|
|
1349
1316
|
|
|
1350
|
-
// The three types the parser gives a texture `path` to
|
|
1351
|
-
//
|
|
1352
|
-
// `
|
|
1353
|
-
//
|
|
1354
|
-
//
|
|
1355
|
-
// resolve no region at all.
|
|
1317
|
+
// The three types the parser gives a texture `path` to — and reads a
|
|
1318
|
+
// `sequence` on. `readAttachment` reaches `getValue(map, "path", name)` in the
|
|
1319
|
+
// `region` branch (`SkeletonJson.js:529`) and in the shared `mesh`/`linkedmesh`
|
|
1320
|
+
// branch (`:560`); `boundingbox`, `path`, `point` and `clipping` are
|
|
1321
|
+
// constructed from the name alone and resolve no region at all.
|
|
1356
1322
|
const resolvesRegion = linked || type === 'region' || type === 'mesh';
|
|
1357
1323
|
|
|
1358
|
-
/**
|
|
1359
|
-
* The atlas region a dropped `name` was carrying, which the rebuild has to
|
|
1360
|
-
* state as `path` or lose.
|
|
1361
|
-
*
|
|
1362
|
-
* 🚨 **The name is not bookkeeping — it is the region key** (issue #742).
|
|
1363
|
-
* `readAttachment` reads `name = getValue(map, "name", placeholder)` and then
|
|
1364
|
-
* `path = getValue(map, "path", name)`, so `path` defaults to the NAME and not
|
|
1365
|
-
* to the placeholder. A source that states a `name` and no `path` therefore
|
|
1366
|
-
* draws the region that name spells; rigc drops the name (the rig spec has no
|
|
1367
|
-
* field for one — see below) and used to write no `path` either, so the
|
|
1368
|
-
* rebuild asked the atlas for the PLACEHOLDER. Measured on a forged export
|
|
1369
|
-
* whose three attachments state `name` and no `path`: the source loads against
|
|
1370
|
-
* its own pack and the rebuild failed `A08_REGION_NAMES_MATCH_ATTACHMENTS`
|
|
1371
|
-
* three times and `A00_ROUNDTRIP_PARSE` with it.
|
|
1372
|
-
*
|
|
1373
|
-
* ⚠️ A name EQUAL to the placeholder resolves the same region either way, so
|
|
1374
|
-
* nothing is written for it — the rebuild would otherwise spell a field the
|
|
1375
|
-
* source did not, and `A18`'s reference is the source file.
|
|
1376
|
-
*
|
|
1377
|
-
* 🔑 This is not `at.contested`'s business, and that is the half the card
|
|
1378
|
-
* nearly missed. Where a placeholder is contested rigc composes a name of its
|
|
1379
|
-
* own and `nameSkinAttachment` pins `path` at the PLACEHOLDER — so a contested
|
|
1380
|
-
* source naming its own regions lost them with no finding at all, which is
|
|
1381
|
-
* quieter than the uncontested case rather than safer.
|
|
1382
|
-
*/
|
|
1383
|
-
const keptPath =
|
|
1384
|
-
att.path === undefined && resolvesRegion && typeof att.name === 'string' && att.name !== placeholder
|
|
1385
|
-
? att.name
|
|
1386
|
-
: undefined;
|
|
1387
|
-
|
|
1388
|
-
/** What became of the atlas region once the name is gone — the same four shapes wherever the name is lost. */
|
|
1389
|
-
const regionFate =
|
|
1390
|
-
keptPath !== undefined
|
|
1391
|
-
? `That name was also the ATLAS REGION this attachment resolves, because \`path\` defaults to the name and ` +
|
|
1392
|
-
`not to the placeholder (\`SkeletonJson.js:526\`, \`:529\`, \`:560\`) — so the region key is KEPT as ` +
|
|
1393
|
-
`"path": ${JSON.stringify(keptPath)} and the name alone is lost`
|
|
1394
|
-
: att.path !== undefined
|
|
1395
|
-
? `The region key is the source's own \`path\` ${JSON.stringify(att.path)}, carried unchanged, so the ` +
|
|
1396
|
-
'name alone is lost'
|
|
1397
|
-
: resolvesRegion
|
|
1398
|
-
? at.contested
|
|
1399
|
-
? 'The region is the placeholder\'s either way — the compiler pins a composed name\'s `path` at the ' +
|
|
1400
|
-
'placeholder — so the name alone is lost'
|
|
1401
|
-
: 'The name is the placeholder itself, so the rebuild resolves the same atlas region and no `path` is ' +
|
|
1402
|
-
'written for it'
|
|
1403
|
-
: `An attachment of type ${JSON.stringify(type)} resolves no atlas region — the parser builds it from ` +
|
|
1404
|
-
'the name alone — so there is no region key to keep and the name is the whole loss';
|
|
1405
|
-
|
|
1406
|
-
// `name` is DERIVED by rigc — `composeSkinAttachmentName` writes one exactly
|
|
1407
|
-
// where a placeholder is contested — so the rig spec has no field for it and
|
|
1408
|
-
// it is dropped. Where one skin fills the placeholder every stated name is a
|
|
1409
|
-
// loss, because the rebuild writes none.
|
|
1410
|
-
if (att.name !== undefined && !at.contested) {
|
|
1411
|
-
note(
|
|
1412
|
-
'lossy',
|
|
1413
|
-
'ATTACHMENT_NAME',
|
|
1414
|
-
at.where,
|
|
1415
|
-
`the attachment states name ${JSON.stringify(att.name)} and only ONE skin fills this placeholder, so rigc ` +
|
|
1416
|
-
'writes no name on the rebuild — it composes "<skin>/<placeholder>" only for a contested placeholder (#541). ' +
|
|
1417
|
-
regionFate,
|
|
1418
|
-
);
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
/**
|
|
1422
|
-
* 🚨 Where a placeholder IS contested the name is re-derived, and re-derived is
|
|
1423
|
-
* not the same as kept (issue #746). The rebuild's attachment answers to what
|
|
1424
|
-
* `composeSkinAttachmentName` returns; the source's answers to what it states,
|
|
1425
|
-
* or to its placeholder where it states nothing (`readAttachment`'s
|
|
1426
|
-
* `getValue(map, "name", placeholder)`, `SkeletonJson.js:526`). Only where
|
|
1427
|
-
* those two strings differ is anything renamed, so only there is a line.
|
|
1428
|
-
*
|
|
1429
|
-
* ⚠️ Comparing is the whole rule, and its cheap neighbour was measured and
|
|
1430
|
-
* rejected on #742: firing whenever a contested entry states a name puts a
|
|
1431
|
-
* `LOSS` on every contested round trip of rigc's own emits, which state
|
|
1432
|
-
* exactly the composed name and rebuild byte for byte.
|
|
1433
|
-
*
|
|
1434
|
-
* ⚠️ A contested placeholder the DEFAULT skin fills gets no line here: the
|
|
1435
|
-
* compiler composes nothing for it — `refuseDefaultSkinContest` refuses the
|
|
1436
|
-
* rebuild by name — so a line claiming a composed name would state one no
|
|
1437
|
-
* build will ever emit.
|
|
1438
|
-
*/
|
|
1439
|
-
const composed = composeSkinAttachmentName(at.skin, placeholder, at.contested);
|
|
1440
|
-
const sourceName = att.name === undefined ? placeholder : att.name;
|
|
1441
|
-
if (composed !== null && at.skin !== DEFAULT_SKIN && sourceName !== composed) {
|
|
1442
|
-
note(
|
|
1443
|
-
'lossy',
|
|
1444
|
-
'ATTACHMENT_NAME',
|
|
1445
|
-
at.where,
|
|
1446
|
-
(att.name === undefined
|
|
1447
|
-
? `the attachment states no name, so the source names it by its placeholder ${JSON.stringify(placeholder)} ` +
|
|
1448
|
-
'(`SkeletonJson.js:526`)'
|
|
1449
|
-
: `the attachment states name ${JSON.stringify(att.name)}`) +
|
|
1450
|
-
`, and more than one skin fills this placeholder, so rigc composes the name ${JSON.stringify(composed)} ` +
|
|
1451
|
-
`on the rebuild (#541) — the rig spec has no field for a name, so the rebuilt attachment answers to ` +
|
|
1452
|
-
`${JSON.stringify(composed)} where the source's answered to ${JSON.stringify(sourceName)}. ` +
|
|
1453
|
-
regionFate,
|
|
1454
|
-
);
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
1324
|
/** The texture side, which the skeleton does not encode. Inverts `buildRigRegion`'s tail. */
|
|
1458
1325
|
const carryArt = (): void => {
|
|
1459
1326
|
if (att.path !== undefined) out.path = att.path;
|
|
1460
|
-
else if (keptPath !== undefined) out.path = keptPath;
|
|
1461
1327
|
// `buildRigRegion` writes `path` when the image basename differs from the
|
|
1462
|
-
//
|
|
1463
|
-
//
|
|
1464
|
-
//
|
|
1465
|
-
// `
|
|
1466
|
-
//
|
|
1467
|
-
|
|
1468
|
-
|
|
1328
|
+
// name the attachment carries — its stated `name`, else the placeholder —
|
|
1329
|
+
// so naming the image after the region this attachment RESOLVES
|
|
1330
|
+
// (`path ?? name ?? placeholder`, the parser's own defaults at
|
|
1331
|
+
// `SkeletonJson.js:526`, `:529`, `:560`) reproduces the same `path`
|
|
1332
|
+
// decision AND the same atlas region name.
|
|
1333
|
+
if (opts.art === 'loose') {
|
|
1334
|
+
const region = att.path ?? (typeof att.name === 'string' ? att.name : placeholder);
|
|
1335
|
+
out.image = `${String(region)}.png`;
|
|
1336
|
+
}
|
|
1469
1337
|
if (att.width !== undefined) out.width = att.width;
|
|
1470
1338
|
if (att.height !== undefined) out.height = att.height;
|
|
1471
1339
|
};
|
|
@@ -1592,7 +1460,17 @@ function ingestAttachment(
|
|
|
1592
1460
|
);
|
|
1593
1461
|
}
|
|
1594
1462
|
}
|
|
1595
|
-
|
|
1463
|
+
// 🔑 The attachment's own `name`, carried VERBATIM wherever the source states
|
|
1464
|
+
// one — and nowhere else (issue #796). It is the runtime's `Attachment.name`
|
|
1465
|
+
// (`getValue(map, "name", placeholder)`, `SkeletonJson.js:526`) and, with no
|
|
1466
|
+
// `path`, the region the attachment draws; the rig spec has a field for it
|
|
1467
|
+
// and `compile` writes exactly what that field states, so nothing about it is
|
|
1468
|
+
// lost and nothing is re-derived. A name equal to its placeholder is carried
|
|
1469
|
+
// too: the source spelled it, and the rebuild is held to the source's text.
|
|
1470
|
+
// Put right after `type`, the order `RIG_KEYS` gives the field.
|
|
1471
|
+
if (att.name === undefined) return out;
|
|
1472
|
+
const { type: carriedType, ...rest } = out;
|
|
1473
|
+
return carriedType === undefined ? { name: att.name, ...rest } : { type: carriedType, name: att.name, ...rest };
|
|
1596
1474
|
}
|
|
1597
1475
|
|
|
1598
1476
|
/**
|
package/src/rig.ts
CHANGED
|
@@ -575,9 +575,36 @@ export type RigMeshGenerator = RigRingGenerator | RigRibbonGenerator | RigContou
|
|
|
575
575
|
/** The four `kind` names a generator may carry, and the order `RIG_KEYS` takes them in. */
|
|
576
576
|
export const RIG_GENERATOR_KINDS = ['ring', 'ribbon', 'contour', 'grid'] as const;
|
|
577
577
|
|
|
578
|
+
/**
|
|
579
|
+
* The attachment's own NAME, as distinct from the placeholder key it is filed
|
|
580
|
+
* under — one field, the same on every attachment type (issue #796).
|
|
581
|
+
*
|
|
582
|
+
* `readAttachment` reads `name = getValue(map, "name", placeholder)`
|
|
583
|
+
* (`SkeletonJson.js:526`) for every type, so this is the runtime's
|
|
584
|
+
* `Attachment.name` — what a consumer reads off `slot.attachment.name` — and,
|
|
585
|
+
* on the three types that draw, the default of `path` (`:529`, `:560`). Absent,
|
|
586
|
+
* the runtime names the attachment by its placeholder.
|
|
587
|
+
*
|
|
588
|
+
* 🔑 Nothing in the format resolves BY it. A skin's table, a slot's setup
|
|
589
|
+
* `attachment`, an attachment or deform timeline and a linked mesh's `source`
|
|
590
|
+
* are all keyed by the placeholder (`:415-418`, `:433`, `:1140`) — measured: a
|
|
591
|
+
* link whose `source` spells its source's name rather than its key throws
|
|
592
|
+
* `Source mesh not found`. So two skins may give one placeholder two
|
|
593
|
+
* attachments of one name, and the runtime keeps both.
|
|
594
|
+
*
|
|
595
|
+
* ⭐ rigc writes this field exactly when the spec states it and never derives
|
|
596
|
+
* one. Until #796 it composed `<skin>/<placeholder>` for a placeholder several
|
|
597
|
+
* skins fill, and a transcribed name had no field to live in — so a rebuild of
|
|
598
|
+
* an editor export answered to other names than its source did, which a
|
|
599
|
+
* consumer reading `attachment.name` can see and no gate could.
|
|
600
|
+
*/
|
|
601
|
+
export type RigAttachmentName = string;
|
|
602
|
+
|
|
578
603
|
/** `SkeletonJson.ts:540-559`. `type` defaults to `region` (`:539`). */
|
|
579
604
|
export interface RigRegionAttachment {
|
|
580
605
|
type?: 'region';
|
|
606
|
+
/** The runtime's attachment name — see `RigAttachmentName`. Absent, it is the placeholder. */
|
|
607
|
+
name?: RigAttachmentName;
|
|
581
608
|
/** The atlas region to resolve. Defaults to the attachment's own name. */
|
|
582
609
|
path?: string;
|
|
583
610
|
/**
|
|
@@ -675,6 +702,8 @@ export interface RigMeshBinding {
|
|
|
675
702
|
|
|
676
703
|
export interface RigMeshAttachment {
|
|
677
704
|
type: 'mesh';
|
|
705
|
+
/** The runtime's attachment name — see `RigAttachmentName`. Absent, it is the placeholder. */
|
|
706
|
+
name?: RigAttachmentName;
|
|
678
707
|
path?: string;
|
|
679
708
|
image?: string;
|
|
680
709
|
/** Its length defines `worldVerticesLength`; required with authored geometry. */
|
|
@@ -735,8 +764,11 @@ export interface RigMeshAttachment {
|
|
|
735
764
|
* under in its skin — and not its `name`.** The resolution is
|
|
736
765
|
* `skin.getAttachment(sourceSlotIndex, source)` (`SkeletonJson.js:433`), and a
|
|
737
766
|
* skin's table is keyed by the JSON key `readSkin` iterated (`:415-418`), so a
|
|
738
|
-
*
|
|
739
|
-
*
|
|
767
|
+
* source that states a `name` of its own is still found under its placeholder
|
|
768
|
+
* alone — and a `source` spelling that name throws `Source mesh not found`.
|
|
769
|
+
* Two skins each filling the source's placeholder are told apart by `skin`,
|
|
770
|
+
* never by a name (issue #796, which retired #541's reading that a link
|
|
771
|
+
* resolves its source by name).
|
|
740
772
|
*
|
|
741
773
|
* 🚨 **A linked mesh states no geometry of its own, and the parser is silent
|
|
742
774
|
* about one that does.** The `source` branch returns before `readVertices`
|
|
@@ -756,6 +788,8 @@ export interface RigMeshAttachment {
|
|
|
756
788
|
*/
|
|
757
789
|
export interface RigLinkedMeshAttachment {
|
|
758
790
|
type: 'linkedmesh';
|
|
791
|
+
/** The runtime's attachment name — see `RigAttachmentName`. Absent, it is the placeholder. */
|
|
792
|
+
name?: RigAttachmentName;
|
|
759
793
|
/** The art this link draws, exactly as a mesh's: its own region. */
|
|
760
794
|
path?: string;
|
|
761
795
|
image?: string;
|
|
@@ -853,6 +887,8 @@ export interface RigVertexGeometry {
|
|
|
853
887
|
*/
|
|
854
888
|
export interface RigBoundingBoxAttachment extends RigVertexGeometry {
|
|
855
889
|
type: 'boundingbox';
|
|
890
|
+
/** The runtime's attachment name — see `RigAttachmentName`. Absent, it is the placeholder. */
|
|
891
|
+
name?: RigAttachmentName;
|
|
856
892
|
}
|
|
857
893
|
|
|
858
894
|
/**
|
|
@@ -869,6 +905,8 @@ export interface RigBoundingBoxAttachment extends RigVertexGeometry {
|
|
|
869
905
|
*/
|
|
870
906
|
export interface RigClippingAttachment extends RigVertexGeometry {
|
|
871
907
|
type: 'clipping';
|
|
908
|
+
/** The runtime's attachment name — see `RigAttachmentName`. Absent, it is the placeholder. */
|
|
909
|
+
name?: RigAttachmentName;
|
|
872
910
|
/**
|
|
873
911
|
* The last slot this clip applies to, by name. Absent leaves `endSlot` null,
|
|
874
912
|
* which is the parser's own encoding for "clip everything after this one".
|
|
@@ -915,6 +953,8 @@ export interface RigClippingAttachment extends RigVertexGeometry {
|
|
|
915
953
|
*/
|
|
916
954
|
export interface RigPathAttachment extends RigVertexGeometry {
|
|
917
955
|
type: 'path';
|
|
956
|
+
/** The runtime's attachment name — see `RigAttachmentName`. Absent, it is the placeholder. */
|
|
957
|
+
name?: RigAttachmentName;
|
|
918
958
|
/** Default false. When true the last knot joins the first. */
|
|
919
959
|
closed?: boolean;
|
|
920
960
|
/**
|
|
@@ -1672,21 +1712,21 @@ export const RIG_KEYS = {
|
|
|
1672
1712
|
],
|
|
1673
1713
|
RigTransformProperty: ['offset', 'to'],
|
|
1674
1714
|
RigTransformTo: ['offset', 'max', 'scale'],
|
|
1675
|
-
RigRegionAttachment: ['type', 'path', 'image', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'width', 'height', 'color', 'sequence'],
|
|
1715
|
+
RigRegionAttachment: ['type', 'name', 'path', 'image', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'width', 'height', 'color', 'sequence'],
|
|
1676
1716
|
RigMeshAttachment: [
|
|
1677
|
-
'type', 'path', 'image', 'uvs', 'triangles', 'vertices', 'weights', 'boneIndexing', 'hull', 'edges',
|
|
1717
|
+
'type', 'name', 'path', 'image', 'uvs', 'triangles', 'vertices', 'weights', 'boneIndexing', 'hull', 'edges',
|
|
1678
1718
|
'width', 'height', 'color', 'generator', 'sequence',
|
|
1679
1719
|
],
|
|
1680
1720
|
RigLinkedMeshAttachment: [
|
|
1681
|
-
'type', 'path', 'image', 'source', 'slot', 'skin', 'timelines', 'width', 'height', 'color', 'sequence',
|
|
1721
|
+
'type', 'name', 'path', 'image', 'source', 'slot', 'skin', 'timelines', 'width', 'height', 'color', 'sequence',
|
|
1682
1722
|
// Declared so the refusal can name them — see `RigLinkedMeshAttachment`.
|
|
1683
1723
|
'uvs', 'triangles', 'vertices', 'weights', 'boneIndexing', 'hull', 'edges', 'generator',
|
|
1684
1724
|
],
|
|
1685
1725
|
RigMeshBinding: ['bone', 'x', 'y', 'weight'],
|
|
1686
1726
|
RigSequence: ['count', 'start', 'digits', 'setup'],
|
|
1687
|
-
RigBoundingBoxAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type'],
|
|
1688
|
-
RigClippingAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'end', 'convex', 'inverse'],
|
|
1689
|
-
RigPathAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'closed', 'constantSpeed', 'lengths'],
|
|
1727
|
+
RigBoundingBoxAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'name'],
|
|
1728
|
+
RigClippingAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'name', 'end', 'convex', 'inverse'],
|
|
1729
|
+
RigPathAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'name', 'closed', 'constantSpeed', 'lengths'],
|
|
1690
1730
|
RigRingGenerator: ['kind', 'hull', 'center', 'inner', 'size', 'bias', 'controls'],
|
|
1691
1731
|
RigRibbonGenerator: ['kind', 'size', 'rows', 'chain'],
|
|
1692
1732
|
RigContourGenerator: ['kind', 'tolerance', 'margin', 'maxVertices', 'alpha', 'depth', 'soft'],
|
package/src/timelines.ts
CHANGED
|
@@ -494,12 +494,25 @@ export const PHYSICS_POSE_RULES: PhysicsPoseRule[] = [
|
|
|
494
494
|
timeline: 'damping',
|
|
495
495
|
field: 'damping',
|
|
496
496
|
toPose: (v) => v,
|
|
497
|
-
poseOk: (v) => v
|
|
497
|
+
poseOk: (v) => v >= 0 && v <= 1,
|
|
498
498
|
keyOk: null,
|
|
499
499
|
inertAtSetup: false,
|
|
500
500
|
outside: null,
|
|
501
|
-
states: 'inside
|
|
502
|
-
statesKeyed: 'inside
|
|
501
|
+
states: 'inside [0, 1]',
|
|
502
|
+
statesKeyed: 'inside [0, 1]',
|
|
503
|
+
// 🔑 The interval is CLOSED (issue #794). `1 ** x` is 1 and `0 ** x` is 0
|
|
504
|
+
// for every positive exponent, so both ends are finite at every `fps` and
|
|
505
|
+
// both are rigs somebody can mean: 1 holds the jiggle, 0 follows the bone
|
|
506
|
+
// with no overshoot. [measured] through spine-core on the generated physics
|
|
507
|
+
// fixture, 120 steps from `Physics.reset` at 60, 45 and 30 fps under a
|
|
508
|
+
// displacing animation, as a setup value and as a key: every pose value
|
|
509
|
+
// finite, the velocity never 0 at 1 and 0 after every step at 0. And 1 is
|
|
510
|
+
// not only a choice: it is 4.2's own parser default for an omitted
|
|
511
|
+
// `damping` (`SkeletonJson.js:242` in 4.2.120,
|
|
512
|
+
// `getValue(constraintMap, "damping", 1)`; 4.3.13 reads 0.85 at `:308`), so a
|
|
513
|
+
// rig migrated from 4.2 that keys "the default" writes 1 and the editor
|
|
514
|
+
// exports it as it is. The open interval this replaced refused that rig.
|
|
515
|
+
//
|
|
503
516
|
// ⚠️ The frame-rate half is why the bound cannot be checked by playing a rig
|
|
504
517
|
// at 60 fps (issue #748): `step` is `1 / fps`, the constraint's own rate, so
|
|
505
518
|
// the exponent is exactly 1 there and a negative damping only flips the
|
|
@@ -508,11 +521,13 @@ export const PHYSICS_POSE_RULES: PhysicsPoseRule[] = [
|
|
|
508
521
|
// steps of the key at 45 and at 120 (exponents 1.3333 and 0.5).
|
|
509
522
|
why:
|
|
510
523
|
'the per-step decay is `damping ** (60 * step)`, with `step` = 1 / the constraint\'s `fps`, and every velocity ' +
|
|
511
|
-
'is multiplied by it (`PhysicsConstraint.js:114,148,158,163,210,222,227`), so 1
|
|
512
|
-
'
|
|
513
|
-
'
|
|
514
|
-
'
|
|
515
|
-
'
|
|
524
|
+
'is multiplied by it (`PhysicsConstraint.js:114,148,158,163,210,222,227`), so above 1 every velocity grows on ' +
|
|
525
|
+
'every step and the offset diverges, at every rate. Below 0 the result depends on `fps`: where `60 / fps` is a ' +
|
|
526
|
+
'whole number a negative base stays finite — at 60 fps it is the velocity\'s sign flipped each step, which can ' +
|
|
527
|
+
'look like a jiggle settling — and at any other rate it is a negative number raised to a fractional power, ' +
|
|
528
|
+
'which is NaN (`(-0.5) ** (60 / 45)`), so a rig tried only at 60 fps never shows the failure. The two ends are ' +
|
|
529
|
+
'values the runtime plays at every rate: at 1 the velocity never decays, so the jiggle holds for as long as it ' +
|
|
530
|
+
'runs, and at 0 every velocity is zeroed on every step, so the offset follows the bone with no overshoot',
|
|
516
531
|
},
|
|
517
532
|
];
|
|
518
533
|
|
package/src/types.ts
CHANGED
|
@@ -281,7 +281,7 @@ export type BoneProperty =
|
|
|
281
281
|
* export; `PHYSICS_TRACKS` in `compile.ts` carries the argument.
|
|
282
282
|
*
|
|
283
283
|
* 🔒 **Four of them have a compile-time range** (issue #610): `mass` must be
|
|
284
|
-
* `> 0`, `damping`
|
|
284
|
+
* `> 0`, `damping` inside the closed `[0, 1]`, and `mix` and `strength` `0` or
|
|
285
285
|
* more. The bounds are `PHYSICS_POSE_RULES` in `src/timelines.ts` and they are
|
|
286
286
|
* the runtime's, not a policy — `inertia`, `wind`, `gravity` and the top of
|
|
287
287
|
* `mix` are bounded nowhere, because the runtime documents nothing for the first
|
|
@@ -826,18 +826,19 @@ export interface SpineSlot {
|
|
|
826
826
|
*
|
|
827
827
|
* `readAttachment` reads `const name = getValue(map, "name", placeholder)`
|
|
828
828
|
* (`SkeletonJson.ts:526`), so an absent field means "the placeholder is also the
|
|
829
|
-
* name"
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
*
|
|
829
|
+
* name". Nothing in the format resolves by it — skins, setup attachments,
|
|
830
|
+
* attachment and deform timelines and a linked mesh's `source` are keyed by the
|
|
831
|
+
* placeholder — so several skins' entries under one placeholder may carry one
|
|
832
|
+
* name, which is what the editor itself exports (issue #796).
|
|
833
833
|
*
|
|
834
|
-
* 🚨
|
|
834
|
+
* 🚨 It moves a second field's default with it. For the three types that carry
|
|
835
835
|
* texture art, `path` defaults to **`name`**, not to the placeholder
|
|
836
|
-
* (`:529`, `:559`), so an attachment given a name and no path resolves
|
|
837
|
-
*
|
|
838
|
-
* `
|
|
836
|
+
* (`:529`, `:559`), so an attachment given a name and no path resolves the region
|
|
837
|
+
* that name spells. rigc emits it exactly when the rig spec states it
|
|
838
|
+
* (`RigAttachmentName` in `rig.ts`); from #541 to #796 it composed
|
|
839
|
+
* `<skin>/<placeholder>` for a placeholder several skins fill, and that is gone.
|
|
839
840
|
*
|
|
840
|
-
* ⚠️ And the **`default` skin may never be one of the skins sharing
|
|
841
|
+
* ⚠️ And the **`default` skin may never be one of the skins sharing a
|
|
841
842
|
* placeholder** — a fact about the editor rather than about the format (issue
|
|
842
843
|
* #567, Spine 4.3.26, round trips 7 and 8), and a `CompileError` rather than a
|
|
843
844
|
* spelling. The editor's named skins hold *skin placeholders*, a key holding a
|
|
@@ -848,8 +849,7 @@ export interface SpineSlot {
|
|
|
848
849
|
* makes that attachment's name collide with the named skins' placeholder of the
|
|
849
850
|
* same name, which the editor refuses at import (trip 8). Both spellings are
|
|
850
851
|
* measured, so there is no third; `refuseDefaultSkinContest` in `compile.ts` is
|
|
851
|
-
* where that lives
|
|
852
|
-
* for the skins that are left.
|
|
852
|
+
* where that lives.
|
|
853
853
|
*/
|
|
854
854
|
export interface SpineRegionAttachment {
|
|
855
855
|
name?: string;
|
|
@@ -888,7 +888,9 @@ export interface SpineMeshAttachment {
|
|
|
888
888
|
type: 'mesh';
|
|
889
889
|
/** See `SpineRegionAttachment.name` — and it takes `path` with it. */
|
|
890
890
|
name?: string;
|
|
891
|
+
/** `path` and `color` are written right after `type` — `compile.ts`'s `meshTextureKeys` says where that is measured. */
|
|
891
892
|
path?: string;
|
|
893
|
+
color?: string;
|
|
892
894
|
uvs: number[];
|
|
893
895
|
triangles: number[];
|
|
894
896
|
/** Weighted encoding: boneCount, (boneIndex, bindX, bindY, weight)*n, repeated. */
|
|
@@ -910,7 +912,6 @@ export interface SpineMeshAttachment {
|
|
|
910
912
|
/** Nonessential, but they make the mesh budget assertions readable. */
|
|
911
913
|
width: number;
|
|
912
914
|
height: number;
|
|
913
|
-
color?: string;
|
|
914
915
|
sequence?: SpineSequence;
|
|
915
916
|
}
|
|
916
917
|
|
package/src/validate.ts
CHANGED
|
@@ -570,7 +570,11 @@ const SETUP_POSE_SAYS: Record<
|
|
|
570
570
|
// back and below 0 one that is pushed away, and the row's `why` — the key's
|
|
571
571
|
// sentence — quotes the second from the same object.
|
|
572
572
|
strength: (pose, _animations, rule) => `has strength ${pose.strength}; ${physicsOutsideSays(rule, pose.strength)}`,
|
|
573
|
-
|
|
573
|
+
// The bound is read off the row, so the setup pose and a key cannot state two
|
|
574
|
+
// intervals; the reason is the two ways out, since the ends are inside (#794).
|
|
575
|
+
damping: (pose, _animations, rule) =>
|
|
576
|
+
`has damping ${pose.damping}; must be ${rule.states} — above 1 every velocity grows on every step, and below 0 ` +
|
|
577
|
+
'it is NaN at any fps where 60 / fps is not whole',
|
|
574
578
|
};
|
|
575
579
|
|
|
576
580
|
/**
|
|
@@ -3090,8 +3094,10 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
3090
3094
|
//
|
|
3091
3095
|
// Every failure mode here is silent. The five component fields default to
|
|
3092
3096
|
// 0, so a constraint can drive nothing at all; `mix` 0 mutes it; `mass` 0
|
|
3093
|
-
// becomes an infinite massInverse; and `damping`
|
|
3094
|
-
// on a mesh-driving bone means the canvas re-rasterises forever.
|
|
3097
|
+
// becomes an infinite massInverse; and `damping` above 1 never settles, which
|
|
3098
|
+
// on a mesh-driving bone means the canvas re-rasterises forever. 1 itself is
|
|
3099
|
+
// inside the bound since issue #794: it never decays, which is finite and a
|
|
3100
|
+
// choice, and what it costs a consumer's frame is the consumer's to judge.
|
|
3095
3101
|
//
|
|
3096
3102
|
// 🔑 **Two arms, one criterion.** The second arm below reads every physics
|
|
3097
3103
|
// TIMELINE key, and it is written against `PHYSICS_POSE_RULES` — the table
|
|
@@ -533,7 +533,13 @@ export function diffSummaryLines(reportPath: string): string[] {
|
|
|
533
533
|
function checkFigures(reportPath: string): string[] {
|
|
534
534
|
if (!existsSync(reportPath)) return ['(no check report was written, so nothing was read from one)'];
|
|
535
535
|
interface Anim {
|
|
536
|
-
|
|
536
|
+
/**
|
|
537
|
+
* `null` on the row `check` writes for the SETUP pose (`dir: "setup"`) — which
|
|
538
|
+
* is the only row a rig with no animations gets. Read as a string, that report
|
|
539
|
+
* threw at step 5 on every animation-less build (issue #796's two editor fixtures).
|
|
540
|
+
*/
|
|
541
|
+
animation: string | null;
|
|
542
|
+
dir?: string;
|
|
537
543
|
compared: number;
|
|
538
544
|
meanMae: number;
|
|
539
545
|
worstMae: number;
|
|
@@ -546,7 +552,7 @@ function checkFigures(reportPath: string): string[] {
|
|
|
546
552
|
if (animations.length === 0) return ['(the check report carried no animations — read it before believing this run)'];
|
|
547
553
|
return animations.map(
|
|
548
554
|
(a) =>
|
|
549
|
-
` ${a.animation.padEnd(12)} ${a.compared} frame(s) mean MAE ${a.meanMae.toFixed(4)} worst ${a.worstMae.toFixed(4)}` +
|
|
555
|
+
` ${(a.animation ?? `(${a.dir ?? 'setup'})`).padEnd(12)} ${a.compared} frame(s) mean MAE ${a.meanMae.toFixed(4)} worst ${a.worstMae.toFixed(4)}` +
|
|
550
556
|
` worst drift ${a.worstDrift.toFixed(3)}px${a.worstDriftSlot ? ` on ${a.worstDriftSlot} @ f${a.worstDriftFrame}` : ''}`,
|
|
551
557
|
);
|
|
552
558
|
}
|
|
@@ -844,7 +850,7 @@ function main(): void {
|
|
|
844
850
|
{
|
|
845
851
|
interface ImagesProbe {
|
|
846
852
|
skeleton?: { images?: string };
|
|
847
|
-
skins?: Array<{ attachments?: Record<string, Record<string, { type?: string; path?: string }>> }>;
|
|
853
|
+
skins?: Array<{ attachments?: Record<string, Record<string, { type?: string; name?: string; path?: string }>> }>;
|
|
848
854
|
}
|
|
849
855
|
const probe = JSON.parse(readFileSync(source, 'utf8')) as ImagesProbe;
|
|
850
856
|
const declared = probe.skeleton?.images;
|
|
@@ -858,7 +864,10 @@ function main(): void {
|
|
|
858
864
|
for (const slot of Object.values(skin.attachments ?? {})) {
|
|
859
865
|
for (const [placeholder, att] of Object.entries(slot)) {
|
|
860
866
|
if (att.type !== undefined && att.type !== 'mesh') continue;
|
|
861
|
-
|
|
867
|
+
// The parser's own defaults: `path`, else the attachment's `name`,
|
|
868
|
+
// else its placeholder (`SkeletonJson.js:526`, `:529`, `:560`) — a
|
|
869
|
+
// stated `name` is the file the editor looks for (issue #796).
|
|
870
|
+
wanted.add(att.path ?? att.name ?? placeholder);
|
|
862
871
|
}
|
|
863
872
|
}
|
|
864
873
|
}
|