@rig-c/core 2.62.0 → 2.64.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/bin/rigc.cjs +18 -12
- package/docs/AUTHORING.md +130 -3
- package/package.json +1 -1
- package/src/cli/core_commands.ts +182 -13
- package/src/cli/shared.ts +29 -5
- package/src/cli/write_whole.ts +92 -0
- package/src/overlaps.ts +545 -0
- package/src/render_shared.ts +17 -0
- package/tools/font5x7.ts +35 -5
package/README.md
CHANGED
|
@@ -606,7 +606,7 @@ commands take it and what its default is.
|
|
|
606
606
|
| `ingest <skeleton.json> --out <dir>` | `build` run backwards: reads a Spine 4.3 skeleton and writes the rig spec and motion spec that **rebuild it**, plus a findings report naming everything it could not carry. a skeleton that declares no stage is carried as declaring none, `--stage x,y,w,h` adds a box to one — and is refused, rather than ignored, beside one that declares a box — and `--images <dir>` writes the spec's own images directory — the opposite direction from `build --images`, which overrides it — so the rebuild carries no flag at all |
|
|
607
607
|
| `split --rig … --motion … --slot <name> --out <dir>` | the answer to a mesh refused because its triangles are **several pieces that share no vertex** — two legs after the triangles bridging them were deleted. It reads the two specs and writes them back into an absent or empty `--out` with one mesh per piece: piece 1 stays in the slot, piece k gets a new slot `<slot>_<k>` after it, each with the original's uvs and weights unchanged, its triangles renumbered outline first where the hull rule needs it, and every deform key re-sliced to the new indices — plus `split.json`, where every original vertex and triangle went. One piece is said so and nothing is written; what it cannot carry is refused by name ([AUTHORING §3.4.11](docs/AUTHORING.md)) |
|
|
608
608
|
| `explain --rig … --motion …` | the compiled rig as a table — every bone with its resolved parent, the slots in draw order, every timeline key by key. Writes nothing. What to reach for when a rig compiles and still looks wrong |
|
|
609
|
-
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`, one directory per set: `<anim>@<fps>fps/` rather than `<anim>/` at any rate but the default — `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim>/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid. `--folds` adds a `folds.json` per set and prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count and which triangles, their world box and their setup-pose area. `--pass-through <cloth>:<leg>` adds a `pass-through.json` per set and prints the frames where a cloth drawn over a leg has its side inside the leg's drawn outline, with the depth and the world box — a reading, not a gate. `--seam <slot>:<slot>` adds a `seam.json` per set and prints, per pair, the frames where two slots that meet open background inside the joint (a gap) or show the overlap band of one past the other (a ghost), with the worst frame's cell count and world box — a reading, not a gate; the exit status never moves. `--angle-range <bone>:<relative-to>` prints how far one bone turns against another over each set: the lowest and highest angle and the frames they fall on — a reading, not a gate. `--viewport <x,y,w,h>` or `--viewport-from <frames.json>` draws over a given box instead of a fitted one, so two builds land on one pixel grid; a slot that reaches past it is named by set and frame, and `frames.json` records it. `--environment <schedule.json>` draws the rig under the wind and gravity its physics answers, in time — the same frames on the core and on spine-core; an inspection of the response the rig states, not a scene |
|
|
609
|
+
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`, one directory per set: `<anim>@<fps>fps/` rather than `<anim>/` at any rate but the default — `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim>/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid. `--folds` adds a `folds.json` per set and prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count and which triangles, their world box and their setup-pose area. `--pass-through <cloth>:<leg>` adds a `pass-through.json` per set and prints the frames where a cloth drawn over a leg has its side inside the leg's drawn outline, with the depth and the world box — a reading, not a gate. `--seam <slot>:<slot>` adds a `seam.json` per set and prints, per pair, the frames where two slots that meet open background inside the joint (a gap) or show the overlap band of one past the other (a ghost), with the worst frame's cell count and world box — a reading, not a gate; the exit status never moves. `--overlaps` adds an `overlaps.json` per set and prints one `OVERLAP "<under>" under "<over>"` line per pair of slots where the one drawn later covers the earlier one and the motion moves it — the frames, and the most pixels covered and the frame of them — with an outline plate per covered frame; the render reports what was drawn, and the draw-order key is the author's remedy — a reading, not a gate. `--angle-range <bone>:<relative-to>` prints how far one bone turns against another over each set: the lowest and highest angle and the frames they fall on — a reading, not a gate. `--viewport <x,y,w,h>` or `--viewport-from <frames.json>` draws over a given box instead of a fitted one, so two builds land on one pixel grid; a slot that reaches past it is named by set and frame, and `frames.json` records it. `--environment <schedule.json>` draws the rig under the wind and gravity its physics answers, in time — the same frames on the core and on spine-core; an inspection of the response the rig states, not a scene |
|
|
610
610
|
| `preview --candidate <dir>` | one self-contained `.html` that plays it, headed by the line `validate <dir>` prints for it and the rigc version — a refused candidate is still previewed, and its header says so in the gate's words. Repeat `--candidate` for one page with a pane per candidate, in the order given; a green `build` ends by naming this command for its own `--out` |
|
|
611
611
|
| `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` |
|
|
612
612
|
| `pose --images <dir> --frame <png>` | reads part placements **out of** a picture |
|
|
@@ -617,7 +617,7 @@ commands take it and what its default is.
|
|
|
617
617
|
| `bench <rung> --candidate <dir>` | one rung of the benchmark ladder |
|
|
618
618
|
| `guide <RULE>` / `guide §<n>[.<m>[.<k>]] [--all]` | the guide's row for the rule a failure line names — every `FAIL` line, `vote --record` refusal, `ingest` finding and coded compile error ends with ` (see rigc guide <RULE>)`; only legacy uncoded library errors point at `§5.1` — or a section's heading, own prose and subsection index, read from the installed `docs/AUTHORING.md` when it runs. `--all` includes the subsection bodies; a leaf prints its full text either way. Sizes describe full ledger spans. `--all` with a rule is refused, exit 2; an unknown name is refused with the nearest real ones, exit 1 |
|
|
619
619
|
| `skills install [--dir …] [--copy]` | links every agent skill the package ships into `.agents/skills` (or `--dir`) as relative symlinks, or copies them with `--copy` — where Codex, Gemini CLI and Antigravity look. A second run has nothing to do; an entry already there that is not this command's is refused by name and nothing is written |
|
|
620
|
-
| `review <dir> […]` | the human review of a build, handed to the `rig-review` bin of `@rig-c/review` installed beside the package
|
|
620
|
+
| `review <dir> […]` | the human review of a build, handed to the `rig-review` bin of `@rig-c/review` installed beside the package; rigc does not depend on it and never installs it. `rigc review <dir> [--reference <video>] [--animation <name>] [--compare <dir>]` is `rig-review open <dir> …` with those flags 1:1 and prints the loopback URL to give a person; a first argument that is one of review's own words (`open`, `check`, `feedback`, `control`, `strip`, `compare`) or starts with `-` (`--help`, `--`) is handed over untouched. stdout is review's byte for byte, stderr review's plus the launcher's own refusals, the exit code review's. Refused by name, exit 2, before the bin starts: `REVIEW_OUTPUT_MISSING <path>` (the target is not a directory); `REVIEW_NOT_INSTALLED` with two lines, `bun add -d @rig-c/review` and `bunx @rig-c/review open <dir> …` carrying the flags given — the same on a terminal and off, with nothing installed or written; `REVIEW_BUN_TOO_OLD: Bun <found>, @rig-c/review needs <required>`, the floor read off the installed package's `engines.bun`; and an empty argument (`""`) under a Bun that drops one (Bun 1.2.0, measured by a probe), `rigc review: argument N is empty, and Bun <version>, the Bun running this, drops an empty argument when it starts a process — …`. docs/AUTHORING.md lists every line |
|
|
621
621
|
|
|
622
622
|
`diff`, `bonedist`, `check` and `bench` measure against something you were given; the
|
|
623
623
|
first three work on any reference you have, and `bench` is a repository workflow that needs a clone
|
package/bin/rigc.cjs
CHANGED
|
@@ -56,7 +56,15 @@ const entry = present === null ? 'cli_core.ts' : 'cli.ts';
|
|
|
56
56
|
const args = process.argv.slice(2);
|
|
57
57
|
const result = spawnSync('bun', [path.join(root, entry), ...args], { stdio: 'inherit' });
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
// ⚠️ The exit and the signal are read before `error` (issue #1488): Bun 1.2.0 —
|
|
60
|
+
// the declared minimum, and what runs this file as `bun bin/rigc.cjs` — sets
|
|
61
|
+
// `error` on every child that exits non-zero, so reading it first answered the
|
|
62
|
+
// CLI's own refusal (exit 2) with "could not launch bun" and exit 1. `error` is
|
|
63
|
+
// a launch that failed only where the child left neither a status nor a signal.
|
|
64
|
+
// Node never sets `error` beside a status, so under node nothing here changes.
|
|
65
|
+
const launched = typeof result.status === 'number' || result.signal !== null;
|
|
66
|
+
|
|
67
|
+
if (launched && result.status === 0 && args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
|
|
60
68
|
process.stderr.write(
|
|
61
69
|
present === null
|
|
62
70
|
? `entry: ${entry} — ${RUNTIME} absent — the round trip and the commands that need it are not available here; see --help\n`
|
|
@@ -64,20 +72,18 @@ if (!result.error && result.status === 0 && args.length === 1 && (args[0] === '-
|
|
|
64
72
|
);
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
if (result.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
} else {
|
|
71
|
-
process.stderr.write(`rigc: could not launch bun: ${result.error.message}\n`);
|
|
72
|
-
}
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (result.signal) {
|
|
75
|
+
if (typeof result.status === 'number') {
|
|
76
|
+
process.exit(result.status);
|
|
77
|
+
} else if (result.signal !== null) {
|
|
77
78
|
// A signal (e.g. Ctrl-C) killed the child — die the same way instead of
|
|
78
79
|
// inventing an exit code, so the caller sees the same thing it would have
|
|
79
80
|
// seen running bun directly.
|
|
80
81
|
process.kill(process.pid, result.signal);
|
|
81
82
|
} else {
|
|
82
|
-
|
|
83
|
+
if (result.error && result.error.code === 'ENOENT') {
|
|
84
|
+
process.stderr.write('rigc runs on Bun, which was not found on PATH — install it from https://bun.sh\n');
|
|
85
|
+
} else if (result.error) {
|
|
86
|
+
process.stderr.write(`rigc: could not launch bun: ${result.error.message}\n`);
|
|
87
|
+
}
|
|
88
|
+
process.exit(1);
|
|
83
89
|
}
|
package/docs/AUTHORING.md
CHANGED
|
@@ -143,9 +143,9 @@ bun cli.ts vote --candidate path/to/spine-a --candidate path/to/spine-b # -> b
|
|
|
143
143
|
bun cli.ts vote --record vote-<id>.json # -> votes.jsonl
|
|
144
144
|
|
|
145
145
|
# …and when the build is for a person to look at rather than for you to choose
|
|
146
|
-
# between, hand it over:
|
|
147
|
-
#
|
|
148
|
-
bun cli.ts review
|
|
146
|
+
# between, hand it over: give the person the loopback URL this prints on stdout
|
|
147
|
+
# (`rigc review <dir>` is `rig-review open <dir>`; rigc review --help is review's own usage):
|
|
148
|
+
bun cli.ts review path/to/spine [--reference reference.mp4] [--animation <name>]
|
|
149
149
|
```
|
|
150
150
|
|
|
151
151
|
`build` compiles, runs the named assertions — through a `@esotericsoftware/spine-core`
|
|
@@ -261,6 +261,7 @@ What the flags mean:
|
|
|
261
261
|
| `--folds` | `render` only: also read which mesh triangles **fold** — turn over against the setup pose — and write a `folds.json` into each frame directory: per mesh, the frames with a folded triangle, the worst frame's count and which triangles, by index, with their world box and setup-pose area. One printed line per mesh that folds; a triangle degenerate or inverted at the setup pose is said once and never counted. A reading, not a gate. Every other file is byte for byte what the render writes without it, `geometry.json` included. Refused with `--slot`/`--hide`, for `--geometry`'s reason — **§8.3** |
|
|
262
262
|
| `--pass-through` | `render` only: `<cloth>:<leg>[,<cloth>:<leg>…]` — also read where each cloth slot **passes through** its leg slot, and write a `pass-through.json` into each frame directory: the frames where the cloth draws over the leg and its drawn side lies inside the leg's drawn outline by 2 steps or more, each with the depth and the world box. One printed line per pair, and one per frame that passes through. The first name must draw over the second for a frame to be read — the declared order, never both. A reading, not a gate. Every other file is byte for byte what the render writes without it. An unknown slot, a slot that draws on no frame, a malformed list and a pair named twice are refused by name, and so is `--slot`/`--hide` beside it — **§8.5** |
|
|
263
263
|
| `--seam` | `render` only: `<slot>:<slot>[,<slot>:<slot>…]` — also read, for each pair, where the two drawings **stop meeting**, and write a `seam.json` into each frame directory: per pair, the frames with a **gap** (background inside the joint, where the two covered together at the setup pose) and the frames with a **ghost** (the overlap band of one showing where, from its partner's point of view, nothing was drawn), each with its cell count, area and world box, and as runs of frames with their times. One printed line per pair; a pair whose drawings share no cell at the setup pose is a **SKIP**. A reading, not a gate: the exit status never moves. Every other file is byte for byte what the render writes without it. An unknown slot, a slot with itself, a slot that draws on no frame, a malformed list and a pair named twice in either order are refused by name, and so is `--slot`/`--hide` beside it — **§8.8** |
|
|
264
|
+
| `--overlaps` | `render` only: also read **which slot each frame drew over which**, and write an `overlaps.json` into each frame directory: per frame, every pair of slots where the slot drawn later covers pixels the earlier one drew (drawn at 128 of 255 or more, on the frames' own pixels), with the pixel count and the pixel box. One `OVERLAP` line per pair that covers anything — its frames as runs, how many of the set, and the most pixels and the frame of them — and one line counting the pairs that are layering the motion does not move. Each frame with a moving pair also gets an **outline plate**, `overlaps-f<NNNN>.png`: every slot in a moving pair as its contour in its own colour, dashed where a slot drawn later covers it, named at its top. The render reports what was drawn; who should be in front is the author's, and the draw-order key in the motion spec is the remedy. A reading, not a gate. Every other file is byte for byte what the render writes without it, and `frames.json` gains each set's `overlaps` record. Refused beside `--slot`/`--hide` — **§8.9** |
|
|
264
265
|
| `--angle-range` | `render` only: `<bone>:<relative-to>[,<bone>:<relative-to>…]` — also read, for each pair, the range of the first bone's world direction against the second's over each set's frames, and print one line per pair per set: the lowest and the highest angle in degrees, the span, and the frame each was first reached on. A reading, not a gate: nothing is written, and every file is the bytes the render writes without it. An unknown bone (with the bones there are), a bone against itself, a malformed list and a pair named twice are refused by name, and so is a bone with no direction on a frame — **§8.7** |
|
|
265
266
|
| `--viewport`, `--viewport-from` | `render`: give the box every frame is drawn over instead of fitting one — `--viewport <x,y,w,h>`, a world box, y up, `x,y` its bottom-left corner, at the pixel size `--max` gives; or `--viewport-from <frames.json \| dir>`, an earlier render's grid exactly (its box, scale and pixel size), so two candidates land on one pixel grid. A slot that reaches past the box is named by set and frame, and `frames.json` records the box was given under `pinned`. The two together are refused, and so is `--max` beside `--viewport-from` — **§8.6**. On `check` and `inspect`, `--viewport` pins the box those commands draw over instead |
|
|
266
267
|
| `--environment` | `render` only: the wind and gravity the rig's physics constraints answer, in time — a schedule file `{ "spec": "rigc-environment/1", "steps": [{ "t": <seconds>, "wind"?: [x, y], "gravity"?: [x, y] }, …] }`, x right and **y down**, each step holding from its `t` until the next. Both posers set each change before the step that poses the first frame at or after its `t`, and draw the same frames, `geometry.json` and `frames.json` (`RE01`). `frames.json` and `geometry.json` record it, and the framing reads it. A malformed file, a half no physics constraint answers, two steps on one frame and a step after every set's last frame are refused by name, exit 2, nothing written; `check` refuses a frame set drawn under one as a reference. Without it every file is the bytes it always was. An inspection of the response the rig states, not a scene — **§8.4** |
|
|
@@ -325,6 +326,29 @@ the package's copy (<file> differs)`. A link to the package's own folder is not
|
|
|
325
326
|
the way however it is spelled, so an absolute link somebody else made is kept and
|
|
326
327
|
reported `already linked`.
|
|
327
328
|
|
|
329
|
+
`review` is the third, and it is a launcher: its body is the `rig-review` bin of
|
|
330
|
+
`@rig-c/review`, a separate package rigc does not depend on and **never installs**.
|
|
331
|
+
`rigc review <dir> [--reference <video>] [--animation <name>] [--compare <dir>]`
|
|
332
|
+
hands over `open <dir> …` with those flags 1:1; a first argument that is one of
|
|
333
|
+
rig-review's own words — `open`, `check`, `feedback`, `control`, `strip`,
|
|
334
|
+
`compare`, or anything starting with `-` (`--help`, `-h`, and `--`, which hands
|
|
335
|
+
the rest over as it is) — is handed over untouched. Nothing else is added: no
|
|
336
|
+
`--reference` is derived, because no build rigc writes records the clip it was
|
|
337
|
+
retargeted from. What comes back is review's: **stdout is review's stdout byte for
|
|
338
|
+
byte** (the URL line an agent gives a person), stderr is review's plus only the
|
|
339
|
+
launcher's own refusals, and the exit code is review's. The launcher relies on
|
|
340
|
+
review's side of that contract — the URL alone on stdout, its diagnostics on
|
|
341
|
+
stderr, its own refusals in its own words — and does not restate it. Before the
|
|
342
|
+
bin starts, it refuses by name, exit 2, nothing on stdout:
|
|
343
|
+
|
|
344
|
+
| Line on stderr | When | What to do |
|
|
345
|
+
| --- | --- | --- |
|
|
346
|
+
| `rigc review: REVIEW_OUTPUT_MISSING <path>: nothing is there — …` (or `it is not a directory`) | the first argument was read as a target and is not a directory. Checked first, before the package is looked for | point it at the `--out` a green `build` wrote, or pass `--` first to hand the argument over as it is |
|
|
347
|
+
| `rigc review: REVIEW_NOT_INSTALLED: @rig-c/review is not installed beside this project or beside rig-c, and rigc does not install it — …`, then `bun add -d @rig-c/review`, then `bunx @rig-c/review <what the bin would have been handed>` | the package resolves neither from the working directory up nor from rig-c's root up. The same three lines on a terminal and off one; nothing is installed, fetched or written, and `package.json` and `bun.lock` are untouched | run the first line to install it, or the second to run it once without installing (it fetches into Bun's cache — that is your choice to make, not rigc's) |
|
|
348
|
+
| `rigc review: REVIEW_BUN_TOO_OLD: Bun <found>, @rig-c/review needs <required>` | the installed package's own `engines.bun` floor is above the running Bun. `<required>` is read off that manifest, not written in rigc | upgrade Bun |
|
|
349
|
+
| `rigc review: REVIEW_BUN_RANGE_UNREAD: @rig-c/review declares engines.bun "<range>", and rigc reads only ">=X.Y.Z" — …` | the installed package's `engines.bun` is a range shape rigc does not read, so whether this Bun satisfies it was not guessed | run the bin yourself (`bunx rig-review …` from the project), or a rigc that reads that shape |
|
|
350
|
+
| `rigc review: argument N is empty, and Bun <version>, the Bun running this, drops an empty argument when it starts a process — …` | an `""` argument under a Bun that would drop it (Bun 1.2.0), measured by a probe | give it a value, or run rigc under a Bun that keeps it |
|
|
351
|
+
|
|
328
352
|
Pick the profile deliberately, and know which one you got by saying nothing. The
|
|
329
353
|
default is `spine`: "is this valid Spine 4.3 that any runtime plays correctly",
|
|
330
354
|
which is the question a rig authored anywhere is asking. `--profile rig-play`
|
|
@@ -1137,6 +1161,7 @@ bun cli.ts render --candidate path/to/spine --geometry # + geometry.json per
|
|
|
1137
1161
|
bun cli.ts render --candidate path/to/spine --folds # + folds.json per set: which mesh triangles turn over, and when
|
|
1138
1162
|
bun cli.ts render --candidate path/to/spine --pass-through skirt:leg # + pass-through.json: where a cloth goes through a leg
|
|
1139
1163
|
bun cli.ts render --candidate path/to/spine --seam thigh:shin # + seam.json: where two slots that meet open a gap or show a ghost
|
|
1164
|
+
bun cli.ts render --candidate path/to/spine --overlaps # + overlaps.json and outline plates: which slot each frame drew over which
|
|
1140
1165
|
bun cli.ts render --candidate path/to/spine --angle-range tail:hip # how far a bone turns against another, per animation
|
|
1141
1166
|
bun cli.ts render --candidate path/to/spine --viewport-from render/ # an earlier render's grid: two builds, one pixel grid
|
|
1142
1167
|
bun cli.ts preview --candidate path/to/spine [--candidate path/to/another …] [--animation …] [--out preview.html]
|
|
@@ -8879,6 +8904,107 @@ same file, and two runs write the same bytes (the selftest's `seam` suite).
|
|
|
8879
8904
|
look at a frame it names, draw that set over the box it printed:
|
|
8880
8905
|
`render --animation <name> --viewport <x,y,w,h> --max 400`.
|
|
8881
8906
|
|
|
8907
|
+
### 8.9 Which slot covers which — `render --overlaps`
|
|
8908
|
+
|
|
8909
|
+
An arm the draw order puts behind the torso simply vanishes from the frames, on a
|
|
8910
|
+
build that passes every assertion and every check, and nothing in the frames names
|
|
8911
|
+
it. `--overlaps` reads it off the frames `render` draws: **who covers whom, on which
|
|
8912
|
+
frames, by how many pixels**. On the selftest's probe (`overlap_probe`,
|
|
8913
|
+
`fixtures/public.ts`: an opaque stage, a still body, and an arm drawn after the body
|
|
8914
|
+
slid along x across it and out the other side):
|
|
8915
|
+
|
|
8916
|
+
```bash
|
|
8917
|
+
bun cli.ts render --candidate path/to/spine --animation cross --overlaps --max 160 [--skin …] [--fps 12]
|
|
8918
|
+
# rigc render
|
|
8919
|
+
# .. cross 25 frame(s), 2.000s + contact.png + overlaps.json (1 moving pair(s), 11 outline plate(s)) -> …/cross
|
|
8920
|
+
# .. overlap "cross": 2 pair(s) cover on every frame by the same pixels, or with the slot in front drawn wholly on the one behind — layering the motion does not move; overlaps.json lists them
|
|
8921
|
+
# .. overlap "cross": OVERLAP "body" under "arm": frames 7–17 (11 of 25), covered up to 135 px at frame 11
|
|
8922
|
+
```
|
|
8923
|
+
|
|
8924
|
+
⭐ **The render reports what was drawn; who should be in front is the author's, and
|
|
8925
|
+
the draw-order key in the motion spec is the remedy** (§4.7, `drawOrder`). The same
|
|
8926
|
+
slide with a key that puts the arm before the body reads `OVERLAP "arm" under
|
|
8927
|
+
"body"` on the same frames and pixels (`OX02`). On `gallery/walk` at `--fps 20 --max
|
|
8928
|
+
288` the back arm reads `OVERLAP "arm_b" under "torso": frames 0–18 (19 of 19),
|
|
8929
|
+
covered up to 643 px at frame 15`; with `arm_b` moved after `torso` in the rig's
|
|
8930
|
+
slots, that line is gone and `OVERLAP "torso" under "arm_b"` reads the same 643 px at
|
|
8931
|
+
frame 15.
|
|
8932
|
+
|
|
8933
|
+
**Which frames are read.** Every frame of every set drawn, in **that frame's own
|
|
8934
|
+
draw order** — the order its pieces are drawn in, so a draw-order key is honoured.
|
|
8935
|
+
|
|
8936
|
+
**What covering means.** Every piece is rasterised in draw order on the render's own
|
|
8937
|
+
viewport, so a pixel here is a pixel of `f<NNNN>.png`. A pixel is drawn by a slot
|
|
8938
|
+
when the renderer paints it at half opacity or more (128 of 255) — the line every
|
|
8939
|
+
other reading draws outlines at. Each pixel keeps the slots that have drawn it so
|
|
8940
|
+
far; a slot drawing a pixel earlier slots already drew **covers** each of them there,
|
|
8941
|
+
one pixel to each (earlier, later) pair. `under` is the earlier-drawn slot, `over`
|
|
8942
|
+
the later-drawn one. Pairs are by slot name, so `a` under `b` and `b` under `a` —
|
|
8943
|
+
one draw-order key apart — are two pairs.
|
|
8944
|
+
|
|
8945
|
+
**Layering the motion does not move.** A pair that covers on **every** frame, and on
|
|
8946
|
+
each either by the **same** pixel count or with the slot in front drawn **wholly on**
|
|
8947
|
+
the one behind (every pixel `over` draws is one `under` drew first), is static
|
|
8948
|
+
layering: a backdrop under a part that never leaves it, a face drawn over the head
|
|
8949
|
+
that carries it. It is counted on one line and listed in the file, never printed
|
|
8950
|
+
pair by pair. ⚠️ The pixel count alone was measured not to be enough: on
|
|
8951
|
+
`gallery/walk`, a part turning over the backdrop changes its pixel count from frame to
|
|
8952
|
+
frame by rasterisation alone, so none of its 35 pairs read as static and 14 backdrop lines (and the eyes over the head) buried the ones that mattered.
|
|
8953
|
+
Every other pair that covers anything prints one `OVERLAP` line; a pair that never
|
|
8954
|
+
covers prints nothing.
|
|
8955
|
+
|
|
8956
|
+
**Blind spots, stated.** A fringe painted under half opacity covers nothing here
|
|
8957
|
+
although it tints the frame. A slot in front lying wholly on the one behind on every
|
|
8958
|
+
frame reads as layering however it moves across it — an arm swung over the torso
|
|
8959
|
+
without ever leaving it. A slot drawing more than one piece is one slot. And the
|
|
8960
|
+
counts are pixels at the render's `--max`: quote the size beside a figure.
|
|
8961
|
+
|
|
8962
|
+
**The file.** `overlaps.json`, `rigc-overlaps/1`, beside each set's frames:
|
|
8963
|
+
`animation`, `skin` (absent when none was set), `fps`, `reading` (`drawnAlpha` 128,
|
|
8964
|
+
`unit` `"frame pixels"`, and `rule`, the definition above), `frames[]` — every
|
|
8965
|
+
frame, with `index`, `time` and `pairs[]` (`under`, `over`, `px`, and `box`
|
|
8966
|
+
`[x0, y0, x1, y1]`: frame pixels, x right, y down, both ends inclusive) — and
|
|
8967
|
+
`pairs[]`, one per pair that covers on any frame: `under`, `over`, `frames` (how
|
|
8968
|
+
many it covers on), `of` (the set's frames), `runs` (`[[first, last], …]`,
|
|
8969
|
+
inclusive), `most` (`{ index, px }`, the earliest on a tie), `inside` (the slot in
|
|
8970
|
+
front wholly on the one behind on every frame it covers on) and `varies` (`false`
|
|
8971
|
+
for static layering). Both lists are ordered by `under`, then `over`, by code unit.
|
|
8972
|
+
|
|
8973
|
+
**The outline plate.** `overlaps-f<NNNN>.png`, numbered as the frames are, for each
|
|
8974
|
+
frame where a moving pair covers: a white plate of the frame's size; every slot in a
|
|
8975
|
+
moving pair drawn as its **contour** — a drawn pixel with an undrawn neighbour left,
|
|
8976
|
+
right, above or below — in its own colour, in draw order; a contour pixel some slot
|
|
8977
|
+
drawn later also drew is **dashed** (drawn where `(x + y) % 4 < 2`); and the slot's
|
|
8978
|
+
name, spelled as the lines spell it (`arm_b`, not `ARM_B`). Names are placed in draw
|
|
8979
|
+
order and no two touch: each takes the first free box, trying for every contour pixel
|
|
8980
|
+
in raster order (the first is the topmost, the leftmost on a tie) the box above it,
|
|
8981
|
+
below it, to its left and to its right; when none is free, the free spot nearest the
|
|
8982
|
+
topmost pixel, joined to it by a one-pixel leader line; every box clamped inside the
|
|
8983
|
+
plate. A plate with no free spot left prints the name at its first box anyway — the
|
|
8984
|
+
one case two names can meet. A slot keeps one colour across a set's plates (a fixed table of eight,
|
|
8985
|
+
by the slot's place among the set's moving slots in code-unit order); the colour
|
|
8986
|
+
means only "another slot" — the name says which.
|
|
8987
|
+
|
|
8988
|
+
**`frames.json`.** Each set gains `overlaps`: `file`, `pairs` (how many cover on any
|
|
8989
|
+
frame), `static` (how many of those are layering), `plates`, and `moving[]` — each
|
|
8990
|
+
moving pair's `under`, `over`, `frames` and `most` — so a reader that cannot open a
|
|
8991
|
+
plate has the figures.
|
|
8992
|
+
|
|
8993
|
+
**Refused, nothing written:** `--slot`/`--hide` beside the flag. A slot left out
|
|
8994
|
+
covers nothing, so the pairs it is in would vanish and the reading would describe a
|
|
8995
|
+
picture of part of the rig — run `render --overlaps` on the whole rig and the subset
|
|
8996
|
+
without it; both land on one grid.
|
|
8997
|
+
|
|
8998
|
+
**Nothing else moves.** The PNGs, `contact.png` and every other file are byte for
|
|
8999
|
+
byte what the render writes without the flag, and `frames.json` differs only by
|
|
9000
|
+
each set's `overlaps` record (`OX05`); two runs and both posers write the same file
|
|
9001
|
+
and plates (`OX04`).
|
|
9002
|
+
|
|
9003
|
+
**A reading, not a gate.** It never changes the exit status. Whether a covered arm
|
|
9004
|
+
is a defect is yours; when it is, the remedy is a `drawOrder` key in the motion
|
|
9005
|
+
spec (or the slot's place in the rig's `slots`), and the next render says whether
|
|
9006
|
+
the line moved.
|
|
9007
|
+
|
|
8882
9008
|
---
|
|
8883
9009
|
|
|
8884
9010
|
## 9. Checking against the frames — `rigc check`
|
|
@@ -11706,6 +11832,7 @@ pixel (`IN09`).
|
|
|
11706
11832
|
| Does this part's edge follow that bone? | `--show art,weights:<bone>,mesh --overlay-slot <part> --zoom slot:<part>` | the part's own triangles and the bone's weight over them alone, over the whole painting. Where slots stack — a neck under the face, the hair, a collar — every mesh crossing the box is drawn and the heat is the union of every slot the bone moves, so the part's edge has no pixel of its own until the others are left out. The legend's `meshes[]` gives the part's weight for the bone at every vertex, so "does the top row follow the head" is read as numbers as well as colour |
|
|
11707
11833
|
| Is this rig's density where its motion is? | `--show art,density --dim` | each region and mesh filled by its **ratio** — its triangles per world unit² over the figure's — on one ramp, log-scaled and centred on 1×: grey is as dense as the figure, pink to magenta denser (4×, 16×), light to dark green sparser (1/4×, 1/16×), clamped past the end stops (the stops are in the legend). A part's area is its posed mesh's own, the sum of its triangles' areas at the pose drawn, not the image's box; a region is two triangles over its quad. Where parts overlap the pixel is the part drawn last, as in the art, so a part under others is read with `--overlay-slot <part>`. The figure's totals are always over every slot the pose shows — the filter picks what is drawn and listed, never what the figure is — and the legend's `density` gives each slot's attachment, kind, vertices, triangles, hull, area, triangles per area, ratio and the bones it is weighted to, in draw order. A measurement, not a verdict: no bar is drawn, and `A13_MESH_BUDGET` stays the one gate on counts |
|
|
11708
11834
|
| Does a joint tear in this animation? | `render --seam <a>:<b> --animation <name>` (§8.8) | One line per pair: the frames where background opens inside the joint (gap) or the overlap band of one piece shows past its partner (ghost), with the worst frame's cell count and world box; `seam.json` carries every frame and its time, and the runs of frames. A pair whose drawings never overlap is a SKIP, said by name. A reading, not a verdict — the exit status never moves |
|
|
11835
|
+
| Which slot covers which in this animation? | `render --overlaps --animation <name>` (§8.9) | One `OVERLAP "<under>" under "<over>"` line per pair that moves: the frames it covers on, as runs, and the most pixels it covers and the frame of them; one line counting the layering the motion does not move. `overlaps.json` carries every frame's pairs with their pixel count and box, and `overlaps-f<NNNN>.png` draws each covered frame's contours, the covered part dashed. A reading, not a verdict — the draw-order key is the remedy |
|
|
11709
11836
|
| Does anything fold in this animation? | `render --folds --animation <name>` (§8.3), then `inspect --show art,folds,mesh --animation <name> --at <the worst frame's time> --zoom folds` | `render --folds` prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count, which triangles, their world box at that frame and their setup-pose area. The view fills those triangles at the pose drawn — every pixel whose centre a folded triangle holds — in the colour the legend's `folds` states, and the legend's `folds[]` names, per mesh, the folded triangles by index with the same box and area, and the setup pose's degenerate and inverted ones, which are never counted. A fold is usually a sliver a few units across, invisible in a picture of the whole mesh: `--zoom folds` frames the picture to the folded triangles' box. A thin sliver still needs pixels — raise `--max` when the fill is a line. A reading, not a verdict |
|
|
11710
11837
|
| How is this part triangulated? | `--show art,mesh,hull --zoom slot:<slot>` | each mesh's triangle edges, and over them its hull — vertex 0 to 1 to … to `hull − 1` and back. Regions are not drawn: a region is not a mesh. The legend lists every vertex in world units and in pixels |
|
|
11711
11838
|
| Which is drawn over which? | `--show art,slots` | each slot that shows a region or mesh numbered by its place in the draw order **at the pose drawn** (0 is drawn first), on a white box at the centre of its attachment's box — so a `drawOrder` key (§4.7) shows from the first frame `render` draws it on |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.64.0",
|
|
4
4
|
"description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/cli/core_commands.ts
CHANGED
|
@@ -38,6 +38,8 @@ import { FOLDS_FILE, foldLine, foldsFileOf, foldsOf, foldsText, setupFaultLine }
|
|
|
38
38
|
import { AngleRangeError, angleRangeLine, type AngleRangeReading, angleRangesOf, type BonePair, readBonePairs } from '../anglerange.ts';
|
|
39
39
|
import { outsideLine, outsideViewport, pinnedRecord, readViewportBox, viewportOfFramesFile, ViewportError } from '../viewportpin.ts';
|
|
40
40
|
import { PASS_THROUGH_FILE, passFrameLine, type PassPair, passSummaryLine, passThroughFileOf, passThroughOf, passThroughText, PassThroughError, readPassPairs } from '../passthrough.ts';
|
|
41
|
+
import type { Plate } from '../../tools/plate.ts';
|
|
42
|
+
import { OVERLAPS_FILE, OverlapError, overlapLine, overlapPlate, overlapsFileOf, overlapsOf, overlapsPlateFile, overlapsText, plateParticipants, staticLine } from '../overlaps.ts';
|
|
41
43
|
import { readSeamPairs, SEAM_FILE, seamFileOf, seamLine, SeamError, type SeamPair, seamsOf, seamText } from '../seam.ts';
|
|
42
44
|
import { environmentCollisions, environmentFrame, EnvironmentError, environmentResponse, readEnvironmentSchedule, type EnvironmentStep } from '../environment.ts';
|
|
43
45
|
import { estimatePose, poseLines, type PoseOptions } from '../pose.ts';
|
|
@@ -45,6 +47,7 @@ import { BACKGROUND, contactSheet, type Frame, FRAMES_SIDECAR, FRAMES_SPEC, fram
|
|
|
45
47
|
import { type CompileResult } from '../types.ts';
|
|
46
48
|
import { attachmentRegionJoins } from '../region_joins.ts';
|
|
47
49
|
import { cmdRepack } from './repack.ts';
|
|
50
|
+
import { writeWhole } from './write_whole.ts';
|
|
48
51
|
import { GUIDE_FILE, GUIDE_RULE_TABLES_ELSEWHERE, guideAnswer, refusalLine } from '../guide.ts';
|
|
49
52
|
import { ATLAS_ABSENT, COMMANDS, type CommandRun, DelegateError, modelTextBeside, resolveBuild, type BuildGate, runBuild, PACKAGE_ROOT, DEFAULT_CHAINFIT_OUT, DEFAULT_POSE_OUT, DEFAULT_SKILLS_DIR, ExplainError, GuideError, meshBudget, meshDepthNote, meshFit, meshInfluenceNote, PAGE_GRID_UNLOCATED, readAnimationFlag, readJsonFile, readPoserFlag, readSkeletonText, readVersion, resolveCut, resolveDrawable, runCheck, SkillsInstallError, STAGELESS_FRAMING, UsageError, writeJson, documentStageBeside } from './shared.ts';
|
|
50
53
|
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
@@ -704,6 +707,22 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
704
707
|
}
|
|
705
708
|
}
|
|
706
709
|
|
|
710
|
+
// The overlap reading (issue #1480) is its own flag for --folds' reason, and refused beside a subset for
|
|
711
|
+
// --pass-through's: who covers whom is a fact of every slot the rig draws, and a subset drops some of them — a slot
|
|
712
|
+
// left out covers nothing, so the pairs it is in would vanish and the reading would describe a picture of part of the rig.
|
|
713
|
+
const overlaps = flags.overlaps !== undefined;
|
|
714
|
+
if (overlaps) {
|
|
715
|
+
for (const flag of ['slot', 'hide'] as const) {
|
|
716
|
+
if (flags[flag] !== undefined) {
|
|
717
|
+
throw new UsageError(
|
|
718
|
+
`--overlaps takes no --${flag}: it reads which slot covers which as the whole rig draws them, and a subset of the ` +
|
|
719
|
+
`drawn slots would drop the pairs the slots left out are in. Run \`rigc render --overlaps\` on the whole rig for the reading, and --${flag} ${flags[flag]} ` +
|
|
720
|
+
'without --overlaps for the pictures — both land on the same grid',
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
707
726
|
// The seam reading (issue #1456) is its own flag for --folds' reason, and refused beside a subset for --pass-through's:
|
|
708
727
|
// it reads the two slots as the whole rig draws them.
|
|
709
728
|
const seam = flags.seam;
|
|
@@ -913,9 +932,30 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
913
932
|
}
|
|
914
933
|
}
|
|
915
934
|
}
|
|
916
|
-
|
|
935
|
+
// The overlap reading, off the very pieces the frames are drawn from, in their draw order, on the frames' own
|
|
936
|
+
// viewport — one per set — and its outline plates, drawn here so a refusal leaves nothing written.
|
|
937
|
+
const overlapReadings = new Map<string, { file: ReturnType<typeof overlapsFileOf>; plates: Map<number, Plate> }>();
|
|
938
|
+
if (overlaps) {
|
|
939
|
+
for (const [name, frames] of sampled) {
|
|
940
|
+
const animation = name === SETUP_POSE_DIR && facts.animations.length === 0 ? null : name;
|
|
941
|
+
try {
|
|
942
|
+
const reading = overlapsOf(frames, pages, viewport);
|
|
943
|
+
const participants = plateParticipants(reading.pairs);
|
|
944
|
+
const plates = new Map<number, Plate>();
|
|
945
|
+
for (const index of reading.plated.keys()) {
|
|
946
|
+
const plate = overlapPlate(reading, index, participants, viewport);
|
|
947
|
+
if (plate !== null) plates.set(index, plate);
|
|
948
|
+
}
|
|
949
|
+
overlapReadings.set(name, { file: overlapsFileOf(animation, fps, skin, reading), plates });
|
|
950
|
+
} catch (err) {
|
|
951
|
+
if (err instanceof OverlapError) throw new UsageError(`--overlaps, ${JSON.stringify(name)}: ${err.message}`);
|
|
952
|
+
throw err;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return { viewport, fitted, sampled, geometryTexts, foldReadings, passReadings, seamReadings, angleReadings, overlapReadings };
|
|
917
957
|
});
|
|
918
|
-
const { viewport, fitted, sampled, geometryTexts, foldReadings, passReadings, seamReadings, angleReadings } = posed.value;
|
|
958
|
+
const { viewport, fitted, sampled, geometryTexts, foldReadings, passReadings, seamReadings, angleReadings, overlapReadings } = posed.value;
|
|
919
959
|
console.log(` .. poser ${posed.note}`);
|
|
920
960
|
if (environment !== undefined) {
|
|
921
961
|
const response = environmentResponse(skeletonText);
|
|
@@ -969,6 +1009,11 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
969
1009
|
if (passReading !== undefined) writeFileSync(join(dir, PASS_THROUGH_FILE), passThroughText(passReading));
|
|
970
1010
|
const seamReading = seamReadings.get(name);
|
|
971
1011
|
if (seamReading !== undefined) writeFileSync(join(dir, SEAM_FILE), seamText(seamReading));
|
|
1012
|
+
const overlapReading = overlapReadings.get(name);
|
|
1013
|
+
if (overlapReading !== undefined) {
|
|
1014
|
+
writeFileSync(join(dir, OVERLAPS_FILE), overlapsText(overlapReading.file));
|
|
1015
|
+
for (const [index, plate] of overlapReading.plates) plate.writePng(join(dir, overlapsPlateFile(index)));
|
|
1016
|
+
}
|
|
972
1017
|
const duration = frames[frames.length - 1].time;
|
|
973
1018
|
const physics = physicsDeclared.length === 0 ? undefined : physicsSteppedOf(frames, fps);
|
|
974
1019
|
sets.push({
|
|
@@ -980,6 +1025,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
980
1025
|
stride: 1,
|
|
981
1026
|
duration,
|
|
982
1027
|
...(physics === undefined ? {} : { physics: { stepped: physics.stepped.length, fps } }),
|
|
1028
|
+
...(overlapReading === undefined ? {} : { overlaps: overlapsRecord(overlapReading.file, overlapReading.plates.size) }),
|
|
983
1029
|
});
|
|
984
1030
|
const how = frames.length === 1 ? 'a single pose' : `${duration.toFixed(3)}s`;
|
|
985
1031
|
const folding = foldReading?.meshes.filter((m) => m.worst !== null).length ?? 0;
|
|
@@ -988,7 +1034,11 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
988
1034
|
const passNote = passReading === undefined ? '' : ` + ${PASS_THROUGH_FILE} (${passing} of ${passReading.pairs.length} pair(s) pass through)`;
|
|
989
1035
|
const opening = seamReading?.pairs.filter((p) => p.opens.length > 0).length ?? 0;
|
|
990
1036
|
const seamNote = seamReading === undefined ? '' : ` + ${SEAM_FILE} (${opening} of ${seamReading.pairs.length} pair(s) open)`;
|
|
991
|
-
const
|
|
1037
|
+
const overlapNote =
|
|
1038
|
+
overlapReading === undefined
|
|
1039
|
+
? ''
|
|
1040
|
+
: ` + ${OVERLAPS_FILE} (${overlapReading.file.pairs.filter((p) => p.varies).length} moving pair(s), ${overlapReading.plates.size} outline plate(s))`;
|
|
1041
|
+
const extras = `${sheet ? ` + ${SHEET_FILE}` : ''}${geometryOut === undefined ? '' : ` + ${GEOMETRY_FILE}`}${foldNote}${passNote}${seamNote}${overlapNote}`;
|
|
992
1042
|
console.log(` .. ${name.padEnd(16)} ${frames.length} frame(s), ${how}${extras} -> ${dir}`);
|
|
993
1043
|
if (physics !== undefined) console.log(` .. ${physicsLine(physics.stepped, physicsDeclared, fps, frames.length)}`);
|
|
994
1044
|
for (const mesh of foldReading?.meshes ?? []) {
|
|
@@ -1004,6 +1054,11 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
1004
1054
|
for (const frame of pair.frames) console.log(` .. pass ${passFrameLine(passReading?.animation ?? null, pair, frame)}`);
|
|
1005
1055
|
}
|
|
1006
1056
|
for (const pair of seamReading?.pairs ?? []) console.log(` .. seam ${seamLine(seamReading?.animation ?? null, fps, pair)}`);
|
|
1057
|
+
if (overlapReading !== undefined) {
|
|
1058
|
+
const still = staticLine(overlapReading.file.animation, overlapReading.file.pairs);
|
|
1059
|
+
if (still !== null) console.log(` .. overlap ${still}`);
|
|
1060
|
+
for (const pair of overlapReading.file.pairs) if (pair.varies) console.log(` .. overlap ${overlapLine(overlapReading.file.animation, pair)}`);
|
|
1061
|
+
}
|
|
1007
1062
|
for (const reading of angleReadings.get(name) ?? []) {
|
|
1008
1063
|
console.log(` .. angle ${angleRangeLine(name === SETUP_POSE_DIR && facts.animations.length === 0 ? null : name, fps, reading)}`);
|
|
1009
1064
|
}
|
|
@@ -1036,6 +1091,21 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
1036
1091
|
console.log(`rigc: wrote ${join(outRoot, FRAMES_SIDECAR)}`);
|
|
1037
1092
|
}
|
|
1038
1093
|
|
|
1094
|
+
/**
|
|
1095
|
+
* `frames.json`'s record of one set's overlap reading (issue #1480), so a reader that cannot open a plate still has
|
|
1096
|
+
* the figures: the file, how many pairs cover anything, how many of them are static layering, how many plates were
|
|
1097
|
+
* drawn, and every pair that moves with its frame count and its most pixels — the figures its line prints.
|
|
1098
|
+
*/
|
|
1099
|
+
export function overlapsRecord(file: ReturnType<typeof overlapsFileOf>, plates: number): NonNullable<FrameSet['overlaps']> {
|
|
1100
|
+
return {
|
|
1101
|
+
file: OVERLAPS_FILE,
|
|
1102
|
+
pairs: file.pairs.length,
|
|
1103
|
+
static: file.pairs.filter((p) => !p.varies).length,
|
|
1104
|
+
plates,
|
|
1105
|
+
moving: file.pairs.filter((p) => p.varies).map((p) => ({ under: p.under, over: p.over, frames: p.frames, most: p.most })),
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1039
1109
|
/** `render`'s line for a rig that declares no physics constraint (issue #1467): nothing was stepped because nothing could be. */
|
|
1040
1110
|
export const PHYSICS_NONE_LINE = 'physics: none in this rig — it declares no physics constraint, so no frame steps one';
|
|
1041
1111
|
|
|
@@ -2144,7 +2214,9 @@ export function cmdGuide(flags: Record<string, string>, positional: string[]): v
|
|
|
2144
2214
|
}
|
|
2145
2215
|
const answer = guideAnswer(positional[0], { guide: readFileSync(join(PACKAGE_ROOT, GUIDE_FILE), 'utf8'), elsewhere }, { all: flags.all !== undefined });
|
|
2146
2216
|
if (!answer.ok) throw new GuideError(refusalLine(answer.rule, answer.detail));
|
|
2147
|
-
|
|
2217
|
+
// One write of the whole answer, not `console.log`: a §section with `--all` runs past 64 KiB, which Bun 1.2.0's
|
|
2218
|
+
// console corrupts at a chunk boundary and a non-blocking pipe cuts short on every Bun (issue #1487, `writeWhole`).
|
|
2219
|
+
writeWhole(1, answer.lines.map((line) => `${line}\n`).join(''));
|
|
2148
2220
|
}
|
|
2149
2221
|
|
|
2150
2222
|
// ---------------------------------------------------------------------------
|
|
@@ -2272,31 +2344,128 @@ export function emptyArgumentRefusal(pkg: string, raw: readonly string[], keeps:
|
|
|
2272
2344
|
);
|
|
2273
2345
|
}
|
|
2274
2346
|
|
|
2347
|
+
/**
|
|
2348
|
+
* One argument as a POSIX shell reads it back (issue #1486): left bare when it is made only of characters no shell
|
|
2349
|
+
* treats specially, otherwise single-quoted with each `'` spelled `'\''`. The empty argument is `''`. Deterministic
|
|
2350
|
+
* — the same argument always prints the same word — so a guidance line naming the flags a caller gave is one string.
|
|
2351
|
+
*/
|
|
2352
|
+
export function shellWord(arg: string): string {
|
|
2353
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(arg)) return arg;
|
|
2354
|
+
return `'${arg.replaceAll("'", "'\\''")}'`;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
/**
|
|
2358
|
+
* What a delegating command hands its bin, and the build directory it was pointed at (issue #1486). A delegate that
|
|
2359
|
+
* names a `bareTarget` subcommand treats a first argument that is none of its `subcommands` and does not start with
|
|
2360
|
+
* `-` (`--help`, `-h`, `--` and every flag pass through) as a target: `rigc review <dir> …` hands over
|
|
2361
|
+
* `open <dir> …`. Every other command line — no argument at all, an empty first argument, or a subcommand — is handed
|
|
2362
|
+
* over as given, and `target` is `null`.
|
|
2363
|
+
*/
|
|
2364
|
+
export function delegateArguments(delegate: { subcommands?: readonly string[]; bareTarget?: string }, raw: readonly string[]): { argv: string[]; target: string | null } {
|
|
2365
|
+
const first = raw[0];
|
|
2366
|
+
if (delegate.bareTarget === undefined || first === undefined || first === '' || first.startsWith('-') || (delegate.subcommands ?? []).includes(first)) {
|
|
2367
|
+
return { argv: [...raw], target: null };
|
|
2368
|
+
}
|
|
2369
|
+
return { argv: [delegate.bareTarget, ...raw], target: first };
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
/**
|
|
2373
|
+
* The refusal for a delegated package that does not resolve (issue #1486): `REVIEW_NOT_INSTALLED` and two guidance
|
|
2374
|
+
* lines — the command that installs it, and the one that runs it once without installing, carrying `argv` (what the
|
|
2375
|
+
* bin would have been handed, a bare target already spelled `open <dir>`) word for word. With nothing to carry, the
|
|
2376
|
+
* second line names the target it needs as `<output>`. Nothing reads the terminal, so it is the same on a TTY and
|
|
2377
|
+
* off one, and rigc runs neither line: an install and a one-off fetch both change state the caller did not ask for.
|
|
2378
|
+
*/
|
|
2379
|
+
export function notInstalledRefusal(pkg: string, argv: readonly string[]): string {
|
|
2380
|
+
const words = argv.length === 0 ? ['open', '<output>'] : argv.map(shellWord);
|
|
2381
|
+
return (
|
|
2382
|
+
`REVIEW_NOT_INSTALLED: ${pkg} is not installed beside this project or beside rig-c, and rigc does not install it — ` +
|
|
2383
|
+
`install it, or run it once without installing:\n` +
|
|
2384
|
+
`bun add -d ${pkg}\n` +
|
|
2385
|
+
`bunx ${pkg} ${words.join(' ')}`
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
/**
|
|
2390
|
+
* The refusal for a delegated package whose manifest declares a Bun floor the running Bun is under, or `null`
|
|
2391
|
+
* (issue #1486). The floor is read off the resolved package's own `engines.bun`, never written here: a package that
|
|
2392
|
+
* declares none has no floor to hold. Only `>=X.Y.Z` is read; any other range shape is refused by name rather than
|
|
2393
|
+
* guessed at (`REVIEW_BUN_RANGE_UNREAD`). A running version with a pre-release tag is below its own release, as
|
|
2394
|
+
* semver orders them.
|
|
2395
|
+
*/
|
|
2396
|
+
export function bunFloorRefusal(pkg: string, engines: unknown, running: string): string | null {
|
|
2397
|
+
if (engines === undefined) return null;
|
|
2398
|
+
const isObject = typeof engines === 'object' && engines !== null && !Array.isArray(engines);
|
|
2399
|
+
const range = isObject ? (engines as Record<string, unknown>).bun : undefined;
|
|
2400
|
+
if (isObject && range === undefined) return null;
|
|
2401
|
+
const floor = typeof range === 'string' ? /^\s*>=\s*(\d+)\.(\d+)\.(\d+)\s*$/.exec(range) : null;
|
|
2402
|
+
if (floor === null) {
|
|
2403
|
+
const what = isObject ? `engines.bun ${JSON.stringify(range)}` : `engines ${JSON.stringify(engines)}`;
|
|
2404
|
+
return `REVIEW_BUN_RANGE_UNREAD: ${pkg} declares ${what}, and rigc reads only ">=X.Y.Z" — it did not guess whether Bun ${running} satisfies it`;
|
|
2405
|
+
}
|
|
2406
|
+
const found = /^(\d+)\.(\d+)\.(\d+)(-\S*)?/.exec(running);
|
|
2407
|
+
const below = ((): boolean => {
|
|
2408
|
+
if (found === null) return true;
|
|
2409
|
+
for (let i = 1; i <= 3; i++) {
|
|
2410
|
+
const a = Number(found[i]);
|
|
2411
|
+
const b = Number(floor[i]);
|
|
2412
|
+
if (a !== b) return a < b;
|
|
2413
|
+
}
|
|
2414
|
+
return found[4] !== undefined;
|
|
2415
|
+
})();
|
|
2416
|
+
return below ? `REVIEW_BUN_TOO_OLD: Bun ${running}, ${pkg} needs ${range as string}` : null;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2275
2419
|
/**
|
|
2276
2420
|
* A delegating command's body (`CommandDoc.delegate`, issue #1457): resolve the
|
|
2277
2421
|
* package's bin and run it under the Bun running this — the bin is a Bun script
|
|
2278
|
-
* — with
|
|
2279
|
-
*
|
|
2280
|
-
*
|
|
2281
|
-
*
|
|
2422
|
+
* — with stdio inherited, then exit with its code, or die by its signal the way
|
|
2423
|
+
* `bin/rigc.cjs` does, so a caller reads what it would have read running the bin
|
|
2424
|
+
* itself: stdout is the bin's byte for byte, stderr the bin's plus only the
|
|
2425
|
+
* launcher's own refusals, the exit code the bin's.
|
|
2426
|
+
*
|
|
2427
|
+
* Issue #1486 put four things in front of the spawn, in this order, each a
|
|
2428
|
+
* refusal by name with exit 2 and nothing on stdout: a bare target that is not a
|
|
2429
|
+
* directory (`REVIEW_OUTPUT_MISSING <path>`, read before the install because a
|
|
2430
|
+
* guidance line pointing at nothing is no guidance); the package absent
|
|
2431
|
+
* (`REVIEW_NOT_INSTALLED` and two guidance lines — never an install, never a
|
|
2432
|
+
* `bunx`); a Bun below the package's declared floor (`REVIEW_BUN_TOO_OLD`); an
|
|
2433
|
+
* empty argument this Bun would drop (issue #1465). The argument list is the
|
|
2434
|
+
* caller's, a bare target prefixed with `open`, and nothing else: no
|
|
2435
|
+
* `--reference` is derived, because no build rigc writes records the clip it was
|
|
2436
|
+
* retargeted from.
|
|
2282
2437
|
*/
|
|
2283
2438
|
function cmdDelegate(command: string, raw: readonly string[]): void {
|
|
2284
2439
|
const delegate = COMMANDS.find((c) => c.name === command)?.delegate;
|
|
2285
2440
|
if (delegate === undefined) throw new Error(`internal: ${command} names no delegate`);
|
|
2441
|
+
const { argv, target } = delegateArguments(delegate, raw);
|
|
2442
|
+
if (target !== null && !(existsSync(target) && statSync(target).isDirectory())) {
|
|
2443
|
+
throw new DelegateError(
|
|
2444
|
+
`REVIEW_OUTPUT_MISSING ${target}: ${existsSync(target) ? 'it is not a directory' : 'nothing is there'} — \`rigc review <dir>\` opens a build's output directory, ` +
|
|
2445
|
+
`and the first argument is read as one because it is none of ${(delegate.subcommands ?? []).join(', ')} and does not start with "-" (\`rigc review -- <arg>\` hands it over as it is)`,
|
|
2446
|
+
2,
|
|
2447
|
+
);
|
|
2448
|
+
}
|
|
2286
2449
|
const manifest = resolveDelegate(delegate.package);
|
|
2287
|
-
if (manifest === null) throw new DelegateError(
|
|
2288
|
-
const
|
|
2450
|
+
if (manifest === null) throw new DelegateError(notInstalledRefusal(delegate.package, argv), 2);
|
|
2451
|
+
const read = readJsonFile(manifest) as { bin?: unknown; engines?: unknown };
|
|
2452
|
+
const bins = read.bin;
|
|
2289
2453
|
const relativeBin = typeof bins === 'object' && bins !== null ? (bins as Record<string, unknown>)[delegate.bin] : undefined;
|
|
2290
2454
|
const bin = typeof relativeBin === 'string' ? join(dirname(manifest), relativeBin) : null;
|
|
2291
2455
|
if (bin === null || !existsSync(bin)) {
|
|
2292
2456
|
throw new DelegateError(
|
|
2293
|
-
`${delegate.package} at ${dirname(manifest)} ${bin === null ? `declares no "${delegate.bin}" bin` : `declares the "${delegate.bin}" bin ${String(relativeBin)}, which is not there`} —
|
|
2457
|
+
`${delegate.package} at ${dirname(manifest)} ${bin === null ? `declares no "${delegate.bin}" bin` : `declares the "${delegate.bin}" bin ${String(relativeBin)}, which is not there`} — bun add -d ${delegate.package}`,
|
|
2294
2458
|
2,
|
|
2295
2459
|
);
|
|
2296
2460
|
}
|
|
2297
|
-
const
|
|
2461
|
+
const old = bunFloorRefusal(delegate.package, read.engines, process.versions.bun ?? 'unknown');
|
|
2462
|
+
if (old !== null) throw new DelegateError(old, 2);
|
|
2463
|
+
const dropped = emptyArgumentRefusal(delegate.package, argv, spawnKeepsEmptyArguments, process.versions.bun ?? 'unknown');
|
|
2298
2464
|
if (dropped !== null) throw new DelegateError(dropped, 2);
|
|
2299
|
-
|
|
2465
|
+
// 🔒 The `--` is Bun's, not the bin's: Bun 1.2.0 and 1.4.2 alike drop one `--` standing right after the script, so
|
|
2466
|
+
// `rigc review -- <dir>` reached the bin as `<dir>` (measured, issue #1486). One given here is the one dropped, and
|
|
2467
|
+
// the arguments after it arrive as they were.
|
|
2468
|
+
const ran = spawnSync(process.execPath, [bin, '--', ...argv], { stdio: 'inherit' });
|
|
2300
2469
|
// ⚠️ The exit and the signal are read before `error`: Bun 1.2.0 — the declared minimum — sets `error` on every
|
|
2301
2470
|
// child that exits non-zero, so reading it first refused review's own exit 7 as "could not run". `error` is a
|
|
2302
2471
|
// launch that failed only where the child left neither a status nor a signal.
|