spine-rigc 0.12.0 โ 0.14.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 +3 -2
- package/cli.ts +305 -2
- package/docs/AUTHORING.md +554 -12
- package/docs/FACE.md +1184 -0
- package/docs/INGEST.md +16 -4
- package/docs/MOTION.md +77 -2
- package/package.json +2 -1
- package/src/compile.ts +374 -28
- package/src/deformgen.ts +405 -0
- package/src/deformmeasure.ts +453 -0
- package/src/motion.ts +423 -0
- package/src/pose.ts +44 -5
- package/src/render.ts +121 -19
- package/src/rig.ts +56 -0
- package/src/trackgen.ts +354 -0
- package/src/types.ts +129 -3
- package/src/validate.ts +153 -3
package/README.md
CHANGED
|
@@ -398,7 +398,7 @@ was verified, and what writing it cost. Repository material: a clone and
|
|
|
398
398
|
| Example | Stars | What it is |
|
|
399
399
|
| --- | --- | --- |
|
|
400
400
|
| [`gallery/walk`](https://github.com/firejune/rigc/tree/main/gallery/walk) | `ik` constraints + **`ik` timelines** | Two two-bone leg chains solved to foot targets โ the planted leg nailed down, the swinging one let go at the top of its lift |
|
|
401
|
-
| [`gallery/squash`](https://github.com/firejune/rigc/tree/main/gallery/squash) | **`deform` timelines** | A ball squashed about its contact point and stretched along its travel, from two affine transforms
|
|
401
|
+
| [`gallery/squash`](https://github.com/firejune/rigc/tree/main/gallery/squash) | **`deform` timelines** | A ball squashed about its contact point and stretched along its travel, from two affine transforms the keys state rather than tabulate |
|
|
402
402
|
| [`gallery/flex`](https://github.com/firejune/rigc/tree/main/gallery/flex) | **`contour` meshes** | A swallow-tailed banner and a serrated leaf: four meshes traced off their own alpha, waved by bone timelines and rippled by a `deform` |
|
|
403
403
|
| [`gallery/ride`](https://github.com/firejune/rigc/tree/main/gallery/ride) | `path` attachments + **path constraints** | A trolley coasting down a drawn rail and rolling back, driven by a `position` timeline, with `groups` + `stagger` keying the wheels and the ears |
|
|
404
404
|
|
|
@@ -448,6 +448,7 @@ the art. Its shape is under
|
|
|
448
448
|
| --- | --- |
|
|
449
449
|
| ๐ **[docs/AUTHORING.md](docs/AUTHORING.md)** | **the format guide, and the one to read before writing a spec.** Both input files field by field with a complete minimal example each, every field with its Spine meaning, the rules that decide what is emitted, the build โ read the report โ fix โ repeat loop, the map from every named failure to the file that has to change, and the features rigc refuses by name so you do not spend a loop discovering them. It travels **inside the npm package**, at `node_modules/spine-rigc/docs/AUTHORING.md` |
|
|
450
450
|
| ๐๏ธ **[docs/MOTION.md](docs/MOTION.md)** | **the key-pose recipe.** How to get two poses, what a pair of poses does and does not fix, the in-betweening rules and where each comes from, and how to spread candidates so a ballot informs. Ships in the package too |
|
|
451
|
+
| ๐ **[docs/FACE.md](docs/FACE.md)** | **authoring a face.** A blink, a gaze and a 2.5D head turn on plain Spine data: the one line of yaw arithmetic every number in a turn comes from, depth as the parameter you are actually authoring, where to put a grid's columns and the angle at which any grid folds, what foreshortens and what does not, channel allocation before the first key, and the three cliffs with their angles. Also the deform audit gap, demonstrated โ a folded mesh gates green โ and the differential check that works today |
|
|
451
452
|
| ๐ฅ **[docs/INGEST.md](docs/INGEST.md)** | **working with a skeleton you did not author.** What every command can and cannot do with a foreign `skeleton.json`, reading it with the toolchain, transcription as the route that makes it yours, what each validator complaint means on an export, and the re-pivot/rename/extend recipes. Ships in the package too |
|
|
452
453
|
| ๐ค **[docs/PROMPTING.md](docs/PROMPTING.md)** | **handing the authoring to an AI agent** โ the prompt clauses a measured pilot run paid for, and what you can leave unsaid. Ships in the package too |
|
|
453
454
|
| ๐ฌ **[docs/SPEC_COVERAGE.md](docs/SPEC_COVERAGE.md)** | Spine 4.3's full export surface against what rigc emits and what the official examples measurably use, with the ordered gap list |
|
|
@@ -490,7 +491,7 @@ that an agent authors a spineboy-scale rig from the brief alone in one run: the
|
|
|
490
491
|
ladder has not demonstrated that, and each row records which of the two it is.
|
|
491
492
|
|
|
492
493
|
The whole dossier โ the yardstick, `diff` and `check` and what neither of them can
|
|
493
|
-
see, every rung, the run viewer, the
|
|
494
|
+
see, every rung, the run viewer, the 40 assertions and the selftest behind them โ is
|
|
494
495
|
[docs/BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
|
|
495
496
|
Live rung status is
|
|
496
497
|
[docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
|
package/cli.ts
CHANGED
|
@@ -58,10 +58,17 @@ import {
|
|
|
58
58
|
} from './src/bonedist.ts';
|
|
59
59
|
import { checkAgainstFrames, checkLines, CheckError, type CheckOptions, type CheckReport } from './src/check.ts';
|
|
60
60
|
import { compile, CompileError, type CompileOptions } from './src/compile.ts';
|
|
61
|
+
import {
|
|
62
|
+
skeletonDataFromText,
|
|
63
|
+
surveyDeformKeys,
|
|
64
|
+
type DeformExtreme,
|
|
65
|
+
type DeformKeyMeasure,
|
|
66
|
+
} from './src/deformmeasure.ts';
|
|
61
67
|
import { diffLines, diffSkeletons, reportedFigures, sectionFigures, type DiffReport } from './src/diff.ts';
|
|
62
68
|
import { copyAtlasImages } from './src/emit.ts';
|
|
63
69
|
import { DEFAULT_PADDING, DEFAULT_PAGE_SIZE, packAtlas } from './src/atlas.ts';
|
|
64
70
|
import { parseJsonWithPosition } from './src/json-position.ts';
|
|
71
|
+
import { KEY_TIME_EPSILON } from './src/timelines.ts';
|
|
65
72
|
import { findRung, RUNG_IDS, type RungSkeleton } from './src/ladder.ts';
|
|
66
73
|
import {
|
|
67
74
|
DEFAULT_MAX_RESIDUAL,
|
|
@@ -105,7 +112,8 @@ import {
|
|
|
105
112
|
type FrameSet,
|
|
106
113
|
} from './src/render.ts';
|
|
107
114
|
import { CLI_DEFAULT_PROFILE, reportLines, validate, VALIDATE_PROFILES, type ValidateProfile } from './src/validate.ts';
|
|
108
|
-
import
|
|
115
|
+
import { parseMotionSpec } from './src/motion.ts';
|
|
116
|
+
import type { CompileResult } from './src/types.ts';
|
|
109
117
|
|
|
110
118
|
/**
|
|
111
119
|
* One entry of a cuts.json, every path relative to the cuts.json file.
|
|
@@ -431,6 +439,264 @@ function meshBudget(rig: CompileResult['rig']): string {
|
|
|
431
439
|
return rig.meshTriangleBudget === null ? '(no budget declared)' : `(budget ${rig.meshTriangleBudget})`;
|
|
432
440
|
}
|
|
433
441
|
|
|
442
|
+
/**
|
|
443
|
+
* One extreme, as `x0.637306 tri 0`, or an em dash when no triangle on the key
|
|
444
|
+
* could carry the quantity.
|
|
445
|
+
*
|
|
446
|
+
* A dash rather than `x1.000000`: a key over a mesh whose every triangle is a
|
|
447
|
+
* hair has no ratio and no map, and printing the identity there would report a
|
|
448
|
+
* measurement that was never taken โ this repository's favourite false green.
|
|
449
|
+
*/
|
|
450
|
+
function deformExtreme(extreme: DeformExtreme | null): string {
|
|
451
|
+
return extreme === null ? 'โ'.padEnd(9) : `x${extreme.value.toFixed(6)} tri ${extreme.triangle}`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* The `MEMBER` report block โ a group track's per-member values, side by side
|
|
456
|
+
* (issue #295).
|
|
457
|
+
*
|
|
458
|
+
* ## Why side by side is the whole point
|
|
459
|
+
*
|
|
460
|
+
* The complaint that filed #295 was not the line count. `gallery/portrait`'s
|
|
461
|
+
* held yaw put six sibling bones' `translatex` in six separate tracks, and the
|
|
462
|
+
* reason that is bad is that **nobody can see a wrong sign in a column that is
|
|
463
|
+
* eighty lines from its neighbours.** FACE ยง3 makes the same argument from the
|
|
464
|
+
* other side: a residual is 1โ6 units where a total is 30โ40, and the split is
|
|
465
|
+
* *an auditing decision before it is a rigging one*. So the report's job is to
|
|
466
|
+
* put the numbers in the arrangement the audit needs โ one row per member, one
|
|
467
|
+
* block per key โ which is exactly the arrangement the emitted format cannot
|
|
468
|
+
* have, because Spine keys one bone per timeline.
|
|
469
|
+
*
|
|
470
|
+
* ## It quotes; it does not re-derive
|
|
471
|
+
*
|
|
472
|
+
* The same rule as the `DEFORM` block. Every value here is the one the compiler
|
|
473
|
+
* **emitted**, carried on `result.trackDerivations`, so the block and the
|
|
474
|
+
* artifact cannot disagree. `derived` and `formula` are the model's own strings
|
|
475
|
+
* from `src/trackgen.ts`, so the block names the closed form the spec stated
|
|
476
|
+
* rather than a second reading of it.
|
|
477
|
+
*
|
|
478
|
+
* ## What it deliberately does not print
|
|
479
|
+
*
|
|
480
|
+
* **Tracks whose members all share one value** โ the ordinary `groups` entry.
|
|
481
|
+
* There is one number there and the timelines above already show it on every
|
|
482
|
+
* member; a table of six identical rows would be a tautology, and the block
|
|
483
|
+
* exists to make a *difference* visible. `look_l`/`look_r` in the worked example
|
|
484
|
+
* are exactly that case and they are right to be absent from here.
|
|
485
|
+
*
|
|
486
|
+
* **`stagger`.** A per-member time offset is printed as it always was โ on each
|
|
487
|
+
* member's own timeline, where the shifted key times are. Repeating it here
|
|
488
|
+
* would put one lag in two places.
|
|
489
|
+
*/
|
|
490
|
+
function memberReportLines(result: CompileResult): string[] {
|
|
491
|
+
if (result.trackDerivations.length === 0) return [];
|
|
492
|
+
const out: string[] = [
|
|
493
|
+
'',
|
|
494
|
+
'group members (the per-member values of one track, side by side โ issue #295)',
|
|
495
|
+
' .. a row per member and a block per key, because a wrong sign is visible in a column of six and',
|
|
496
|
+
' .. invisible in six tracks. Values are the EMITTED ones, so this and the artifact cannot disagree',
|
|
497
|
+
' .. a group whose members all share one value is not here: there is one number and the timelines',
|
|
498
|
+
' .. above already carry it. `stagger` is not here either โ the shifted key times are on those timelines',
|
|
499
|
+
];
|
|
500
|
+
for (const entry of result.trackDerivations) {
|
|
501
|
+
const states =
|
|
502
|
+
entry.model === null
|
|
503
|
+
? 'stated per member'
|
|
504
|
+
: `derive ${entry.model.kind} ${entry.model.stated} -> ${entry.model.projection === 'shift' ? 'the displacement' : 'the narrowing'}`;
|
|
505
|
+
out.push(
|
|
506
|
+
` MEMBER ${entry.animation} ${entry.targetKind} "${entry.target}".${entry.property} ` +
|
|
507
|
+
`t=${entry.time.toFixed(6)} ${entry.members.length} member(s) ${states}`,
|
|
508
|
+
);
|
|
509
|
+
if (entry.model !== null) {
|
|
510
|
+
out.push(` ${entry.model.formula}`);
|
|
511
|
+
for (const line of entry.model.derived) out.push(` ${line}`);
|
|
512
|
+
}
|
|
513
|
+
const width = Math.max(6, ...entry.members.map((m) => m.member.length));
|
|
514
|
+
for (let i = 0; i < entry.members.length; i++) {
|
|
515
|
+
const m = entry.members[i];
|
|
516
|
+
const value = Array.isArray(m.value) ? m.value.join(', ') : JSON.stringify(m.value);
|
|
517
|
+
// The model's own row carries the two inputs that produced the value โ the
|
|
518
|
+
// coordinate it read off the rig and the depth the spec stated โ because
|
|
519
|
+
// "5.513" alone is a number a reader can only take on trust, and `โ62` and
|
|
520
|
+
// `150` beside it are a claim they can check.
|
|
521
|
+
const from = entry.model === null ? '' : ` <- ${entry.model.members[i].at >= 0 ? ' ' : ''}${entry.model.members[i].at} at depth ${entry.model.members[i].depth}`;
|
|
522
|
+
out.push(` ${m.member.padEnd(width)} ${value.padStart(12)}${from}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return out;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** `head/head key 1`, which is how A39's own message names a key. */
|
|
529
|
+
function deformKeyName(key: DeformKeyMeasure): string {
|
|
530
|
+
return `${key.slot}/${key.attachment} key ${key.key}`;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Does this compiled `transform` report belong to this loaded key?
|
|
535
|
+
*
|
|
536
|
+
* โ ๏ธ The two times are not the same number and cannot be compared with `===`.
|
|
537
|
+
* The report's is the spec's own `t`; the survey's came back through
|
|
538
|
+
* `Float32Array`, because that is what `spine-core` reads a timeline's frames
|
|
539
|
+
* into โ a key written `0.62` arrives as `0.6200000047683716`. So the tolerance
|
|
540
|
+
* is the compiler's own key-time grid plus one float32 ulp at this magnitude,
|
|
541
|
+
* which is narrower than any key spacing the format can hold and wide enough for
|
|
542
|
+
* both roundings.
|
|
543
|
+
*/
|
|
544
|
+
function sameKeyTime(specTime: number, loaded: number): boolean {
|
|
545
|
+
return Math.abs(specTime - loaded) <= KEY_TIME_EPSILON + Math.abs(loaded) * 2 ** -23;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* The `DEFORM` report block โ what each deform key does to the geometry, per key
|
|
550
|
+
* and then per animation (issue #316).
|
|
551
|
+
*
|
|
552
|
+
* ## Why this is a report and not an assertion
|
|
553
|
+
*
|
|
554
|
+
* Because a 3ร stretch is a real thing to author, for the same reason issue #277
|
|
555
|
+
* settled mesh coverage as a printed figure on authored geometry rather than a
|
|
556
|
+
* bar. The one deformed-geometry fault that has no legitimate counter-example is
|
|
557
|
+
* the fold, and that one already IS an assertion โ
|
|
558
|
+
* `A39_DEFORM_KEEPS_TRIANGLE_WINDING`. What this block adds is **the approach to
|
|
559
|
+
* that wall**: FACE ยง4.2's table of ratios down to the fold at 31.37ยฐ was
|
|
560
|
+
* measured by rendering seven variants of `gallery/portrait` and looking at them,
|
|
561
|
+
* and `0.637` was a number an author derived from the closed form rather than one
|
|
562
|
+
* the tool printed.
|
|
563
|
+
*
|
|
564
|
+
* ## It quotes; it does not re-derive
|
|
565
|
+
*
|
|
566
|
+
* - the reversal and collapse counts are the **survey's**, which is A39's own
|
|
567
|
+
* survey ([`src/deformmeasure.ts`](src/deformmeasure.ts)) โ one measurement,
|
|
568
|
+
* two readers, so the block and the gate cannot disagree about a fold;
|
|
569
|
+
* - a key's model is the **compiler's** `transform` report (ยง4.11.1), so the
|
|
570
|
+
* block names the same `kind` and parameters the spec stated;
|
|
571
|
+
* - the fold ANGLE is nowhere here. It is A39's, derived at run time from the
|
|
572
|
+
* grid, and a second copy of it printed beside a ratio would be a number that
|
|
573
|
+
* goes stale when somebody moves a column.
|
|
574
|
+
*
|
|
575
|
+
* ## And what it deliberately does not print
|
|
576
|
+
*
|
|
577
|
+
* **Deformed coverage**, which #296 asked for. The coverage figure is rasterised
|
|
578
|
+
* from the attachment's **uvs** against the part's alpha, and a deform moves
|
|
579
|
+
* positions and never uvs โ so it is identical at every key by construction, and
|
|
580
|
+
* a `coverage 100.00% (setup 100.00%)` line would be a tautology wearing a
|
|
581
|
+
* measurement's clothes. The header line says so and points at `meshes`, because
|
|
582
|
+
* an author who came here asking whether their deform broke the coverage
|
|
583
|
+
* deserves the answer rather than a silence. What does move is the stretch.
|
|
584
|
+
*/
|
|
585
|
+
function deformReportLines(result: CompileResult, exempt: ReadonlySet<string>): string[] {
|
|
586
|
+
const survey = surveyDeformKeys(skeletonDataFromText(result.skeletonText, result.atlasText));
|
|
587
|
+
if (survey.timelines === 0) return [];
|
|
588
|
+
const out: string[] = ['', 'deform (what each key does to the geometry โ figures with names, never a bar; issue #316)'];
|
|
589
|
+
// The legend costs six lines and is worth them exactly once โ on a report that
|
|
590
|
+
// has figures in it. A bounding box or a clipping polygon deformed and nothing
|
|
591
|
+
// else gets the reason it has no figures and no essay about them.
|
|
592
|
+
if (survey.keys.length) {
|
|
593
|
+
out.push(
|
|
594
|
+
' .. every key measured at its OWN time against the same pose with the deform CLEARED, so the',
|
|
595
|
+
' .. denominator is 1.000 by definition and a NEGATIVE area ratio IS a reversed triangle',
|
|
596
|
+
' .. stretch is the two singular values of the map from the cleared triangle to the deformed one โ',
|
|
597
|
+
' .. the worst stretch and the worst squash the drawing takes there; their product is |area ratio|',
|
|
598
|
+
' .. coverage is NOT here: it is rasterised from the uvs, which no deform moves, so the figure on',
|
|
599
|
+
' .. the `meshes` line below is already the deformed one',
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if (survey.notAMesh.length) {
|
|
603
|
+
out.push(` .. ${survey.notAMesh.join(', ')} deform an attachment with no triangles โ nothing to measure`);
|
|
604
|
+
}
|
|
605
|
+
for (const key of survey.keys) {
|
|
606
|
+
const model = result.deformTransforms.find(
|
|
607
|
+
(g) =>
|
|
608
|
+
g.animation === key.animation &&
|
|
609
|
+
g.skin === key.skin &&
|
|
610
|
+
g.slot === key.slot &&
|
|
611
|
+
g.attachment === key.placeholder &&
|
|
612
|
+
sameKeyTime(g.time, key.time),
|
|
613
|
+
);
|
|
614
|
+
// A stated model is quoted rather than reduced to its results: `yaw
|
|
615
|
+
// radius=170 degrees=12` is what a reviewer checks the ratios against, and an
|
|
616
|
+
// authored table says so instead of saying nothing, because "no model here"
|
|
617
|
+
// is itself the thing a reader of a wrong ratio needs to know.
|
|
618
|
+
const states = model === undefined ? 'authored table' : `transform ${model.kind} ${model.stated}`;
|
|
619
|
+
out.push(
|
|
620
|
+
` DEFORM ${key.animation} ${key.skin}/${key.slot}/${key.placeholder} key ${key.key} ` +
|
|
621
|
+
`t=${key.time.toFixed(6)} ${states}`,
|
|
622
|
+
);
|
|
623
|
+
// A key that moves nothing gets one line and no figures. `{ "t": 2.2 }` with
|
|
624
|
+
// no run is the format's own way of writing "back to the setup pose" (ยง4.11),
|
|
625
|
+
// and its geometry is bit-identical to the cleared pose it would be measured
|
|
626
|
+
// against โ so `x1.000000` there is the definition and not a measurement, and
|
|
627
|
+
// four lines of it on every loop's opening and closing key is the noise that
|
|
628
|
+
// stops the block being read. It is still counted in the rollup below,
|
|
629
|
+
// because A39 measures it too.
|
|
630
|
+
if (key.moved === 0) {
|
|
631
|
+
out.push(
|
|
632
|
+
` moved 0 of ${key.vertices} vertices โ this key IS the setup pose, so every ` +
|
|
633
|
+
`figure is the identity (${key.triangles} triangles, all kept)`,
|
|
634
|
+
);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
out.push(
|
|
638
|
+
` moved ${key.moved} of ${key.vertices} vertices, ` +
|
|
639
|
+
`worst ${key.maxDisplacement.toFixed(4)}px at v${key.maxDisplacementVertex}`,
|
|
640
|
+
);
|
|
641
|
+
out.push(
|
|
642
|
+
` area min ${deformExtreme(key.areaRatioMin)} max ${deformExtreme(key.areaRatioMax)} ` +
|
|
643
|
+
`(${key.triangles} triangles, ${key.degenerate} with no area at the cleared pose, band ${key.band.toFixed(6)}pxยฒ)`,
|
|
644
|
+
);
|
|
645
|
+
out.push(
|
|
646
|
+
` stretch max ${deformExtreme(key.stretchMax)} min ${deformExtreme(key.stretchMin)}`,
|
|
647
|
+
);
|
|
648
|
+
// The marker has to know about the exemption, or it says the false half of
|
|
649
|
+
// the truth on the one build where it matters: a declared fold IS a fold and
|
|
650
|
+
// A39 does not refuse it โ it SKIPs the slot entirely.
|
|
651
|
+
const exempted = exempt.has(key.slot);
|
|
652
|
+
const fold = key.reversed.length
|
|
653
|
+
? exempted
|
|
654
|
+
? ' <- a fold, and A39 does not gate it โ see below'
|
|
655
|
+
: ' <- a fold: A39 refuses this key by name'
|
|
656
|
+
: '';
|
|
657
|
+
out.push(
|
|
658
|
+
` winding ${key.triangles - key.reversed.length} of ${key.triangles} kept, ` +
|
|
659
|
+
`${key.collapsed} collapsed${fold}`,
|
|
660
|
+
);
|
|
661
|
+
if (exempted) {
|
|
662
|
+
out.push(
|
|
663
|
+
` .. A39 is exempt on "${key.slot}" (invariants.deformMayFold), so nothing here is gated`,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
// The rollup, per animation: the worst key by each quantity. A timeline's own
|
|
668
|
+
// eight keys are eight blocks above, and "which of them is the one to look at"
|
|
669
|
+
// is the question the sweep in issue #313's landing comment answered by hand.
|
|
670
|
+
for (const animation of [...new Set(survey.keys.map((k) => k.animation))]) {
|
|
671
|
+
const keys = survey.keys.filter((k) => k.animation === animation);
|
|
672
|
+
const worst = (
|
|
673
|
+
pick: (key: DeformKeyMeasure) => DeformExtreme | null,
|
|
674
|
+
better: (a: number, b: number) => boolean,
|
|
675
|
+
): string => {
|
|
676
|
+
let best: { key: DeformKeyMeasure; extreme: DeformExtreme } | null = null;
|
|
677
|
+
for (const key of keys) {
|
|
678
|
+
const extreme = pick(key);
|
|
679
|
+
if (extreme === null) continue;
|
|
680
|
+
if (best === null || better(extreme.value, best.extreme.value)) best = { key, extreme };
|
|
681
|
+
}
|
|
682
|
+
return best === null ? 'โ' : `x${best.extreme.value.toFixed(6)} (${deformKeyName(best.key)} tri ${best.extreme.triangle})`;
|
|
683
|
+
};
|
|
684
|
+
const reversed = keys.reduce((n, k) => n + k.reversed.length, 0);
|
|
685
|
+
const collapsed = keys.reduce((n, k) => n + k.collapsed, 0);
|
|
686
|
+
const samples = keys.reduce((n, k) => n + k.triangles, 0);
|
|
687
|
+
out.push(
|
|
688
|
+
` WORST ${animation} area ${worst((k) => k.areaRatioMin, (a, b) => a < b)} ` +
|
|
689
|
+
`stretch ${worst((k) => k.stretchMax, (a, b) => a > b)} ` +
|
|
690
|
+
`squash ${worst((k) => k.stretchMin, (a, b) => a < b)}`,
|
|
691
|
+
);
|
|
692
|
+
out.push(
|
|
693
|
+
` .. ${''.padEnd(animation.length)} reversed ${reversed}, collapsed ${collapsed}, over ` +
|
|
694
|
+
`${keys.length} key(s) and ${samples} triangle sample(s) <- A39 reads the same two counts`,
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
return out;
|
|
698
|
+
}
|
|
699
|
+
|
|
434
700
|
/**
|
|
435
701
|
* Read one non-negative integer flag, or its default.
|
|
436
702
|
*
|
|
@@ -1693,7 +1959,10 @@ function cmdExplain(flags: Record<string, string>): void {
|
|
|
1693
1959
|
console.log(` .. rig ${opts.rigPath}`);
|
|
1694
1960
|
console.log(` .. motion ${opts.motionPath}`);
|
|
1695
1961
|
const result = compile(opts);
|
|
1696
|
-
|
|
1962
|
+
// `compile` has already parsed this file, so the read below cannot fail โ but
|
|
1963
|
+
// it goes through the same parser rather than a cast, because the cast was the
|
|
1964
|
+
// last one in the repository and issue #307 was about exactly that.
|
|
1965
|
+
const motion = parseMotionSpec(readJsonFile(opts.motionPath), opts.motionPath);
|
|
1697
1966
|
|
|
1698
1967
|
console.log(`\nstage ${result.skeleton.skeleton.width} x ${result.skeleton.skeleton.height} (spine ${result.skeleton.skeleton.spine})`);
|
|
1699
1968
|
|
|
@@ -1813,6 +2082,30 @@ function cmdExplain(flags: Record<string, string>): void {
|
|
|
1813
2082
|
: 'back to the setup pose';
|
|
1814
2083
|
const curve = Array.isArray(key.curve) ? `bezier[${key.curve.length}]` : key.curve === 'stepped' ? 'stepped' : 'linear';
|
|
1815
2084
|
console.log(` t=${String(key.time).padEnd(7)} ${span.padEnd(46)} ${curve}`);
|
|
2085
|
+
// A generated key prints its MODEL and then every offset the model
|
|
2086
|
+
// produced (issue #294). Both halves are the point: the model is
|
|
2087
|
+
// what a reviewer checks a claim against, and the offsets are what
|
|
2088
|
+
// reaches the file โ printing only the first would ask a reader to
|
|
2089
|
+
// trust an evaluation they cannot see, which is the gap FACE ยง9.3
|
|
2090
|
+
// records. The numbers are the emitted ones, not a second
|
|
2091
|
+
// evaluation, so this block and the artifact cannot disagree.
|
|
2092
|
+
const gen = result.deformTransforms.find(
|
|
2093
|
+
(g) => g.animation === animName && g.skin === skinName && g.slot === slotName && g.attachment === attName && g.time === key.time,
|
|
2094
|
+
);
|
|
2095
|
+
if (gen === undefined) continue;
|
|
2096
|
+
console.log(` transform ${gen.kind} ${gen.stated}`);
|
|
2097
|
+
console.log(` ${gen.formula}`);
|
|
2098
|
+
for (const line of gen.derived) console.log(` ${line}`);
|
|
2099
|
+
console.log(
|
|
2100
|
+
` ${gen.vertexCount} vertices, largest offset ${gen.maxOffset}px at vertex ${gen.maxOffsetVertex}`,
|
|
2101
|
+
);
|
|
2102
|
+
for (let v = 0; v < gen.vertexCount; v += 4) {
|
|
2103
|
+
const pairs: string[] = [];
|
|
2104
|
+
for (let k = v; k < Math.min(v + 4, gen.vertexCount); k++) {
|
|
2105
|
+
pairs.push(`v${String(k).padStart(3)} (${gen.offsets[2 * k]}, ${gen.offsets[2 * k + 1]})`);
|
|
2106
|
+
}
|
|
2107
|
+
console.log(` ${pairs.join(' ')}`);
|
|
2108
|
+
}
|
|
1816
2109
|
}
|
|
1817
2110
|
}
|
|
1818
2111
|
}
|
|
@@ -1834,6 +2127,16 @@ function cmdExplain(flags: Record<string, string>): void {
|
|
|
1834
2127
|
}
|
|
1835
2128
|
}
|
|
1836
2129
|
|
|
2130
|
+
// The `MEMBER` block sits beside the `DEFORM` one and for the same reason:
|
|
2131
|
+
// both re-print timelines the reader has just read, in the arrangement the
|
|
2132
|
+
// question needs rather than the one the format has.
|
|
2133
|
+
for (const line of memberReportLines(result)) console.log(line);
|
|
2134
|
+
|
|
2135
|
+
// The `DEFORM` block goes after the timelines and before the constraints,
|
|
2136
|
+
// because it is a measurement OF the deform timelines printed above โ the keys
|
|
2137
|
+
// it names are the keys the reader has just read, by the same index.
|
|
2138
|
+
for (const line of deformReportLines(result, new Set(result.rig.deformMayFold))) console.log(line);
|
|
2139
|
+
|
|
1837
2140
|
if (result.physics.length) {
|
|
1838
2141
|
console.log('\nphysics constraints (4.3 top-level `constraints` array, type per entry)');
|
|
1839
2142
|
for (const ph of result.physics) {
|