spine-rigc 2.55.0 → 2.57.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 +4 -2
- package/docs/AUTHORING.md +549 -8
- package/package.json +1 -1
- package/skills/rigc/SKILL.md +4 -1
- package/src/anglerange.ts +190 -0
- package/src/cli/core_commands.ts +218 -13
- package/src/cli/shared.ts +160 -9
- package/src/compile.ts +69 -8
- package/src/core/entry.ts +6 -1
- package/src/deformgen.ts +333 -2
- package/src/meshsplit.ts +683 -0
- package/src/motion.ts +20 -8
- package/src/render_shared.ts +30 -0
- package/src/rig.ts +95 -3
- package/src/stop.ts +472 -0
- package/src/transform.ts +20 -0
- package/src/viewportpin.ts +168 -0
- package/src/whirl.ts +580 -0
package/src/cli/shared.ts
CHANGED
|
@@ -39,10 +39,11 @@ import { compile, CompileError, droppedStateReason, headerBoundsOf, type Compile
|
|
|
39
39
|
import { depthStepLevels, type FoldLimit, type TurnCeiling } from '../depth.ts';
|
|
40
40
|
import { FOLDS_FILE } from '../folds.ts';
|
|
41
41
|
import { PASS_THROUGH_FILE, PASS_THROUGH_RUN } from '../passthrough.ts';
|
|
42
|
+
import { readViewportBox, ViewportError } from '../viewportpin.ts';
|
|
42
43
|
import { parseJsonWithPosition } from '../json-position.ts';
|
|
43
44
|
import { RUNG_IDS } from '../ladder.ts';
|
|
44
45
|
import { MODEL_DOCUMENT_FILE, MODEL_DOCUMENT_SPEC, modelDocument, spineFileSha256 } from '../model.ts';
|
|
45
|
-
import { compileErrorLine } from '../guide.ts';
|
|
46
|
+
import { compileErrorLine, nearestNames } from '../guide.ts';
|
|
46
47
|
import { PACKAGE_ROOT, readPackageMeta, readVersion } from '../package_meta.ts';
|
|
47
48
|
import { DEFAULT_MAX_RESIDUAL, DEFAULT_SCALE_MAX, DEFAULT_SCALE_MIN } from '../pose.ts';
|
|
48
49
|
import {
|
|
@@ -64,6 +65,7 @@ import { InspectError, DEFAULT_SHOW, DIM_KEEP, INSPECT_DEFAULT_FPS, ON_GRID_FRAM
|
|
|
64
65
|
import { NotAPngError } from '../png.ts';
|
|
65
66
|
import { PoseError } from '../pose.ts';
|
|
66
67
|
import { RepackError } from '../repack.ts';
|
|
68
|
+
import { SplitError } from '../meshsplit.ts';
|
|
67
69
|
import { SpineRuntimeError, SPINE_SIDE_ABSENT } from '../spine_side.ts';
|
|
68
70
|
import type { CompileResult, DroppedState } from '../types.ts';
|
|
69
71
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
@@ -675,12 +677,12 @@ function readCheckFlags(
|
|
|
675
677
|
out.fps = fps;
|
|
676
678
|
}
|
|
677
679
|
if (flags.viewport !== undefined) {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
680
|
+
try {
|
|
681
|
+
out.viewport = readViewportBox(flags.viewport);
|
|
682
|
+
} catch (err) {
|
|
683
|
+
if (err instanceof ViewportError) throw new UsageError(err.message);
|
|
684
|
+
throw err;
|
|
681
685
|
}
|
|
682
|
-
if (parts[2] <= 0 || parts[3] <= 0) throw new UsageError('--viewport width and height must be positive');
|
|
683
|
-
out.viewport = { x: parts[0], y: parts[1], width: parts[2], height: parts[3] };
|
|
684
686
|
}
|
|
685
687
|
if (flags.as !== undefined) out.as = flags.as;
|
|
686
688
|
// The name is not checked against the candidate here: `check` owns that
|
|
@@ -1021,6 +1023,17 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
1021
1023
|
'world units, one texel of the coarser drawing to a step — each with the depth and the world box. One line per pair, and one per frame that passes through. ' +
|
|
1022
1024
|
'The order is the declaration: the first name must draw over the second for a frame to be read. A reading, not a gate. ' +
|
|
1023
1025
|
'An unknown slot, a slot that draws on no frame, a malformed list and a pair named twice are refused by name. Not with --slot/--hide',
|
|
1026
|
+
'angle-range':
|
|
1027
|
+
'also read, for each <bone>:<relative-to> pair, the range of the first bone\'s world direction against the second\'s over each set\'s ' +
|
|
1028
|
+
'frames — its rotationX less the other\'s, followed from frame to frame so a sweep past ±180° reads as itself — and print one line per ' +
|
|
1029
|
+
'pair per set: the lowest and the highest angle in degrees, the span, and the frame each was first reached on. Read on the frames\' own ' +
|
|
1030
|
+
'grid, so a peak between two frames is not read. A reading, not a gate: nothing is written. An unknown bone, a bone against itself, ' +
|
|
1031
|
+
'a malformed list and a pair named twice are refused by name, and so is a bone with no direction on a frame (a world matrix of zero)',
|
|
1032
|
+
'viewport-from':
|
|
1033
|
+
"draw on an earlier render's grid exactly — the box, the scale and the pixel size its frames.json records (the file, or the " +
|
|
1034
|
+
'directory `render --out` wrote it into) — so a world point lands on the same pixel in both renders. A slot that reaches past ' +
|
|
1035
|
+
'the box is named as under --viewport, and frames.json records the same under `pinned`. Not with --viewport, and not with --max: ' +
|
|
1036
|
+
'the file fixes the pixel size',
|
|
1024
1037
|
environment:
|
|
1025
1038
|
'the world the rig\'s physics answers, in time: a file { "spec": "rigc-environment/1", "steps": [{ "t": <seconds>, ' +
|
|
1026
1039
|
'"wind"?: [x, y], "gravity"?: [x, y] }, …] }, x right and y down, each step holding from its t until the next and set ' +
|
|
@@ -1035,6 +1048,12 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
1035
1048
|
'line of the render or the check report says which and why. `--poser core` on an input that cannot carry it is refused by name. ' +
|
|
1036
1049
|
"`explain`: which reads and poses the DEFORM block's survey — `core` (the model document the compile writes, spine-core untouched) or " +
|
|
1037
1050
|
'`spine` (the Spine skeleton parsed and posed by spine-core); default `core`, and `spine` where the core refuses the document, which the block names',
|
|
1051
|
+
'lever-fold':
|
|
1052
|
+
'`explain` only: also read, on the turn lines\' walk, the steps where a physics constraint that drives scaleX has its inertia lever ' +
|
|
1053
|
+
'divide by less than this fraction (above 0, at most 1) of the divisor it takes at the setup pose — the bone\'s length times its world x-axis ' +
|
|
1054
|
+
'length, less scaleLag × shown on a drive that does not rotate — and the steps where the scale factor it applies moves by more than the ' +
|
|
1055
|
+
'bone\'s setup scale in one step: each line the constraint, the steps, the extreme value and its step. No default: without it the reading ' +
|
|
1056
|
+
'is not taken, and says so; a value that is not such a fraction is refused by name. A reading, not a gate',
|
|
1038
1057
|
'texture-from':
|
|
1039
1058
|
"also measure this run through this atlas's texels, keeping the candidate's own geometry, and report how much " +
|
|
1040
1059
|
'of the MAE is texture resampling rather than the rig — pass the atlas the reference frames were rendered ' +
|
|
@@ -1080,6 +1099,9 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
1080
1099
|
`printed (default ${DEFAULT_MIN_LEVER_PX}); below it the bone is refused \`no-bracket\` naming the measured ` +
|
|
1081
1100
|
'lever, because an angle read across a short lever turns a half-pixel anchor error into several degrees',
|
|
1082
1101
|
animation: 'which animation to show; the default is every one for `render` and the first for `preview`',
|
|
1102
|
+
attachment:
|
|
1103
|
+
"the placeholder the mesh is filed under in its slot, as the rig spec's skin names it. Absent, the slot's one " +
|
|
1104
|
+
'authored mesh in that skin; a slot holding none or several is refused naming them',
|
|
1083
1105
|
skin:
|
|
1084
1106
|
'pose under this skin, by the name the skeleton declares. Without it NO skin is set — every slot resolves ' +
|
|
1085
1107
|
'through the default skin alone, so a slot whose art lives only in a named skin draws nothing. A name the ' +
|
|
@@ -1211,6 +1233,8 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
1211
1233
|
poser: 'core|spine',
|
|
1212
1234
|
environment: '<schedule.json>',
|
|
1213
1235
|
'pass-through': '<cloth>:<leg>[,<cloth>:<leg>…]',
|
|
1236
|
+
'angle-range': '<bone>:<relative-to>[,…]',
|
|
1237
|
+
'viewport-from': '<frames.json|dir>',
|
|
1214
1238
|
reference: '<dir|skeleton.json>',
|
|
1215
1239
|
'reference-atlas': '<path>',
|
|
1216
1240
|
bones: `<correspondence.json|${IDENTITY_CORRESPONDENCE}>`,
|
|
@@ -1225,6 +1249,7 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
1225
1249
|
scale: '<min,max>',
|
|
1226
1250
|
rotation: '<min,max>',
|
|
1227
1251
|
'max-residual': '<0..1>',
|
|
1252
|
+
'lever-fold': '<fraction>',
|
|
1228
1253
|
anchor: '<pose.json>',
|
|
1229
1254
|
hinge: '<min,max>',
|
|
1230
1255
|
stretch: '<ratio>',
|
|
@@ -1233,6 +1258,7 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
1233
1258
|
'anchor-residual': '<0..1>',
|
|
1234
1259
|
'inward-lever': '<px>',
|
|
1235
1260
|
animation: '<name>',
|
|
1261
|
+
attachment: '<name>',
|
|
1236
1262
|
skin: '<name>',
|
|
1237
1263
|
slot: '<name[,name…]>',
|
|
1238
1264
|
hide: '<name[,name…]>',
|
|
@@ -1476,8 +1502,9 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1476
1502
|
'rigc explain --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] [--poser core|spine] (it never gates, and writes nothing)',
|
|
1477
1503
|
'rigc explain … --atlas-in <skeleton.atlas> (resolve the parts against a pack somebody already made, as build does)',
|
|
1478
1504
|
'rigc explain --cut <name> --cuts <cuts.json>',
|
|
1505
|
+
'rigc explain … --lever-fold <fraction> (also name the steps a scaleX lever divides by a collapsed length)',
|
|
1479
1506
|
],
|
|
1480
|
-
flags: ['rig', 'motion', 'out', 'manifest', 'images', 'atlas-in', 'cut', 'cuts', 'poser'],
|
|
1507
|
+
flags: ['rig', 'motion', 'out', 'manifest', 'images', 'atlas-in', 'cut', 'cuts', 'poser', 'lever-fold'],
|
|
1481
1508
|
notes: [
|
|
1482
1509
|
'this line said "the same arguments as build, minus --profile" and was false in both',
|
|
1483
1510
|
'directions (issue #697): --atlas-in was not listed here, so the one flag that lets this',
|
|
@@ -1518,6 +1545,46 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1518
1545
|
},
|
|
1519
1546
|
},
|
|
1520
1547
|
},
|
|
1548
|
+
{
|
|
1549
|
+
name: 'split',
|
|
1550
|
+
runtime: false,
|
|
1551
|
+
spineFormat: false,
|
|
1552
|
+
usage: ['rigc split --rig <path> --motion <path> --slot <name> [--attachment <name>] [--skin <name>] --out <dir>'],
|
|
1553
|
+
flags: ['rig', 'motion', 'slot', 'attachment', 'skin', 'out'],
|
|
1554
|
+
overrides: {
|
|
1555
|
+
out: {
|
|
1556
|
+
value: '<dir>',
|
|
1557
|
+
meaning:
|
|
1558
|
+
'directory to write the proposal into — rig.json and motion.json, the two specs with the mesh split, and ' +
|
|
1559
|
+
'split.json, where every original vertex and triangle went. Absent or empty, and refused otherwise: the ' +
|
|
1560
|
+
"proposal never lands on top of a file, the author's own specs included",
|
|
1561
|
+
},
|
|
1562
|
+
slot: {
|
|
1563
|
+
value: '<name>',
|
|
1564
|
+
meaning: 'the slot whose mesh to split, as the rig spec declares it. A name it does not declare is refused with every one it does',
|
|
1565
|
+
},
|
|
1566
|
+
skin: {
|
|
1567
|
+
meaning:
|
|
1568
|
+
'the skin the mesh is filed under in the rig spec (default: default). A name the rig spec does not declare ' +
|
|
1569
|
+
'is refused with the ones it does',
|
|
1570
|
+
},
|
|
1571
|
+
},
|
|
1572
|
+
notes: [
|
|
1573
|
+
'split reads the two specs — not a build: a mesh of several pieces does not build —',
|
|
1574
|
+
'and finds the pieces of one authored mesh: triangles that share a vertex are one piece.',
|
|
1575
|
+
'A mesh of one piece is said so on one line, exit 0, and nothing is written. Otherwise it',
|
|
1576
|
+
'writes one mesh per piece: piece 1 (the piece whose first triangle is listed first) stays',
|
|
1577
|
+
'in the slot, piece k gets a new slot <slot>_<k> on the same bone, after it in the draw',
|
|
1578
|
+
"order, under the same placeholder. Each piece carries the original's uvs and weights (or",
|
|
1579
|
+
'vertices) unchanged and its triangles renumbered — outline first where the hull rule',
|
|
1580
|
+
'needs it — and every deform key is re-sliced to the new indices; colour and attachment',
|
|
1581
|
+
'tracks on the slot are copied to the new slots. Nothing is invented: a deform index the',
|
|
1582
|
+
'original run did not cover is written 0, which is what the parser held there. What the',
|
|
1583
|
+
'split cannot carry is refused by name, exit 1 (docs/AUTHORING.md §3.4.11 lists it); a',
|
|
1584
|
+
'name the specs do not have is refused with the names they do, exit 2. Nothing is ever',
|
|
1585
|
+
'written over the specs it read: build the proposal, look at it, then adopt it.',
|
|
1586
|
+
],
|
|
1587
|
+
},
|
|
1521
1588
|
{
|
|
1522
1589
|
name: 'diff',
|
|
1523
1590
|
runtime: false,
|
|
@@ -1618,11 +1685,24 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1618
1685
|
'rigc render … --slot <name[,name…]> | --hide <name[,name…]> (a subset of the slots, on the whole rig\'s grid)',
|
|
1619
1686
|
'rigc render … --environment <schedule.json> (the wind and gravity the physics answers, in time)',
|
|
1620
1687
|
'rigc render … --pass-through <cloth>:<leg>[,…] (the frames where a cloth drawn over a leg passes through it)',
|
|
1688
|
+
'rigc render … --angle-range <bone>:<relative-to>[,…] (how far a bone turns against another, per animation)',
|
|
1689
|
+
'rigc render … --viewport <x,y,w,h> | --viewport-from <frames.json> (a box given, not fitted: two renders on one pixel grid)',
|
|
1621
1690
|
],
|
|
1622
|
-
flags: ['candidate', 'atlas', 'animation', 'skin', 'slot', 'hide', 'fps', 'max', 'geometry', 'folds', 'pass-through', 'environment', 'poser', 'out'],
|
|
1691
|
+
flags: ['candidate', 'atlas', 'animation', 'skin', 'slot', 'hide', 'fps', 'max', 'viewport', 'viewport-from', 'geometry', 'folds', 'pass-through', 'angle-range', 'environment', 'poser', 'out'],
|
|
1623
1692
|
overrides: {
|
|
1624
1693
|
out: { value: '<dir>', meaning: 'directory to write the frame series into (default `render/`)' },
|
|
1625
|
-
fps: {
|
|
1694
|
+
fps: {
|
|
1695
|
+
meaning:
|
|
1696
|
+
`frames per second to sample the animation at (default ${PROTOCOL_FPS}). A set drawn at any rate but the default is written to ` +
|
|
1697
|
+
'`<anim>@<fps>fps/` rather than `<anim>/`, so two rates sit side by side: `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim>/`',
|
|
1698
|
+
},
|
|
1699
|
+
viewport: {
|
|
1700
|
+
meaning:
|
|
1701
|
+
'draw every frame over this world box, y up — x,y its bottom-left corner, the numbers frames.json\'s `viewport` records — ' +
|
|
1702
|
+
'instead of the box fitted to the rig; its pixel size follows from --max. A slot that reaches past the box is named by set and ' +
|
|
1703
|
+
'frame, with how far and on which side: what lies past it is not drawn, since each frame is the box. frames.json records that ' +
|
|
1704
|
+
'the box was given, and those slots, under `pinned`. Not with --viewport-from',
|
|
1705
|
+
},
|
|
1626
1706
|
},
|
|
1627
1707
|
},
|
|
1628
1708
|
{
|
|
@@ -1910,6 +1990,16 @@ const USAGE_PARAGRAPHS: ReadonlyArray<{ about: readonly string[] | { flag: strin
|
|
|
1910
1990
|
'is missing from it beats no spec at all.',
|
|
1911
1991
|
],
|
|
1912
1992
|
},
|
|
1993
|
+
{
|
|
1994
|
+
about: ['split'],
|
|
1995
|
+
lines: () => [
|
|
1996
|
+
"split answers the refusal a mesh of several pieces meets at compile — the triangles'",
|
|
1997
|
+
'outline is not one closed loop — with one mesh per piece, its uvs, weights, triangles',
|
|
1998
|
+
'and deform keys renumbered, written beside the specs and never over them:',
|
|
1999
|
+
' rigc split --rig rig.json --motion motion.json --slot legs --out split/',
|
|
2000
|
+
'Build what it wrote. See `rigc split --help`.',
|
|
2001
|
+
],
|
|
2002
|
+
},
|
|
1913
2003
|
{
|
|
1914
2004
|
about: ['pose'],
|
|
1915
2005
|
lines: () => [
|
|
@@ -1995,6 +2085,7 @@ export function usageText(docs: readonly CommandDoc[], checkout: string, rules:
|
|
|
1995
2085
|
...docs.flatMap((c) => c.usage.map((u) => ` ${u}`)),
|
|
1996
2086
|
'',
|
|
1997
2087
|
' rigc <command> --help that command\'s own flag table',
|
|
2088
|
+
...HELP_DOC.usage.map((u) => ` ${u}`),
|
|
1998
2089
|
' rigc --version print the installed version (-v works too)',
|
|
1999
2090
|
];
|
|
2000
2091
|
// Read only by the --profile paragraph, which only an entry running a command that takes --profile prints.
|
|
@@ -2748,6 +2839,46 @@ function commandNeedsRuntime(doc: CommandDoc, runs: readonly string[], args: rea
|
|
|
2748
2839
|
);
|
|
2749
2840
|
}
|
|
2750
2841
|
|
|
2842
|
+
/**
|
|
2843
|
+
* `rigc help` documented as a command is (issue #1440): on the usage of both
|
|
2844
|
+
* entries, and its own page for `rigc help --help` and `rigc help help`, as
|
|
2845
|
+
* every listed command answers `--help`. Not a row of `COMMANDS`, because it
|
|
2846
|
+
* has no body of its own: it is the dispatch reading another spelling of
|
|
2847
|
+
* `--help` (`helpArgv`), on every entry alike.
|
|
2848
|
+
*/
|
|
2849
|
+
const HELP_DOC: CommandDoc = {
|
|
2850
|
+
name: 'help',
|
|
2851
|
+
usage: ['rigc help [<command>] the same page as rigc [<command>] --help, and the same exit'],
|
|
2852
|
+
flags: [],
|
|
2853
|
+
runtime: false,
|
|
2854
|
+
spineFormat: false,
|
|
2855
|
+
};
|
|
2856
|
+
|
|
2857
|
+
/**
|
|
2858
|
+
* `rigc help [<command>]` (issue #1440): the command line it stands for, which
|
|
2859
|
+
* the dispatch then runs in its place — `rigc --help` for `help` alone,
|
|
2860
|
+
* `rigc <command> --help` for a command the table documents — or `null` for
|
|
2861
|
+
* `help`'s own page (`help --help`, `help help`). Rewritten rather than
|
|
2862
|
+
* printed here, so the page and the exit code are the ones that spelling gets
|
|
2863
|
+
* on this entry and cannot come apart from them: a command this entry does not
|
|
2864
|
+
* run is refused as `<command> --help` is.
|
|
2865
|
+
*
|
|
2866
|
+
* ⚠️ A name the table does not document is refused by name with the nearest
|
|
2867
|
+
* one, as a `UsageError` — printed and exited as an unknown command is, exit 2
|
|
2868
|
+
* — and so is more than one name, rather than a guess at which was meant.
|
|
2869
|
+
*/
|
|
2870
|
+
export function helpArgv(rest: readonly string[]): string[] | null {
|
|
2871
|
+
if (rest.length === 0) return ['--help'];
|
|
2872
|
+
if (rest.length > 1) {
|
|
2873
|
+
throw new UsageError(`help takes one command name and was given ${rest.length}: ${rest.join(' ')} — \`rigc help <command>\` is \`rigc <command> --help\``);
|
|
2874
|
+
}
|
|
2875
|
+
const [name] = rest;
|
|
2876
|
+
if (name === HELP_DOC.name || name === '--help' || name === '-h') return null;
|
|
2877
|
+
if (KNOWN_COMMANDS.includes(name)) return [name, '--help'];
|
|
2878
|
+
const [nearest] = nearestNames(name, [...KNOWN_COMMANDS, HELP_DOC.name], 1);
|
|
2879
|
+
throw new UsageError(`unknown command: ${name} — the nearest command is ${nearest} (\`rigc help ${nearest}\`)`);
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2751
2882
|
/**
|
|
2752
2883
|
* Run `argv` (`process.argv` without the runtime and the script) through
|
|
2753
2884
|
* `entry`: the usage, `--version`, a command's `--help`, the command, and
|
|
@@ -2762,6 +2893,20 @@ export function runCli(entry: CliEntry, argv: readonly string[]): void {
|
|
|
2762
2893
|
throw new Error(`internal: this entry runs [${known.join(', ')}] by the command table and registers [${registered.join(', ')}]`);
|
|
2763
2894
|
}
|
|
2764
2895
|
const USAGE = usageText(docs, entry.checkout, entry.profileRules);
|
|
2896
|
+
if (argv[0] === 'help') {
|
|
2897
|
+
let target: string[] | null;
|
|
2898
|
+
try {
|
|
2899
|
+
target = helpArgv(argv.slice(1));
|
|
2900
|
+
} catch (err) {
|
|
2901
|
+
refuse(err, 'help', USAGE, entry);
|
|
2902
|
+
}
|
|
2903
|
+
if (target === null) {
|
|
2904
|
+
console.log(commandHelp(HELP_DOC.name, [HELP_DOC]));
|
|
2905
|
+
process.exit(0);
|
|
2906
|
+
}
|
|
2907
|
+
runCli(entry, target);
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2765
2910
|
const [command, ...rest] = argv;
|
|
2766
2911
|
try {
|
|
2767
2912
|
if (command === undefined) {
|
|
@@ -2878,6 +3023,12 @@ function refuse(err: unknown, command: string | undefined, USAGE: string, entry:
|
|
|
2878
3023
|
console.error(`rigc repack: ${err.message}`);
|
|
2879
3024
|
process.exit(err.status);
|
|
2880
3025
|
}
|
|
3026
|
+
// A split refused (issue #1414): a name the specs do not have (2 — the invocation has to change), or something
|
|
3027
|
+
// the specs hold that the split cannot carry (1). The message names it, and the usage under it would bury that.
|
|
3028
|
+
if (err instanceof SplitError) {
|
|
3029
|
+
console.error(`rigc split: ${err.message}`);
|
|
3030
|
+
process.exit(err.status);
|
|
3031
|
+
}
|
|
2881
3032
|
// A page or frame that is not a PNG rigc can read, from any command that
|
|
2882
3033
|
// opens one (`render`, `check`, `preview`, …) — issue #732. The sentence is
|
|
2883
3034
|
// the one reader's and already names the file, what it is and what rigc
|
package/src/compile.ts
CHANGED
|
@@ -99,6 +99,7 @@ import {
|
|
|
99
99
|
type MeshGeometry,
|
|
100
100
|
type MeshVertexWeight,
|
|
101
101
|
} from './mesh.ts';
|
|
102
|
+
import { meshPieces, shellWord } from './meshsplit.ts';
|
|
102
103
|
import { Plate, readPlate } from '../tools/plate.ts';
|
|
103
104
|
import {
|
|
104
105
|
extractRegion,
|
|
@@ -120,7 +121,7 @@ import {
|
|
|
120
121
|
type PhysicsPoseRule,
|
|
121
122
|
type SlotColorChannel,
|
|
122
123
|
} from './timelines.ts';
|
|
123
|
-
import { evaluateDeformTransform } from './deformgen.ts';
|
|
124
|
+
import { evaluateDeformTransform, type DeformSoftMask } from './deformgen.ts';
|
|
124
125
|
import { evaluateTrackDerive, TRACK_DERIVE_PROJECTIONS, type TrackDeriveMember } from './trackgen.ts';
|
|
125
126
|
import {
|
|
126
127
|
computeExactFrameTransforms,
|
|
@@ -2875,6 +2876,9 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2875
2876
|
// `skin/slot/attachment` -> per-vertex z, for the attachments that named a
|
|
2876
2877
|
// depth map. Read at deform-key time; never emitted. See `AttachmentContext`.
|
|
2877
2878
|
const attachmentDepths = new Map<string, number[]>();
|
|
2879
|
+
// The same for a `soft` mask's per-vertex level, which a deform envelope's
|
|
2880
|
+
// `mask` reads (issue #1413). Never emitted either.
|
|
2881
|
+
const attachmentSoftMasks = new Map<string, DeformSoftMask>();
|
|
2878
2882
|
|
|
2879
2883
|
for (const rigSlot of rig.slots) {
|
|
2880
2884
|
const part = partBySlot.get(rigSlot.name);
|
|
@@ -3042,6 +3046,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3042
3046
|
skinName,
|
|
3043
3047
|
imagesDir,
|
|
3044
3048
|
depths: attachmentDepths,
|
|
3049
|
+
softMasks: attachmentSoftMasks,
|
|
3045
3050
|
slotNames: new Set(rig.slots.map((s) => s.name)),
|
|
3046
3051
|
atlasRegions: atlasIn === null ? null : atlasIn.byName,
|
|
3047
3052
|
links: pendingLinks,
|
|
@@ -3201,6 +3206,17 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3201
3206
|
if (!boneNames.has(spec.bone)) {
|
|
3202
3207
|
throw new CompileError(`physics constraint "${name}" targets unknown bone "${spec.bone}"`);
|
|
3203
3208
|
}
|
|
3209
|
+
// A stop's copy and clamps sit right after the rig constraint that swings
|
|
3210
|
+
// each link (`src/stop.ts`); this table is emitted after every rig
|
|
3211
|
+
// constraint, so a swing here would run after the clamp meant to hold it.
|
|
3212
|
+
const stop = (rig.stops ?? []).find((s) => s.links.includes(spec.bone));
|
|
3213
|
+
if (stop !== undefined) {
|
|
3214
|
+
throw new CompileError(
|
|
3215
|
+
`physics constraint "${name}" drives "${spec.bone}", a link stop "${stop.chain}" holds, from the physics table — ` +
|
|
3216
|
+
"the table is emitted after every rig constraint, past the stop's copy and clamp, which would read the link " +
|
|
3217
|
+
'before this swings it; declare the constraint in the rig spec\'s "constraints"',
|
|
3218
|
+
);
|
|
3219
|
+
}
|
|
3204
3220
|
// Every component and parameter the table states is held, 0 and the
|
|
3205
3221
|
// parser's default included; which of them Spine leaves out is the
|
|
3206
3222
|
// emitter's (`emitConstraint`). `components` is the report's list of the
|
|
@@ -3544,6 +3560,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3544
3560
|
{
|
|
3545
3561
|
...deformGeometryOf(attachment, at, transforms),
|
|
3546
3562
|
depth: attachmentDepths.get(`${skinName}/${track.slot}/${track.attachment}`) ?? null,
|
|
3563
|
+
soft: attachmentSoftMasks.get(`${skinName}/${track.slot}/${track.attachment}`) ?? null,
|
|
3547
3564
|
},
|
|
3548
3565
|
deformTransforms,
|
|
3549
3566
|
);
|
|
@@ -4174,6 +4191,12 @@ interface AttachmentContext {
|
|
|
4174
4191
|
* read by `evaluateDeformTransform` at key time and never emitted at all.
|
|
4175
4192
|
*/
|
|
4176
4193
|
depths: Map<string, number[]>;
|
|
4194
|
+
/**
|
|
4195
|
+
* A `soft` mask's file and its sampled level per vertex, keyed like `depths`
|
|
4196
|
+
* — what a deform envelope's `mask` reads at key time (issue #1413). Never
|
|
4197
|
+
* emitted, for `depths`' reason.
|
|
4198
|
+
*/
|
|
4199
|
+
softMasks: Map<string, DeformSoftMask>;
|
|
4177
4200
|
/** Every slot the rig declares — a clipping attachment's `end` resolves here. */
|
|
4178
4201
|
slotNames: Set<string>;
|
|
4179
4202
|
/**
|
|
@@ -5477,6 +5500,7 @@ function authoredHullAndEdges(
|
|
|
5477
5500
|
vertexCount: number,
|
|
5478
5501
|
triangles: number[],
|
|
5479
5502
|
where: string,
|
|
5503
|
+
named: { skin: string; slot: string; placeholder: string },
|
|
5480
5504
|
): { hull: number; edges: number[] } {
|
|
5481
5505
|
try {
|
|
5482
5506
|
const outline = traceOutline(vertexCount, triangles);
|
|
@@ -5489,11 +5513,31 @@ function authoredHullAndEdges(
|
|
|
5489
5513
|
checkHullOrder(outline, vertexCount);
|
|
5490
5514
|
return { hull: outline.hull, edges: att.edges ?? meshEdges(vertexCount, triangles, outline.hull) };
|
|
5491
5515
|
} catch (err) {
|
|
5492
|
-
if (err instanceof MeshError) throw new CompileError(`${where}: ${err.message}`);
|
|
5516
|
+
if (err instanceof MeshError) throw new CompileError(`${where}: ${err.message}${splitPointer(vertexCount, triangles, named)}`);
|
|
5493
5517
|
throw err;
|
|
5494
5518
|
}
|
|
5495
5519
|
}
|
|
5496
5520
|
|
|
5521
|
+
/**
|
|
5522
|
+
* The command that answers an outline refusal whose cause is pieces that share
|
|
5523
|
+
* no vertex (issue #1414): one mesh is one outline, so the fix is one mesh per
|
|
5524
|
+
* piece, and `rigc split` writes that proposal. Empty for every other cause —
|
|
5525
|
+
* a pinch, a hole, a doubled triangle — whose fix is the triangles, and for a
|
|
5526
|
+
* list the refusal is about because its indices are not vertices at all.
|
|
5527
|
+
*/
|
|
5528
|
+
function splitPointer(vertexCount: number, triangles: readonly number[], named: { skin: string; slot: string; placeholder: string }): string {
|
|
5529
|
+
if (triangles.length % 3 !== 0 || triangles.some((v) => !Number.isInteger(v) || v < 0 || v >= vertexCount)) return '';
|
|
5530
|
+
const count = meshPieces(vertexCount, triangles).pieces.length;
|
|
5531
|
+
if (count < 2) return '';
|
|
5532
|
+
// Each name one shell word (issue #1442): the command is copied into a shell, and a bare `left legs` is two arguments.
|
|
5533
|
+
const skin = named.skin === 'default' ? '' : ` --skin ${shellWord(named.skin)}`;
|
|
5534
|
+
return (
|
|
5535
|
+
` — the triangles are ${count} pieces that share no vertex, and one mesh is one outline: ` +
|
|
5536
|
+
`\`rigc split --rig <rig> --motion <motion> --slot ${shellWord(named.slot)} --attachment ${shellWord(named.placeholder)}${skin} --out <dir>\` ` +
|
|
5537
|
+
'writes one mesh per piece, its deform keys renumbered (AUTHORING §3.4.11)'
|
|
5538
|
+
);
|
|
5539
|
+
}
|
|
5540
|
+
|
|
5497
5541
|
/**
|
|
5498
5542
|
* The same two fields for geometry rigc built. The generators already put the
|
|
5499
5543
|
* outline first and in order — a contour is all outline, a ring's outer ring
|
|
@@ -5577,7 +5621,7 @@ function buildRigMesh(
|
|
|
5577
5621
|
vertices = { weighted: false, xy: raw.map(f32) };
|
|
5578
5622
|
}
|
|
5579
5623
|
}
|
|
5580
|
-
const { hull, edges } = authoredHullAndEdges(att, uvCount / 2, att.triangles, where);
|
|
5624
|
+
const { hull, edges } = authoredHullAndEdges(att, uvCount / 2, att.triangles, where, { skin: ctx.skinName, slot: ctx.slotName, placeholder });
|
|
5581
5625
|
// `width`/`height` are the size of the image the mesh is drawn on. Stated
|
|
5582
5626
|
// wins; otherwise the PNG the attachment names, already measured for the
|
|
5583
5627
|
// atlas — the same `CompiledImage.width` a region reads, so a page with a
|
|
@@ -6292,7 +6336,7 @@ function softRegionWeights(
|
|
|
6292
6336
|
grid: SheetGrid,
|
|
6293
6337
|
where: string,
|
|
6294
6338
|
ctx: AttachmentContext,
|
|
6295
|
-
): { weights: MeshVertexWeight[][]; bone: string; mask: string; digest: string; carried: number; ramped: number } {
|
|
6339
|
+
): { weights: MeshVertexWeight[][]; levels: number[]; bone: string; mask: string; digest: string; carried: number; ramped: number } {
|
|
6296
6340
|
const { bone } = spec;
|
|
6297
6341
|
if (typeof bone !== 'string' || bone.length === 0) {
|
|
6298
6342
|
throw new CompileError(`${where}: the "soft" block has no "bone"; it is the bone the soft region is carried by`);
|
|
@@ -6316,8 +6360,10 @@ function softRegionWeights(
|
|
|
6316
6360
|
const [toSheetX, toSheetY] = sheetRatio(grid);
|
|
6317
6361
|
let carried = 0;
|
|
6318
6362
|
let ramped = 0;
|
|
6363
|
+
const levels: number[] = [];
|
|
6319
6364
|
const weights = points.map(([x, y]) => {
|
|
6320
6365
|
const w = sampleLevel(sheet, x * toSheetX, y * toSheetY) / 255;
|
|
6366
|
+
levels.push(w);
|
|
6321
6367
|
if (w >= 1) carried++;
|
|
6322
6368
|
else if (w > 0) ramped++;
|
|
6323
6369
|
if (w <= 0) return [{ bone: 'anchor', weight: 1 }] as MeshVertexWeight[];
|
|
@@ -6334,7 +6380,7 @@ function softRegionWeights(
|
|
|
6334
6380
|
'check that the mask is painted where the mesh actually is, and that it is white where the art is soft.',
|
|
6335
6381
|
);
|
|
6336
6382
|
}
|
|
6337
|
-
return { weights, bone, mask: spec.mask, digest: depthDigest(sheet), carried, ramped };
|
|
6383
|
+
return { weights, levels, bone, mask: spec.mask, digest: depthDigest(sheet), carried, ramped };
|
|
6338
6384
|
}
|
|
6339
6385
|
|
|
6340
6386
|
/**
|
|
@@ -6461,6 +6507,7 @@ function buildGridAttachment(
|
|
|
6461
6507
|
ctx.meshBones.add(ctx.anchorBone);
|
|
6462
6508
|
if (bound) ctx.meshBones.add(bound.bone);
|
|
6463
6509
|
if (depth !== undefined) ctx.depths.set(`${ctx.skinName}/${ctx.slotName}/${placeholder}`, depth.z);
|
|
6510
|
+
if (bound) ctx.softMasks.set(`${ctx.skinName}/${ctx.slotName}/${placeholder}`, { mask: bound.mask, levels: bound.levels });
|
|
6464
6511
|
ctx.meshes.push({
|
|
6465
6512
|
slot: ctx.slotName,
|
|
6466
6513
|
kind: 'grid',
|
|
@@ -8520,6 +8567,12 @@ export interface DeformGeometry {
|
|
|
8520
8567
|
* caller, which is where the skin, slot and attachment names live.
|
|
8521
8568
|
*/
|
|
8522
8569
|
depth: readonly number[] | null;
|
|
8570
|
+
/**
|
|
8571
|
+
* The attachment's `soft` mask and its level per vertex, when its generator
|
|
8572
|
+
* named one — what a deform envelope's `mask` reads (issue #1413). Filled in
|
|
8573
|
+
* by the caller, as `depth` is, and for the same reason.
|
|
8574
|
+
*/
|
|
8575
|
+
soft: DeformSoftMask | null;
|
|
8523
8576
|
}
|
|
8524
8577
|
|
|
8525
8578
|
/**
|
|
@@ -8596,8 +8649,9 @@ export function deformGeometryOf(
|
|
|
8596
8649
|
influenceBones: null,
|
|
8597
8650
|
setupWhy: null,
|
|
8598
8651
|
// Filled in by the caller; `deformGeometryOf` reads the attachment's
|
|
8599
|
-
// record, and the depth is deliberately not in it.
|
|
8652
|
+
// record, and the depth is deliberately not in it — nor the soft mask.
|
|
8600
8653
|
depth: null,
|
|
8654
|
+
soft: null,
|
|
8601
8655
|
};
|
|
8602
8656
|
}
|
|
8603
8657
|
// Walk the bindings for the per-vertex influence counts. Their shape is
|
|
@@ -8631,7 +8685,7 @@ export function deformGeometryOf(
|
|
|
8631
8685
|
// LOADED attachment, which is where the `/3*2` in the parser comes from.
|
|
8632
8686
|
let influences = 0;
|
|
8633
8687
|
for (const n of boneCounts) influences += n;
|
|
8634
|
-
const common = { weighted, deformLength: influences * 2, vertexCount: boneCounts.length, boneCounts, depth: null };
|
|
8688
|
+
const common = { weighted, deformLength: influences * 2, vertexCount: boneCounts.length, boneCounts, depth: null, soft: null };
|
|
8635
8689
|
// -- one bind space, unchanged -------------------------------------------
|
|
8636
8690
|
//
|
|
8637
8691
|
// Every vertex a single influence of a single bone: the run IS that bone's
|
|
@@ -8874,7 +8928,14 @@ function compileDeformTrack(
|
|
|
8874
8928
|
'yourself and start the run with "offset".',
|
|
8875
8929
|
);
|
|
8876
8930
|
}
|
|
8877
|
-
const report = evaluateDeformTransform(
|
|
8931
|
+
const report = evaluateDeformTransform(
|
|
8932
|
+
key.transform,
|
|
8933
|
+
geometry.setup,
|
|
8934
|
+
onModelGrid,
|
|
8935
|
+
`${where} (t=${key.t})`,
|
|
8936
|
+
geometry.depth,
|
|
8937
|
+
geometry.soft,
|
|
8938
|
+
);
|
|
8878
8939
|
if (report.offsets.length !== geometry.vertexCount * 2) {
|
|
8879
8940
|
// Unreachable while `setup` is one pair per vertex, which is the whole
|
|
8880
8941
|
// reason both are derived from the same walk. Stated rather than
|
package/src/core/entry.ts
CHANGED
|
@@ -20,7 +20,11 @@
|
|
|
20
20
|
* `openTracks`, issue #1336; `physicsClockStep` and `PHYSICS_CLOCK_LEAD`,
|
|
21
21
|
* the clock a live track's `clock` option takes, issue #1396;
|
|
22
22
|
* `PhysicsEnvironment`, what a live track's `setEnvironment` takes, issue
|
|
23
|
-
* #1416
|
|
23
|
+
* #1416; `bezierPolyline` and `BEZIER_PIECES`, the polyline the poser draws
|
|
24
|
+
* a Bézier segment as, so that a fitter holds its tolerance to the poser's
|
|
25
|
+
* own evaluation rather than to a mirror of it, issue #1417 — re-exported,
|
|
26
|
+
* never copied, because a copy is the drift the request exists to remove;
|
|
27
|
+
* `CA12` holds the identity).
|
|
24
28
|
* The list is `scripts/install_smoke.ts`'s `OBSERVED_SYMBOLS` rows for
|
|
25
29
|
* `./core`, and a symbol added here and not there is not promised.
|
|
26
30
|
*/
|
|
@@ -39,4 +43,5 @@ export type { DrawnRegion, UvPage, UvRegion, UvSource } from './uvs.ts';
|
|
|
39
43
|
export { clipThrough, clipShapeOf, convexWhy, REGION_UVS, REGION_TRIANGLES } from './clipping.ts';
|
|
40
44
|
export type { ClipShape, ClipResult } from './clipping.ts';
|
|
41
45
|
export { eventsFired } from './events.ts';
|
|
46
|
+
export { bezierPolyline, BEZIER_PIECES } from './animation.ts';
|
|
42
47
|
export type { ModelPage, ModelPageRegion } from '../model.ts';
|