spine-rigc 1.2.2 → 1.3.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 +7 -5
- package/cli.ts +63 -6
- package/docs/AUTHORING.md +309 -18
- package/docs/FACE.md +15 -0
- package/docs/INGEST.md +1 -1
- package/package.json +25 -3
- package/src/atlas.ts +120 -2
- package/src/compile.ts +335 -0
- package/src/mesh.ts +405 -1
- package/src/pose.ts +213 -0
- package/src/rig.ts +140 -7
- package/src/types.ts +30 -0
- package/src/validate.ts +101 -24
package/src/rig.ts
CHANGED
|
@@ -557,6 +557,72 @@ export interface RigGridGenerator {
|
|
|
557
557
|
soft?: RigSoftRegion;
|
|
558
558
|
}
|
|
559
559
|
|
|
560
|
+
/**
|
|
561
|
+
* One segment stated outright, for a pull that no bone's own span describes.
|
|
562
|
+
*
|
|
563
|
+
* `from` and `to` are in the part's own pixels, y down — the frame every other
|
|
564
|
+
* point a generator takes is in (`ring.center`, `ring.hull`). The bone is
|
|
565
|
+
* resolved by name like everything else; the two points belong to this mesh
|
|
566
|
+
* alone, which is the case a bone's `length` cannot cover: two meshes over one
|
|
567
|
+
* bone that each want it to pull along a different line.
|
|
568
|
+
*/
|
|
569
|
+
export interface RigSegmentSpan {
|
|
570
|
+
bone: string;
|
|
571
|
+
from: [number, number];
|
|
572
|
+
to: [number, number];
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* How distance to a segment becomes a weight: `w = 1 / (d + radius)^power`
|
|
577
|
+
* per candidate bone, the strongest `maxBones` kept and normalised, any share
|
|
578
|
+
* under `minWeight` dropped, the rest normalised again.
|
|
579
|
+
*/
|
|
580
|
+
export interface RigSegmentsFalloff {
|
|
581
|
+
/** The exponent. Default 2. */
|
|
582
|
+
power?: number;
|
|
583
|
+
/**
|
|
584
|
+
* **Required.** Added to every distance, in the part's pixels, so a vertex
|
|
585
|
+
* ON a segment has a finite weight and the blend between two segments is as
|
|
586
|
+
* wide as this says. No default: it is a length on this part's art.
|
|
587
|
+
*/
|
|
588
|
+
radius: number;
|
|
589
|
+
/** At most this many bones pull one vertex. Default 4. */
|
|
590
|
+
maxBones?: number;
|
|
591
|
+
/** A normalised share under this is dropped. Default 0.03. */
|
|
592
|
+
minWeight?: number;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* A lattice over the part's alpha, weighted by distance to named bone segments
|
|
597
|
+
* (`buildSegmentsLattice` and `segmentWeights` in [`mesh.ts`](mesh.ts)).
|
|
598
|
+
*
|
|
599
|
+
* ⭐ The one authoring decision is `bones` — which segments may pull this part.
|
|
600
|
+
* The geometry comes off the attachment's own `image`, the segments off the
|
|
601
|
+
* skeleton's setup pose, and the weights off the distance between the two, so
|
|
602
|
+
* nothing else here is a judgement about the art.
|
|
603
|
+
*/
|
|
604
|
+
export interface RigSegmentsGenerator {
|
|
605
|
+
kind: 'segments';
|
|
606
|
+
/** **Required.** The lattice's cell, in the part's pixels, a whole number of at least 1. */
|
|
607
|
+
cell: number;
|
|
608
|
+
/**
|
|
609
|
+
* **Required, at least one entry.** Each is a bone name (origin to its
|
|
610
|
+
* `length` tip), a chain — a list of bone names, root first, each link
|
|
611
|
+
* running from its origin to the next link's (the last to its `length` tip) —
|
|
612
|
+
* or a `RigSegmentSpan` stated outright.
|
|
613
|
+
*/
|
|
614
|
+
bones: Array<string | string[] | RigSegmentSpan>;
|
|
615
|
+
/** The falloff — see `RigSegmentsFalloff`. `radius` is required, so the block is too. */
|
|
616
|
+
falloff: RigSegmentsFalloff;
|
|
617
|
+
/** Alpha at or above which a pixel counts as art, 1..255. Default 1 — `contour`'s own. */
|
|
618
|
+
alpha?: number;
|
|
619
|
+
/**
|
|
620
|
+
* Where the slot bone sits in the part's pixels, y down. Default the window's
|
|
621
|
+
* centre — the placement every generator on this route uses.
|
|
622
|
+
*/
|
|
623
|
+
anchor?: [number, number];
|
|
624
|
+
}
|
|
625
|
+
|
|
560
626
|
/**
|
|
561
627
|
* Which builder in `src/mesh.ts` makes this mesh's geometry, and its parameters.
|
|
562
628
|
*
|
|
@@ -570,10 +636,15 @@ export interface RigGridGenerator {
|
|
|
570
636
|
* and measured art lives in the manifest. `generator` is for a skeleton with no
|
|
571
637
|
* manifest behind it.
|
|
572
638
|
*/
|
|
573
|
-
export type RigMeshGenerator =
|
|
639
|
+
export type RigMeshGenerator =
|
|
640
|
+
| RigRingGenerator
|
|
641
|
+
| RigRibbonGenerator
|
|
642
|
+
| RigContourGenerator
|
|
643
|
+
| RigGridGenerator
|
|
644
|
+
| RigSegmentsGenerator;
|
|
574
645
|
|
|
575
|
-
/** The
|
|
576
|
-
export const RIG_GENERATOR_KINDS = ['ring', 'ribbon', 'contour', 'grid'] as const;
|
|
646
|
+
/** The five `kind` names a generator may carry, and the order `RIG_KEYS` takes them in. */
|
|
647
|
+
export const RIG_GENERATOR_KINDS = ['ring', 'ribbon', 'contour', 'grid', 'segments'] as const;
|
|
577
648
|
|
|
578
649
|
/**
|
|
579
650
|
* The attachment's own NAME, as distinct from the placeholder key it is filed
|
|
@@ -1591,6 +1662,35 @@ export interface RigInvariants {
|
|
|
1591
1662
|
* left to measure, and on the build's stats line when something is.
|
|
1592
1663
|
*/
|
|
1593
1664
|
consumerDrivenMix?: RigConsumerDrivenMix[];
|
|
1665
|
+
/**
|
|
1666
|
+
* The rig's `idle` deforms meshes **on purpose**, so
|
|
1667
|
+
* `A15_IDLE_NO_MESH_BONE_KEYS` reports the renderer cost instead of refusing
|
|
1668
|
+
* each keyed bone (issues #855, #858).
|
|
1669
|
+
*
|
|
1670
|
+
* 🔑 **What A15 assumes, and the genre that breaks it.** The rule exists for a
|
|
1671
|
+
* renderer that skips redrawing a mesh nothing moved, so an `idle` keying a
|
|
1672
|
+
* mesh-driving bone spends that skip on every frame — right for a rig whose
|
|
1673
|
+
* meshes are mostly static, wrong for a painting rig: one illustration
|
|
1674
|
+
* decomposed into layers, most of them weighted meshes over bone chains, with
|
|
1675
|
+
* an `idle` whose whole job is to move them (hair, sleeves, breathing). The
|
|
1676
|
+
* only way through without this field was a same-origin parent above every
|
|
1677
|
+
* keyed bone — 42 extra bones on the first such rig, the same pose, the rule's
|
|
1678
|
+
* wording met and its purpose not.
|
|
1679
|
+
*
|
|
1680
|
+
* 🚨 **An opt-OUT, held to `deformMayFold`'s standard.** The shape is
|
|
1681
|
+
* `{ "why": … }` and a missing, blank or non-string `why` is refused by name.
|
|
1682
|
+
* What it buys is a **SKIP, never a pass**, and the SKIP carries the cost —
|
|
1683
|
+
* the keyed bones, how many mesh attachments they drive and how many vertices
|
|
1684
|
+
* those hold. A declaration on a rig whose `idle` keys no mesh-driving bone is
|
|
1685
|
+
* refused at the gate: it would switch off nothing while reading like it did.
|
|
1686
|
+
*/
|
|
1687
|
+
idleDrivesMeshes?: RigIdleDrivesMeshes;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/** The rig's `idle` is meant to deform meshes — `invariants.idleDrivesMeshes` (`A15`). */
|
|
1691
|
+
export interface RigIdleDrivesMeshes {
|
|
1692
|
+
/** Required, and blank is refused by name — see `idleDrivesMeshes`. */
|
|
1693
|
+
why: string;
|
|
1594
1694
|
}
|
|
1595
1695
|
|
|
1596
1696
|
/** One constraint whose mix the consumer drives — `invariants.consumerDrivenMix`. */
|
|
@@ -1696,7 +1796,8 @@ export const RIG_KEYS = {
|
|
|
1696
1796
|
RigBoneFrom: ['anchor', 'slotWindow', 'meshCenter', 'rotation'],
|
|
1697
1797
|
RigSlot: ['name', 'bone', 'attachment', 'color', 'dark', 'blend'],
|
|
1698
1798
|
RigEvent: ['int', 'float', 'string', 'audio', 'volume', 'balance'],
|
|
1699
|
-
RigInvariants: ['meshSlots', 'meshTriangles', 'axisBone', 'massBone', 'detached', 'deformMayFold', 'editorRoundTrip', 'consumerDrivenMix'],
|
|
1799
|
+
RigInvariants: ['meshSlots', 'meshTriangles', 'axisBone', 'massBone', 'detached', 'deformMayFold', 'editorRoundTrip', 'consumerDrivenMix', 'idleDrivesMeshes'],
|
|
1800
|
+
RigIdleDrivesMeshes: ['why'],
|
|
1700
1801
|
RigDetachedRule: ['bone', 'notUnder', 'why'],
|
|
1701
1802
|
RigDeformFoldExemption: ['slot', 'why'],
|
|
1702
1803
|
RigConsumerDrivenMix: ['constraint', 'type', 'why'],
|
|
@@ -1745,6 +1846,9 @@ export const RIG_KEYS = {
|
|
|
1745
1846
|
RigRibbonGenerator: ['kind', 'size', 'rows', 'chain'],
|
|
1746
1847
|
RigContourGenerator: ['kind', 'tolerance', 'margin', 'maxVertices', 'alpha', 'depth', 'soft'],
|
|
1747
1848
|
RigGridGenerator: ['kind', 'us', 'vs', 'cols', 'rows', 'depth', 'soft'],
|
|
1849
|
+
RigSegmentsGenerator: ['kind', 'cell', 'bones', 'falloff', 'alpha', 'anchor'],
|
|
1850
|
+
RigSegmentsFalloff: ['power', 'radius', 'maxBones', 'minWeight'],
|
|
1851
|
+
RigSegmentSpan: ['bone', 'from', 'to'],
|
|
1748
1852
|
RigMeshBias: ['axis_deg', 'ramp'],
|
|
1749
1853
|
RigDepthMap: ['image', 'near', 'zScale', 'gamma', 'contrast', 'bias'],
|
|
1750
1854
|
RigSoftRegion: ['bone', 'mask'],
|
|
@@ -1781,6 +1885,7 @@ const GENERATOR_SHAPE: Record<string, keyof typeof RIG_KEYS> = {
|
|
|
1781
1885
|
ribbon: 'RigRibbonGenerator',
|
|
1782
1886
|
contour: 'RigContourGenerator',
|
|
1783
1887
|
grid: 'RigGridGenerator',
|
|
1888
|
+
segments: 'RigSegmentsGenerator',
|
|
1784
1889
|
};
|
|
1785
1890
|
|
|
1786
1891
|
/**
|
|
@@ -1926,6 +2031,7 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
|
|
|
1926
2031
|
for (const [i, rule] of (Array.isArray(raw.invariants.consumerDrivenMix) ? raw.invariants.consumerDrivenMix : []).entries()) {
|
|
1927
2032
|
at(rule, 'RigConsumerDrivenMix', `invariants.consumerDrivenMix[${i}]`);
|
|
1928
2033
|
}
|
|
2034
|
+
at(raw.invariants.idleDrivesMeshes, 'RigIdleDrivesMeshes', 'invariants.idleDrivesMeshes');
|
|
1929
2035
|
}
|
|
1930
2036
|
|
|
1931
2037
|
for (const [skinName, skin] of Object.entries(isObj(raw.skins) ? raw.skins : {})) {
|
|
@@ -1985,6 +2091,10 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
|
|
|
1985
2091
|
at(gen.bias, 'RigMeshBias', `${who} generator.bias`);
|
|
1986
2092
|
at(gen.depth, 'RigDepthMap', `${who} generator.depth`);
|
|
1987
2093
|
at(gen.soft, 'RigSoftRegion', `${who} generator.soft`);
|
|
2094
|
+
at(gen.falloff, 'RigSegmentsFalloff', `${who} generator.falloff`);
|
|
2095
|
+
for (const [i, entry] of (Array.isArray(gen.bones) ? gen.bones : []).entries()) {
|
|
2096
|
+
at(entry, 'RigSegmentSpan', `${who} generator.bones[${i}]`);
|
|
2097
|
+
}
|
|
1988
2098
|
}
|
|
1989
2099
|
}
|
|
1990
2100
|
}
|
|
@@ -2134,9 +2244,9 @@ export function parseRigSpec(raw: unknown, where: string): RigSpec {
|
|
|
2134
2244
|
}
|
|
2135
2245
|
}
|
|
2136
2246
|
|
|
2137
|
-
// `invariants.deformMayFold` — one of the
|
|
2138
|
-
// CHECK OFF (`consumerDrivenMix`, below,
|
|
2139
|
-
// checked harder than the fields that turn one on. A
|
|
2247
|
+
// `invariants.deformMayFold` — one of the three fields in this file that TURN
|
|
2248
|
+
// A CHECK OFF (`consumerDrivenMix` and `idleDrivesMeshes`, below, are the
|
|
2249
|
+
// others), so its own shape is checked harder than the fields that turn one on. A
|
|
2140
2250
|
// typo in a slot name here would silently exempt nothing and gate everything,
|
|
2141
2251
|
// which reads exactly like the check working; and an exemption with no reason
|
|
2142
2252
|
// is unreviewable six months later. Both are refused by name.
|
|
@@ -2265,6 +2375,29 @@ export function parseRigSpec(raw: unknown, where: string): RigSpec {
|
|
|
2265
2375
|
}
|
|
2266
2376
|
}
|
|
2267
2377
|
|
|
2378
|
+
// `invariants.idleDrivesMeshes` — the third field that TURNS A CHECK OFF
|
|
2379
|
+
// (issues #855, #858), so it is held to the same standard: the one shape it
|
|
2380
|
+
// accepts is `{ "why": … }`, and a `why` that is missing, blank or not a
|
|
2381
|
+
// string is refused by name. `true` is refused too, even though it reads like
|
|
2382
|
+
// the obvious spelling — a switch with no reason attached is exactly the
|
|
2383
|
+
// exemption nobody can review later. Whether the declaration switches off
|
|
2384
|
+
// anything is a question about the emitted `idle`, so it is the gate's (A15
|
|
2385
|
+
// refuses a stale one), not this parser's.
|
|
2386
|
+
const idleDrives = spec.invariants?.idleDrivesMeshes;
|
|
2387
|
+
if (idleDrives !== undefined) {
|
|
2388
|
+
const shape = '{ "why": "<why this idle deforms meshes on purpose>" }';
|
|
2389
|
+
if (!isObj(idleDrives)) {
|
|
2390
|
+
throw new CompileError(`${where}: invariants.idleDrivesMeshes is ${JSON.stringify(idleDrives)}, expected ${shape}`);
|
|
2391
|
+
}
|
|
2392
|
+
if (typeof idleDrives.why !== 'string' || idleDrives.why.trim().length === 0) {
|
|
2393
|
+
throw new CompileError(
|
|
2394
|
+
`${where}: invariants.idleDrivesMeshes needs a "why" (a non-blank string), got ` +
|
|
2395
|
+
`${idleDrives.why === undefined ? 'none' : JSON.stringify(idleDrives.why)} — expected ${shape}. This field switches ` +
|
|
2396
|
+
'A15_IDLE_NO_MESH_BONE_KEYS off, and an exemption nobody can date or justify is how a defect ships as a decision',
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2268
2401
|
// --- skins: the attachment table, and what the skin ACTIVATES --------------
|
|
2269
2402
|
//
|
|
2270
2403
|
// The lists resolve by name like everything else in this format, and the
|
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
|
}
|