spine-rigc 1.2.3 → 1.4.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 +2 -2
- package/cli.ts +124 -20
- package/docs/AUTHORING.md +471 -19
- package/docs/FACE.md +15 -0
- package/docs/INGEST.md +1 -1
- package/package.json +17 -1
- package/src/atlas.ts +151 -2
- package/src/compile.ts +335 -0
- package/src/mesh.ts +405 -1
- package/src/pose.ts +297 -10
- package/src/render.ts +376 -21
- package/src/rig.ts +140 -7
- package/src/types.ts +30 -0
- package/src/validate.ts +101 -24
package/src/types.ts
CHANGED
|
@@ -1414,6 +1414,14 @@ export interface RigInfo {
|
|
|
1414
1414
|
* muted constraint as before (issue #784).
|
|
1415
1415
|
*/
|
|
1416
1416
|
consumerDrivenMix: Array<{ type: 'ik' | 'transform'; constraint: string; why: string }>;
|
|
1417
|
+
/**
|
|
1418
|
+
* The `why` of `invariants.idleDrivesMeshes`, or null when the rig does not
|
|
1419
|
+
* declare that its `idle` deforms meshes on purpose. Declared, `A15` SKIPs
|
|
1420
|
+
* with the renderer cost it measured, or FAILs when the `idle` keys no
|
|
1421
|
+
* mesh-driving bone and the declaration switches off nothing. A bare
|
|
1422
|
+
* `validate <dir>` has no rig, so null, and A15 refuses per bone as before.
|
|
1423
|
+
*/
|
|
1424
|
+
idleDrivesMeshes: string | null;
|
|
1417
1425
|
/**
|
|
1418
1426
|
* The atlas region(s) the build names as its base plate — the one image
|
|
1419
1427
|
* `A19_OVERLAY_PNGS_HAVE_ALPHA` lets be opaque — in compile order.
|
|
@@ -1557,6 +1565,28 @@ export interface CompileResult {
|
|
|
1557
1565
|
* and a nose does not wobble.
|
|
1558
1566
|
*/
|
|
1559
1567
|
soft?: { mask: string; digest: string; bone: string; carried: number; ramped: number };
|
|
1568
|
+
/**
|
|
1569
|
+
* How a `segments` mesh's bones share its vertices — the figures `build`
|
|
1570
|
+
* and `explain` print under its line. Only `segments` has one: it is the
|
|
1571
|
+
* generator whose weights are the whole point, and every other generator's
|
|
1572
|
+
* weighting is fixed by its kind.
|
|
1573
|
+
*/
|
|
1574
|
+
influence?: {
|
|
1575
|
+
/** Most bones any one vertex binds, and the mean over the mesh. */
|
|
1576
|
+
maxBones: number;
|
|
1577
|
+
meanBones: number;
|
|
1578
|
+
/** Vertices bound to exactly one bone. */
|
|
1579
|
+
singleBone: number;
|
|
1580
|
+
/** The bones some vertex binds, in the order `bones` names them. */
|
|
1581
|
+
bound: string[];
|
|
1582
|
+
/** The lattice: its cell, cells across and down, cells with art, cells kept, islands joined. */
|
|
1583
|
+
cell: number;
|
|
1584
|
+
cols: number;
|
|
1585
|
+
rows: number;
|
|
1586
|
+
artCells: number;
|
|
1587
|
+
keptCells: number;
|
|
1588
|
+
islands: number;
|
|
1589
|
+
};
|
|
1560
1590
|
/**
|
|
1561
1591
|
* What a depth map put on this mesh's vertices, when one was named.
|
|
1562
1592
|
*
|
package/src/validate.ts
CHANGED
|
@@ -2317,23 +2317,57 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
2317
2317
|
});
|
|
2318
2318
|
|
|
2319
2319
|
// --- A15: idle must not key a mesh-driving bone (dirty-skip lever) -----
|
|
2320
|
+
//
|
|
2321
|
+
// 🔑 The rule assumes meshes are mostly static: the renderer it serves skips
|
|
2322
|
+
// redrawing a mesh nothing moved, and an `idle` keying one of its bones spends
|
|
2323
|
+
// that skip on every frame. A painting rig is the genre where the assumption
|
|
2324
|
+
// is false by design (issues #855, #858) — one illustration in layers, most
|
|
2325
|
+
// of them weighted meshes, and an `idle` whose job is to move them — so the
|
|
2326
|
+
// rig can say so in `invariants.idleDrivesMeshes`, and the rule then reports
|
|
2327
|
+
// what the declaration costs instead of refusing each bone.
|
|
2320
2328
|
check('A15_IDLE_NO_MESH_BONE_KEYS', () => {
|
|
2321
|
-
const
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2329
|
+
const A15 = 'A15_IDLE_NO_MESH_BONE_KEYS';
|
|
2330
|
+
/**
|
|
2331
|
+
* Every mesh attachment, loaded, with the bones that drive it — its slot's
|
|
2332
|
+
* bone, and, once weights exist, every bone its weights name. A ring mesh
|
|
2333
|
+
* is driven by its CONTROL bone, a different bone from the slot's, and
|
|
2334
|
+
* checking only the slot bone would let `idle` key the one bone that
|
|
2335
|
+
* actually dirties the canvas every frame.
|
|
2336
|
+
*/
|
|
2337
|
+
const meshDrivers: Array<{ mesh: MeshAttachment; bones: Set<string> }> = [];
|
|
2338
|
+
for (const skin of data.skins) {
|
|
2339
|
+
for (const entry of skin.getAttachments()) {
|
|
2340
|
+
const mesh = entry.attachment;
|
|
2341
|
+
if (!(mesh instanceof MeshAttachment)) continue;
|
|
2342
|
+
const bones = new Set<string>([data.slots[entry.slotIndex].boneData.name]);
|
|
2343
|
+
if (mesh.bones) {
|
|
2344
|
+
for (let i = 0; i < mesh.bones.length; ) {
|
|
2345
|
+
const boneCount = mesh.bones[i++];
|
|
2346
|
+
for (let n = 0; n < boneCount; n++, i++) {
|
|
2347
|
+
const bone = data.bones[mesh.bones[i]];
|
|
2348
|
+
if (bone) bones.add(bone.name);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2334
2351
|
}
|
|
2352
|
+
meshDrivers.push({ mesh, bones });
|
|
2335
2353
|
}
|
|
2336
2354
|
}
|
|
2355
|
+
const meshBoneNames = new Set<string>();
|
|
2356
|
+
for (const { bones } of meshDrivers) for (const name of bones) meshBoneNames.add(name);
|
|
2357
|
+
const declared = input.rig?.idleDrivesMeshes ?? null;
|
|
2358
|
+
/**
|
|
2359
|
+
* A declaration that switches off nothing is refused rather than skipped,
|
|
2360
|
+
* the standard `consumerDrivenMix` is held to: an opt-out that exempts
|
|
2361
|
+
* nothing reads exactly like one that worked, and the next reader cannot
|
|
2362
|
+
* tell the rig that needs it from the rig it was copied onto.
|
|
2363
|
+
*/
|
|
2364
|
+
const stale = (why: string): void => {
|
|
2365
|
+
fail(
|
|
2366
|
+
A15,
|
|
2367
|
+
`the rig "${input.rig?.archetype}" declares invariants.idleDrivesMeshes ("${declared}"), but ${why}, so the ` +
|
|
2368
|
+
'declaration switches off nothing — remove it',
|
|
2369
|
+
);
|
|
2370
|
+
};
|
|
2337
2371
|
// The same shape as A06's and A17's guards, found by auditing for it
|
|
2338
2372
|
// (#568): a rig with no `idle` at all has nothing here to be wrong, and a
|
|
2339
2373
|
// rule that reports "held" over a subject that does not exist is the
|
|
@@ -2342,15 +2376,45 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
2342
2376
|
// versus one that keys no bone.
|
|
2343
2377
|
const idle = isObj(raw?.animations) ? (raw.animations as Json).idle : undefined;
|
|
2344
2378
|
if (!isObj(idle)) {
|
|
2345
|
-
return
|
|
2379
|
+
if (declared !== null) return stale('the skeleton declares no "idle" animation');
|
|
2380
|
+
return skip(A15, 'the skeleton declares no "idle" animation, so nothing here can key a mesh-driving bone');
|
|
2346
2381
|
}
|
|
2347
2382
|
if (!isObj(idle.bones)) {
|
|
2348
|
-
return
|
|
2383
|
+
if (declared !== null) return stale('"idle" carries no bone timeline at all');
|
|
2384
|
+
return skip(A15, '"idle" carries no bone timeline at all, so there is no key to hold against the mesh-driving bones');
|
|
2349
2385
|
}
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2386
|
+
const keyed = Object.keys(idle.bones as Json).filter((name) => meshBoneNames.has(name));
|
|
2387
|
+
if (declared !== null) {
|
|
2388
|
+
if (keyed.length === 0) return stale('"idle" keys no bone that drives a mesh');
|
|
2389
|
+
const keyedSet = new Set(keyed);
|
|
2390
|
+
const moved = meshDrivers.filter(({ bones }) => [...bones].some((name) => keyedSet.has(name)));
|
|
2391
|
+
// `worldVerticesLength` is two numbers per vertex whatever the encoding,
|
|
2392
|
+
// which is the count the runtime recomputes; reading `vertices.length`
|
|
2393
|
+
// would count a weighted mesh's bone entries instead.
|
|
2394
|
+
const vertices = moved.reduce((sum, { mesh }) => sum + mesh.worldVerticesLength / 2, 0);
|
|
2395
|
+
const SHOWN = 8;
|
|
2396
|
+
const names =
|
|
2397
|
+
keyed.slice(0, SHOWN).map((name) => `"${name}"`).join(', ') + (keyed.length > SHOWN ? ` +${keyed.length - SHOWN}` : '');
|
|
2398
|
+
return skip(
|
|
2399
|
+
A15,
|
|
2400
|
+
`declared by the rig ("${declared}"): idle keys ${keyed.length} bone(s) that drive ${moved.length} mesh ` +
|
|
2401
|
+
`attachment(s) totalling ${vertices} vertices — ${names} — and each of those meshes is recomputed on every ` +
|
|
2402
|
+
'frame it is shown',
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
for (const [i, boneName] of keyed.entries()) {
|
|
2406
|
+
// The case is named once, on the first line an agent reads, rather than
|
|
2407
|
+
// after every bone: the first painting rig this met printed 42 of these,
|
|
2408
|
+
// and 42 identical sentences read as 42 separate mistakes. It rides the
|
|
2409
|
+
// first finding rather than being a finding of its own, so the count of
|
|
2410
|
+
// FAIL lines is still the count of bones.
|
|
2411
|
+
const hint =
|
|
2412
|
+
i > 0
|
|
2413
|
+
? ''
|
|
2414
|
+
: `. ${keyed.length} bone(s) keyed by idle drive meshes; if this idle is meant to deform them (a painting ` +
|
|
2415
|
+
'rig), declare invariants.idleDrivesMeshes: { "why": … } in the rig spec — or, where the motion belongs to ' +
|
|
2416
|
+
'a pivot above the mesh, key that pivot one link up (FACE.md §3)';
|
|
2417
|
+
fail(A15, `idle keys bone "${boneName}", which drives a mesh — meshes never idle-skip${hint}`);
|
|
2354
2418
|
}
|
|
2355
2419
|
});
|
|
2356
2420
|
|
|
@@ -2532,13 +2596,26 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
2532
2596
|
// a linked mesh has no rim of its OWN at all — the one it draws belongs to
|
|
2533
2597
|
// its source, and is measured there. Nothing to measure is a SKIP — never
|
|
2534
2598
|
// a pass, and never a failure on somebody else's correct geometry.
|
|
2535
|
-
|
|
2599
|
+
//
|
|
2600
|
+
// 🔸 A `segments` mesh is rigc's own geometry and still has no rim to pin:
|
|
2601
|
+
// its outline is traced off the art and weighted by distance like every
|
|
2602
|
+
// other vertex, because a layer pulled by named bones is SUPPOSED to move
|
|
2603
|
+
// at its edge. "Pinned to the slot bone" is not a claim that generator
|
|
2604
|
+
// makes, so there is nothing here to hold it to — and `A20`'s coherence
|
|
2605
|
+
// rules, which it does make, still apply to it in full.
|
|
2606
|
+
const rimless = (m: MeshAttachment): boolean => kindOf(m) === 'authored' || kindOf(m) === 'segments';
|
|
2607
|
+
const measurable = meshAttachments.filter((m) => m.bones && !rimless(m));
|
|
2536
2608
|
if (measurable.length === 0) {
|
|
2537
2609
|
const authored = authoredMeshNames(meshAttachments.filter((m) => m.bones));
|
|
2610
|
+
const segmented = meshAttachments.filter((m) => m.bones && kindOf(m) === 'segments').map((m) => `"${m.name}"`);
|
|
2538
2611
|
return skip(
|
|
2539
2612
|
'A21_MESH_RIM_PINNED',
|
|
2540
|
-
|
|
2541
|
-
|
|
2613
|
+
segmented.length === 0
|
|
2614
|
+
? `every weighted mesh here is authored or linked geometry (${authored.join(', ')}), not a rigc ring or ` +
|
|
2615
|
+
'ribbon — rigc did not place its rim, so it has no rim of its own to find unpinned'
|
|
2616
|
+
: `every weighted mesh here is ${authored.length ? `authored or linked geometry (${authored.join(', ')}) or ` : ''}` +
|
|
2617
|
+
`a "segments" lattice (${segmented.join(', ')}), not a rigc ring or ribbon — a segments mesh weights its ` +
|
|
2618
|
+
'outline by distance to the bones it names, so it has no rim pinned to the slot bone to find unpinned',
|
|
2542
2619
|
);
|
|
2543
2620
|
}
|
|
2544
2621
|
for (const mesh of measurable) {
|
|
@@ -5831,12 +5908,12 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
5831
5908
|
// of one whose meshes are contours — and the last two are cases where a
|
|
5832
5909
|
// reader should know that a mesh went unmeasured on purpose.
|
|
5833
5910
|
const unpaired = Object.entries(kinds)
|
|
5834
|
-
.filter(([, kind]) => kind === 'authored' || kind === 'contour')
|
|
5911
|
+
.filter(([, kind]) => kind === 'authored' || kind === 'contour' || kind === 'segments')
|
|
5835
5912
|
.map(([slot, kind]) => `"${slot}" (${kind})`);
|
|
5836
5913
|
return skip(
|
|
5837
5914
|
'A28_RIBBON_ROWS_SHARE_WEIGHTS',
|
|
5838
5915
|
unpaired.length
|
|
5839
|
-
? `the rig "${input.rig.archetype}" declares no ribbon mesh on this cut — its mesh slot(s) ${unpaired.join(', ')} have no rows to pair: authored geometry is somebody else's topology,
|
|
5916
|
+
? `the rig "${input.rig.archetype}" declares no ribbon mesh on this cut — its mesh slot(s) ${unpaired.join(', ')} have no rows to pair: authored geometry is somebody else's topology, a contour is one silhouette loop, and a segments lattice has cells rather than cross rows`
|
|
5840
5917
|
: `the rig "${input.rig.archetype}" declares no ribbon mesh on this cut`,
|
|
5841
5918
|
);
|
|
5842
5919
|
}
|