@rig-c/parts 1.14.0 → 1.16.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 -0
- package/cli.ts +52 -14
- package/docs/AUTHORING.md +288 -208
- package/docs/MIGRATION.md +62 -0
- package/docs/STABILITY.md +7 -5
- package/package.json +1 -1
- package/skills/rig-parts/SKILL.md +8 -1
- package/src/build.ts +14 -12
- package/src/check.ts +100 -24
- package/src/contour.ts +41 -5
- package/src/errors.ts +16 -0
- package/src/guide.ts +255 -0
- package/src/meshdraft.ts +28 -6
- package/src/requirements.ts +6 -5
- package/src/rig.ts +1 -1
- package/src/scene.ts +2 -2
package/docs/MIGRATION.md
CHANGED
|
@@ -148,3 +148,65 @@ nothing else.
|
|
|
148
148
|
What to do: install with `npm i -D @rig-c/parts` (or `npm install -g @rig-c/parts`);
|
|
149
149
|
command lines do not change. A dependant that names `rig-parts` keeps receiving
|
|
150
150
|
every release under it (#222).
|
|
151
|
+
|
|
152
|
+
## After 1.14.0: the idle is read for folds
|
|
153
|
+
|
|
154
|
+
| | 1.14.0 | after 1.14.0 |
|
|
155
|
+
| --- | --- | --- |
|
|
156
|
+
| `check.json`'s `ANIMATIONS_DO_NOT_FOLD` | written only when `motion.json` holds an animation beside the idle; its `animations` lists those | written whenever `motion.json` holds an animation, a rig of the idle alone included; its `animations` lists every one read, the idle first (#333) |
|
|
157
|
+
| the console's fold line | `N animation(s) beside the idle read by rigc render --folds …` | `N animation(s) read by rigc render --folds …`, the idle counted and named |
|
|
158
|
+
| the summary of a rig with an idle and no other animation | `… of 9 bar(s) measured …` | `… of 10 bar(s) measured …` |
|
|
159
|
+
| `may_fold` naming `"animation": "idle"` | refused, `REQUIREMENTS_MAY_FOLD` (the bar did not read the idle) | accepted like any animation name; a fold it names is listed with its why and not held, and one that does not fold fails the bar |
|
|
160
|
+
|
|
161
|
+
The reading is the one #320 made for the other animations — one `rigc render --folds` at
|
|
162
|
+
12 fps over the build with its physics constraints left out — and now takes the idle in
|
|
163
|
+
the same run. A rig whose idle turns a mesh triangle over against the setup pose, which
|
|
164
|
+
`check` passed before, fails it, naming the idle and quoting rigc's line. On the four
|
|
165
|
+
public examples and the trailer no idle folds [measured for #333, rig-c 2.47.1], so no
|
|
166
|
+
verdict moved; every example's `expected/check.json` moved by the key and nothing else.
|
|
167
|
+
`reference-demo`, an idle alone, gains the whole key after `TEXTURE_STRETCH`:
|
|
168
|
+
|
|
169
|
+
```diff
|
|
170
|
+
@@ -259,6 +259,18 @@
|
|
171
|
+
"severity": 1.074
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
+ },
|
|
175
|
+
+ "ANIMATIONS_DO_NOT_FOLD": {
|
|
176
|
+
+ "status": "PASS",
|
|
177
|
+
+ "animations": [
|
|
178
|
+
+ "idle"
|
|
179
|
+
+ ],
|
|
180
|
+
+ "fps": 12,
|
|
181
|
+
+ "physics_left_out": 0,
|
|
182
|
+
+ "folding": [],
|
|
183
|
+
+ "folding_undeclared": 0,
|
|
184
|
+
+ "declared_not_folding": [],
|
|
185
|
+
+ "bar": 0
|
|
186
|
+
},
|
|
187
|
+
"RECOMPOSITE_HOLES": {
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
and `demo`, `scarf` and `reference-sample` gain `"idle"` as the first entry of its
|
|
191
|
+
`animations`.
|
|
192
|
+
|
|
193
|
+
What to do: nothing for a rig whose idle does not fold. A consumer that reads
|
|
194
|
+
`check.json` finds the key on every rig with an animation. An idle that folds on purpose
|
|
195
|
+
is declared, `"may_fold": [{ "slot": …, "animation": "idle", "why": … }]`, in the
|
|
196
|
+
`--requirements` file.
|
|
197
|
+
|
|
198
|
+
## After 1.14.0: faint islands under `contour.bridges`
|
|
199
|
+
|
|
200
|
+
`contour.bridges` shipped in 1.14.0 meshing every island past `stray` and bridging it.
|
|
201
|
+
The patch after it reads islands in a fixed order (issue #326, HQ's ruling): dust first
|
|
202
|
+
(`stray`), then **faint** islands — every pixel's alpha under 64 — left out and named,
|
|
203
|
+
then every visible island bridged whatever its bridge costs. No field was added and
|
|
204
|
+
no declared form changed.
|
|
205
|
+
|
|
206
|
+
| change | what moved | measured |
|
|
207
|
+
| --- | --- | --- |
|
|
208
|
+
| #326, faint islands left out | only a part that declares `contour.bridges` and holds a faint island (not the largest): its mesh, its `mesh_report.json` row (`contour.bridges.faint`, and `art_coverage`, which counts those pixels as art not covered) and, from `mesh-draft`, its written entry's note and `mesh_draft.json`'s `candidate.bridges.faint`. A part without a faint island writes the bytes 1.14.0 wrote | demo `earring` (the only such part a `mesh-draft` of the public examples drafts): its 8, 2 and 1 px specks, brightest alpha 17, 17 and 11, are left out — 33 → 26 triangles, overdraw 4.82 → 1.94; the idle rendered at scale 1.01 differs from the build of #335 (the change 1.14.0 carries) by at most 9 of 255 levels on at most 38 px where the specks were (4 frames; elsewhere 158 px differ by 1 level, as they do with the earring slot hidden), and `check`'s seam bar reads 0 px over 40 on both |
|
|
209
|
+
|
|
210
|
+
What to do: nothing for a config without `contour.bridges`. A part that declares it and
|
|
211
|
+
holds a faint island no longer draws that island; if those pixels matter, keep the
|
|
212
|
+
lattice for that part.
|
package/docs/STABILITY.md
CHANGED
|
@@ -35,8 +35,9 @@ Exit codes are part of the interface: **0** the command did what it says, **1**
|
|
|
35
35
|
refused its input (every reason a `FAIL` line), **2** a usage error or a command
|
|
36
36
|
this version does not implement. No command is unimplemented at 1.0.0 (`LATER`
|
|
37
37
|
in `cli.ts` is empty). A refusal line has the shape
|
|
38
|
-
` FAIL <RULE>: <object> — <value found>; <value required
|
|
39
|
-
stable identifier,
|
|
38
|
+
` FAIL <RULE>: <object> — <value found>; <value required> (see rig-parts guide <RULE>)`:
|
|
39
|
+
the rule name is a stable identifier, the pointer at the end is fixed text, and the
|
|
40
|
+
detail between them is prose that may be reworded. A command
|
|
40
41
|
also answers to the alias `spine-parts` as long as the install keeps it
|
|
41
42
|
([MIGRATION.md](MIGRATION.md)). `-h`, `help` and `-v` are the short forms of
|
|
42
43
|
`--help` and `--version`. The flags' meanings are `rig-parts --help`'s; their
|
|
@@ -60,6 +61,7 @@ defaults are the help's too, and none is guessed from the art.
|
|
|
60
61
|
| `build` | `--config`, `--source`, `--full`, `--head`, `--out`; optional `--seam`, `--project`, `--page-edges`, `--pack-shape`, `--loop`, `--lead-in`, `--requirements`, `--idle-keys`, `--seam-pairs`, `--keep-stills` |
|
|
61
62
|
| `compose` | `--scene`, `--out`; optional `--requirements`, `--accept` |
|
|
62
63
|
| `mesh-draft` | `--config`, `--parts`, `--out`; optional `--requirements`, `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon), `--more-triangles` |
|
|
64
|
+
| `guide` | one positional argument: a rule name, `§<n>`, `§<n>.<m>` or `§<n>.<m>.<k>` |
|
|
63
65
|
| (none) | `--version`, `--help` |
|
|
64
66
|
<!-- /stable:commands -->
|
|
65
67
|
|
|
@@ -171,11 +173,11 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
|
|
|
171
173
|
| --- | --- | --- |
|
|
172
174
|
| `parts.json`, `parts/<name>.png` | `assemble`, `build` | one record per part: `from` (`<run>:<tag>` with a See-through v3 tag, `<run>:author:<label>` for a layer the author made — issue #256, read by no role rule — or `painting:<name>` for a patch), `cut_from` on a cut's piece only (the plan part it was cut from, issue #230), the box `x`, `y`, `w`, `h` (rig px), the pixel counts `src/parts.ts` documents (`opaque_px`, `visible_px`, `occluded_px`, `projected_core_px`, `source_px_taken`, `visible_not_projected_px`, `refused_drift_px`, `merged_px`, `seam_override_px`, and `fringe_pushed_back` when not empty); `rig_size`, `scale_rig_per_source`, `ghost_px`, `recomposite`, and `scene` (`figure_alpha_above`, `figure_px`, `figure_mask`) under `config.scene` only, with the figure's mask `figure_rig.png` beside the recomposite (issue #242) |
|
|
173
175
|
| `rig/rig.json`, `rig/motion.json`, `rig/images/` | `rig`, `build`, `compose` | rig-c's rig spec and motion spec, `spec` `rigc-rig/1` and `rigc-motion/1`: the format is rig-c's, and this package writes only what rig-c's parser accepts |
|
|
174
|
-
| `rig/mesh_report.json` | `rig`, `build` | one row per mesh: `part`, `vertices`, `triangles`, `hull`, `bones`, `max_influences`, `mean_influences`, `art_coverage`, then `grid` (a lattice); `mode` "contour" with `params`, `contour`, `regions` (`params.maxEdge` and `contour.outlineCut` only when the mesh declares `contour.maxEdge`; `params.bridges` and `contour.bridges` — `islands`, `units`, `joins` — only when it declares `contour.bridges`); or `mode` "auto" with `settings`, `source`, `result`, `residuals`, `worst_residual`, `worst_region`, `motion_amplitude`, `termination`, `weights`, `regions`, `deformation`, `quality_report` (rig-c's `mesh-quality-report/1` document whole), `motion_report`, and, only when they apply, `stray_cleared` (after `source`, when `source.stray` is declared), `skinning_residual` and `replay` (`AutoMeshReport` in `src/rig.ts`); in every mode, `exponent` when the mesh declares one (after `grid`, or after `mode`); `influences`, as declared, when a lattice or contour mesh declares it (after `exponent`, or where `exponent` would be); `stray` and `stray_cleared` when a lattice mesh declares `stray` (after `influences`, or where `influences` would be); `ribs` when a contour or auto mesh declares them (after `exponent`, or after `mode`), and then `contour.ribs` (`source.contour.ribs` in the automatic mode) |
|
|
175
|
-
| `check/check.json` and the rest of `check/` | `check`, `build`, `compose` | the reference's fields in its order (`gate_spine_html_green`, `rigc_entry`, `pack_mode`, `loop_max_diff`, the seam figures — `seam_mean`, `seam_px_over_40`, `seam_px_over_80` and, from issue #216, `seam_frame_scale`, the frame scale the bar divides the count by), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `seam_border` after the seam figures when `parts.json` declares a scene (issue #242), `seam_pairs` after the seam figures when the seam bar fails or `--seam-pairs` asks (each row's picture under `check/seam/`; `--keep-stills` leaves the setup-pose still and its `frames.json` under `check/setup_still/`), then the judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`), `ANIMATIONS_DO_NOT_FOLD` after them when `motion.json` holds an animation
|
|
176
|
+
| `rig/mesh_report.json` | `rig`, `build` | one row per mesh: `part`, `vertices`, `triangles`, `hull`, `bones`, `max_influences`, `mean_influences`, `art_coverage`, then `grid` (a lattice); `mode` "contour" with `params`, `contour`, `regions` (`params.maxEdge` and `contour.outlineCut` only when the mesh declares `contour.maxEdge`; `params.bridges` and `contour.bridges` — `islands`, `units`, `joins`, and `faint` only when an island was left out as faint — only when it declares `contour.bridges`); or `mode` "auto" with `settings`, `source`, `result`, `residuals`, `worst_residual`, `worst_region`, `motion_amplitude`, `termination`, `weights`, `regions`, `deformation`, `quality_report` (rig-c's `mesh-quality-report/1` document whole), `motion_report`, and, only when they apply, `stray_cleared` (after `source`, when `source.stray` is declared), `skinning_residual` and `replay` (`AutoMeshReport` in `src/rig.ts`); in every mode, `exponent` when the mesh declares one (after `grid`, or after `mode`); `influences`, as declared, when a lattice or contour mesh declares it (after `exponent`, or where `exponent` would be); `stray` and `stray_cleared` when a lattice mesh declares `stray` (after `influences`, or where `influences` would be); `ribs` when a contour or auto mesh declares them (after `exponent`, or after `mode`), and then `contour.ribs` (`source.contour.ribs` in the automatic mode) |
|
|
177
|
+
| `check/check.json` and the rest of `check/` | `check`, `build`, `compose` | the reference's fields in its order (`gate_spine_html_green`, `rigc_entry`, `pack_mode`, `loop_max_diff`, the seam figures — `seam_mean`, `seam_px_over_40`, `seam_px_over_80` and, from issue #216, `seam_frame_scale`, the frame scale the bar divides the count by), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `seam_border` after the seam figures when `parts.json` declares a scene (issue #242), `seam_pairs` after the seam figures when the seam bar fails or `--seam-pairs` asks (each row's picture under `check/seam/`; `--keep-stills` leaves the setup-pose still and its `frames.json` under `check/setup_still/`), then the judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`), `ANIMATIONS_DO_NOT_FOLD` after them when `motion.json` holds an animation, the idle included — on every rig with an idle since issue #333 (issue #320: `status`, `animations`, `fps`, `physics_left_out`, `folding`, `folding_undeclared`, `declared_not_folding`, `bar`), `RECOMPOSITE_HOLES` (its first figure `declared_scene` when `parts.json` declares a scene), `SETUP_POSE_VS_SOURCE`, `skipped`, a `requirements` block under `--requirements`, and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes |
|
|
176
178
|
| `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections, `turn` with `--full`/`--head`, and `notes` |
|
|
177
179
|
| `basis.json` | `propose` | spec `spine-parts-basis/1` |
|
|
178
|
-
| `config.json`, `mesh_draft.json` | `mesh-draft` | `config.json` is the config read, byte for byte, but for each drafted part's `meshes` entry; `mesh_draft.json` is spec `rig-parts-mesh-draft/1`: `rules`, `lattice_check`, `draft_check`, `figure_overdraw`, and per lattice part `part`, `mode`, `lattice`, `candidate` (with `bridges` — `units`, `joins` of `span` and `area_px2`, `area_px2`, `overdraw` — only when the candidate is a bridged one, issue #326), `delta` (under `--more-triangles` only), `readings`, `gate` (each line's `reading`, `pass`, `text`, `margin`), `note`; and `not_compared` (issue #307), only when the config holds an `auto` or `contour` entry: per such entry, in config order, `part`, `mode` and `note` — the part was neither drafted nor compared, and a config of lattices alone writes no such key |
|
|
180
|
+
| `config.json`, `mesh_draft.json` | `mesh-draft` | `config.json` is the config read, byte for byte, but for each drafted part's `meshes` entry; `mesh_draft.json` is spec `rig-parts-mesh-draft/1`: `rules`, `lattice_check`, `draft_check`, `figure_overdraw`, and per lattice part `part`, `mode`, `lattice`, `candidate` (with `bridges` — `units`, `joins` of `span` and `area_px2`, `area_px2`, `overdraw`, and `faint` only when an island was left out as faint — only when the candidate is a bridged one, issue #326), `delta` (under `--more-triangles` only), `readings`, `gate` (each line's `reading`, `pass`, `text`, `margin`), `note`; and `not_compared` (issue #307), only when the config holds an `auto` or `contour` entry: per such entry, in config order, `part`, `mode` and `note` — the part was neither drafted nor compared, and a config of lattices alone writes no such key |
|
|
179
181
|
| the scene report | `compose` | spec `spine-parts-scene-report/1` |
|
|
180
182
|
| `idle.png`, `idle-indexed.png`, `idle.gif` | `loop`, `build --loop` | APNG and GIF encodings of the rendered idle |
|
|
181
183
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/parts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "AI-authored 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through rig-c before anything is written. A CLI for agents that cannot see the image.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,13 @@ rig-c, which compiles, gates, packs and renders. You cannot see the painting,
|
|
|
13
13
|
the layers or the rig. The printed lines, `docs/AUTHORING.md` and the files each
|
|
14
14
|
stage writes are the whole interface.
|
|
15
15
|
|
|
16
|
+
**A FAIL line ends with the command that prints its row of the guide:**
|
|
17
|
+
`FAIL RULE: object — detail (see rig-parts guide RULE)`. Run that command before
|
|
18
|
+
anything else. It prints the one row of `docs/AUTHORING.md` that maps the rule to
|
|
19
|
+
what has to change, read from the installed file. `rig-parts guide §<n>` prints a
|
|
20
|
+
section, `§<n>.<m>` the m-th `###` under it and `§<n>.<m>.<k>` the k-th `####`
|
|
21
|
+
under that.
|
|
22
|
+
|
|
16
23
|
## Non-negotiables
|
|
17
24
|
|
|
18
25
|
- **rig-c's gate is never bypassed.** rig-parts writes no Spine data
|
|
@@ -26,7 +33,7 @@ stage writes are the whole interface.
|
|
|
26
33
|
because the tool refused.
|
|
27
34
|
- **The refusal is the instruction.** `FAIL RULE: object — detail` names the object,
|
|
28
35
|
the value found and the value required; AUTHORING §6 maps every rule to the field
|
|
29
|
-
or input that has to change.
|
|
36
|
+
or input that has to change, and `rig-parts guide RULE` prints that row.
|
|
30
37
|
- **Green is not "looks right".** `check` measures the gates, the seam, the loop and
|
|
31
38
|
six judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`,
|
|
32
39
|
`TIP_OVER_ROOT`, `STILL_REGIONS_DARK` — its face half in the head's own frame —
|
package/src/build.ts
CHANGED
|
@@ -70,9 +70,9 @@ import {
|
|
|
70
70
|
splitSchedule,
|
|
71
71
|
untestedIntervals,
|
|
72
72
|
} from './autoreplay.ts';
|
|
73
|
-
import { besideIdleLine, FOLD_LINE, type FoldsLine, buildGateLines, causeLines, IDLE_FPS, IDLE_MAX_PX, maxRgbDiff, REQUIREMENTS_DIR, type CheckReport, type PackLine, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, JUDGEMENT_LINES, type JudgementLine, packedBuildArgs, packedBuildLabel, type PackMode, type PackShape, type PageEdges, readFrameSet, rigcFailed, REPORTED_LINES, type ReportedLine, type RigcRunner, runCheck, SEAM_DIR, SEAM_MEAN_BAR, seamPairText, seamRigPxText, SOURCE_LINE, SEAM_PX_BAR, SEAM_PX_LEVEL, SEAM_PX_LEVEL_HIGH, SPINEBOY_YARDSTICK, STILL_DIR } from './check.ts';
|
|
73
|
+
import { besideIdleLine, FOLD_LINE, type FoldsLine, buildGateLines, causeLines, IDLE_FPS, IDLE_MAX_PX, maxRgbDiff, REQUIREMENTS_DIR, type CheckReport, type PackLine, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, JUDGEMENT_LINES, type JudgementLine, packedBuildArgs, packedBuildLabel, type PackMode, type PackShape, type PageEdges, readFrameSet, rigcFailed, rigcIdentity, rigcRanClause, REPORTED_LINES, type ReportedLine, type RigcRunner, runCheck, SEAM_DIR, SEAM_MEAN_BAR, seamPairText, seamRigPxText, SOURCE_LINE, SEAM_PX_BAR, SEAM_PX_LEVEL, SEAM_PX_LEVEL_HIGH, SPINEBOY_YARDSTICK, STILL_DIR } from './check.ts';
|
|
74
74
|
import { type Cut, loadConfig, loadConfigAndAnimations, loadEarlyConfig } from './config.ts';
|
|
75
|
-
import { PartsError, type Problem,
|
|
75
|
+
import { failLine, PartsError, type Problem, refuseIfAny } from './errors.ts';
|
|
76
76
|
import type { MeshQualityReport, MotionSchedule } from 'rig-c/mesh';
|
|
77
77
|
import { encodeGif } from './gif.ts';
|
|
78
78
|
import { CONTROL_SUFFIX, withAnimations } from './motion.ts';
|
|
@@ -277,6 +277,8 @@ export interface GateRun {
|
|
|
277
277
|
* {@link causeLines} says name why.
|
|
278
278
|
*/
|
|
279
279
|
lines: string[];
|
|
280
|
+
/** Which rigc ran it, when the runner said ({@link RigcCall.ran}; issue #337). */
|
|
281
|
+
ran?: string;
|
|
280
282
|
}
|
|
281
283
|
|
|
282
284
|
/**
|
|
@@ -295,7 +297,7 @@ function gateRun(label: string, rigc: RigcRunner, args: string[]): GateRun {
|
|
|
295
297
|
const r = rigc(args);
|
|
296
298
|
const lines = r.out.split('\n').filter((l) => /^ {2}FAIL {2}/.test(l) || /assertions: \d+ measured/.test(l) || /^rigc compile error/.test(l) || DECLARED_SKIP.test(l) || HERE_LINE.test(l));
|
|
297
299
|
const failed = lines.some((l) => /^ {2}FAIL {2}/.test(l) || /^rigc compile error/.test(l));
|
|
298
|
-
return { label, status: r.status, lines: r.status !== 0 && !failed ? [...lines, ...causeLines(r.out)] : lines };
|
|
300
|
+
return { label, status: r.status, lines: r.status !== 0 && !failed ? [...lines, ...causeLines(r.out)] : lines, ...(r.ran === undefined ? {} : { ran: r.ran }) };
|
|
299
301
|
}
|
|
300
302
|
|
|
301
303
|
/**
|
|
@@ -490,7 +492,7 @@ export function motionGates(
|
|
|
490
492
|
{
|
|
491
493
|
code: 'AUTO_MESH_MOTION_INPUT',
|
|
492
494
|
object: c.object,
|
|
493
|
-
detail: `the candidate with the declared stimulus (the rig as written, its motion carrying the stimulus animations) ${candidate.status !== 0 ? `is refused by rigc's gate, exit ${candidate.status}${candidate.lines.length > 0 ? `: ${candidate.lines.map((l) => l.trim()).join(' | ')}` : ''}` : `built green and wrote no ${RIGC_MODEL_DOCUMENT}`}; a candidate that is not gated is not evidence, and nothing is built in the part's place`,
|
|
495
|
+
detail: `the candidate with the declared stimulus (the rig as written, its motion carrying the stimulus animations) ${candidate.status !== 0 ? `is refused by rigc's gate, exit ${candidate.status}${candidate.lines.length > 0 ? `: ${candidate.lines.map((l) => l.trim()).join(' | ')}` : ''}${rigcRanClause(candidate)}` : `built green and wrote no ${RIGC_MODEL_DOCUMENT}`}; a candidate that is not gated is not evidence, and nothing is built in the part's place`,
|
|
494
496
|
},
|
|
495
497
|
],
|
|
496
498
|
notCompared: 'the stimulus candidate build is red',
|
|
@@ -520,7 +522,7 @@ export function motionGates(
|
|
|
520
522
|
{
|
|
521
523
|
code: 'AUTO_MESH_MOTION_INPUT',
|
|
522
524
|
object: c.object,
|
|
523
|
-
detail: `the reference (the rig with "${c.part}" as its unreduced source mesh) ${gate.status !== 0 ? `is refused by rigc's gate, exit ${gate.status}${gate.lines.length > 0 ? `: ${gate.lines.map((l) => l.trim()).join(' | ')}` : ''}` : `built green and wrote no ${RIGC_MODEL_DOCUMENT}`}; a reference that is not gated is not evidence, and nothing is built in the part's place`,
|
|
525
|
+
detail: `the reference (the rig with "${c.part}" as its unreduced source mesh) ${gate.status !== 0 ? `is refused by rigc's gate, exit ${gate.status}${gate.lines.length > 0 ? `: ${gate.lines.map((l) => l.trim()).join(' | ')}` : ''}${rigcRanClause(gate)}` : `built green and wrote no ${RIGC_MODEL_DOCUMENT}`}; a reference that is not gated is not evidence, and nothing is built in the part's place`,
|
|
524
526
|
},
|
|
525
527
|
],
|
|
526
528
|
notCompared: 'the reference build is red',
|
|
@@ -642,7 +644,7 @@ export function replaySearch(
|
|
|
642
644
|
fatal = {
|
|
643
645
|
code: 'AUTO_MESH_MOTION_INPUT',
|
|
644
646
|
object: c.object,
|
|
645
|
-
detail: `the replay to accepted step ${step} (the rig with "${c.part}" as that step's mesh) ${gate.status !== 0 ? `is refused by rigc's gate, exit ${gate.status}${gate.lines.length > 0 ? `: ${gate.lines.map((l) => l.trim()).join(' | ')}` : ''}` : `built green and wrote no ${RIGC_MODEL_DOCUMENT}`}; a candidate that is not gated is not evidence, and nothing is built in the part's place`,
|
|
647
|
+
detail: `the replay to accepted step ${step} (the rig with "${c.part}" as that step's mesh) ${gate.status !== 0 ? `is refused by rigc's gate, exit ${gate.status}${gate.lines.length > 0 ? `: ${gate.lines.map((l) => l.trim()).join(' | ')}` : ''}${rigcRanClause(gate)}` : `built green and wrote no ${RIGC_MODEL_DOCUMENT}`}; a candidate that is not gated is not evidence, and nothing is built in the part's place`,
|
|
646
648
|
};
|
|
647
649
|
return refused(`rigc exit ${gate.status}`);
|
|
648
650
|
}
|
|
@@ -897,7 +899,7 @@ export function rigStage(input: RigStageInput, rigc: RigcRunner, scratch: string
|
|
|
897
899
|
const problems: Problem[] = red.map((g) => ({
|
|
898
900
|
code: 'RIG_RIGC_GREEN',
|
|
899
901
|
object: `rigc ${g.label}`,
|
|
900
|
-
detail: `exited ${g.status}${g.lines.length > 0 ? `: ${g.lines.map((l) => l.trim()).join(' | ')}` : ', and it printed nothing'}; ${ctlRemedies(g.lines, rig.controls, input.command ?? 'rig').map((s) => `${s}; `).join('')}exit 0 is required before anything is written, and nothing was`,
|
|
902
|
+
detail: `exited ${g.status}${g.lines.length > 0 ? `: ${g.lines.map((l) => l.trim()).join(' | ')}` : ', and it printed nothing'}${rigcRanClause(g)}; ${ctlRemedies(g.lines, rig.controls, input.command ?? 'rig').map((s) => `${s}; `).join('')}exit 0 is required before anything is written, and nothing was`,
|
|
901
903
|
}));
|
|
902
904
|
throw new PartsError(problems);
|
|
903
905
|
}
|
|
@@ -1031,14 +1033,14 @@ export function judgementLine(name: string, line: JudgementLine | ReportedLine):
|
|
|
1031
1033
|
}
|
|
1032
1034
|
|
|
1033
1035
|
/**
|
|
1034
|
-
* The fold line (
|
|
1036
|
+
* The fold line (issues #320, #333) as the console prints it: the status, what was
|
|
1035
1037
|
* read and at what rate, and then each folding mesh on a line of its own,
|
|
1036
1038
|
* rigc's own words, with the declaration's why where `may_fold` takes it.
|
|
1037
1039
|
*/
|
|
1038
1040
|
export function foldsLines(line: FoldsLine): string[] {
|
|
1039
1041
|
const physics = line.physics_left_out === 0 ? '' : `, its ${line.physics_left_out} physics constraint(s) left out`;
|
|
1040
1042
|
const head =
|
|
1041
|
-
`${FOLD_LINE}: ${line.status} — ${line.animations.length} animation(s)
|
|
1043
|
+
`${FOLD_LINE}: ${line.status} — ${line.animations.length} animation(s) read by rigc render --folds at ${line.fps} fps${physics} (${line.animations.join(', ')}): ` +
|
|
1042
1044
|
`${line.folding.length} folding mesh(es), ${line.folding_undeclared} not declared in may_fold (<= ${line.bar})` +
|
|
1043
1045
|
(line.declared_not_folding.length === 0 ? '' : `; may_fold declares ${line.declared_not_folding.map((d) => `slot "${d.slot}" in "${d.animation}"`).join(', ')}, which do(es) not fold`);
|
|
1044
1046
|
return [head, ...line.folding.map((r) => ` folds ${r.line}${r.declared === null ? '' : ` — declared in may_fold: ${r.declared}`}`)];
|
|
@@ -1060,7 +1062,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1060
1062
|
log(`rig-parts check: ${input.rig} -> ${input.out}`);
|
|
1061
1063
|
const versionLines = v.out.trim().split('\n');
|
|
1062
1064
|
const entryLine = versionLines.find((l) => l.startsWith('entry:'));
|
|
1063
|
-
log(`
|
|
1065
|
+
log(` ${rigcIdentity(bin, v)}${entryLine === undefined ? '' : `; ${entryLine}`}`);
|
|
1064
1066
|
const mode: PackMode = { pageEdges: input.pageEdges ?? DEFAULT_PAGE_EDGES, packShape: input.packShape ?? DEFAULT_PACK_SHAPE };
|
|
1065
1067
|
const r = runCheck(input.rig, input.out, rigc, input.parts, mode, input.source, input.requirements, input.seamPairs === true, input.keepStills === true);
|
|
1066
1068
|
log(` gate spine-html (rigc ${packedBuildLabel(mode)}), verbatim:`);
|
|
@@ -1093,7 +1095,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1093
1095
|
}
|
|
1094
1096
|
log(` gate: spine-html ${fig.gate_spine_html_green ? 'green' : 'RED'} (${fig.rigc_entry.entry}${fig.rigc_entry.spine_core === null ? ', rigc\'s own validator' : `, the spine-core ${fig.rigc_entry.spine_core} round trip`})`);
|
|
1095
1097
|
log(` wrote ${r.written.map((w) => join(input.out, w)).join(', ')}, ${join(input.out, 'build')}/${r.idle === null ? '' : `, ${join(input.out, 'idle_frames')}/`}${r.requirements === null ? '' : `, ${join(input.out, REQUIREMENTS_DIR)}/`}${fig.seam_pairs === undefined ? '' : `, ${join(input.out, SEAM_DIR)}/`}${existsSync(join(input.out, STILL_DIR)) ? `, ${join(input.out, STILL_DIR)}/` : ''}`);
|
|
1096
|
-
for (const p of r.problems) log(
|
|
1098
|
+
for (const p of r.problems) log(failLine(p));
|
|
1097
1099
|
const reqNotPass = r.requirements === null ? 0 : r.requirements.summary.declared - r.requirements.summary.pass;
|
|
1098
1100
|
const notMet = r.requirements === null ? `${r.problems.length} bar(s) not met` : `${r.problems.length - reqNotPass} bar(s) not met, ${reqNotPass} declared requirement(s) not PASS`;
|
|
1099
1101
|
log(`${fig.PASS ? 'check: PASS' : `check: FAIL — ${notMet}`}; ${barsSummary(r)}`);
|
|
@@ -1464,7 +1466,7 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
|
|
|
1464
1466
|
const refused = (stage: BuildStage, err: unknown, check: CheckReport | null = null): BuildResult => {
|
|
1465
1467
|
if (!(err instanceof PartsError)) throw err;
|
|
1466
1468
|
const say = prefixed(stage);
|
|
1467
|
-
for (const p of err.problems) say(
|
|
1469
|
+
for (const p of err.problems) say(failLine(p));
|
|
1468
1470
|
say(`refused: ${err.problems.length} problem(s)`);
|
|
1469
1471
|
log(`build: stopped at ${stage}; no later stage ran`);
|
|
1470
1472
|
return { stoppedAt: stage, check, artifact: [] };
|
package/src/check.ts
CHANGED
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
* uncovered holes `assemble` recorded in `parts.json`'s `recomposite` block,
|
|
57
57
|
* copied into `check.json` because the seam cannot see them — it compares the
|
|
58
58
|
* setup pose with the flat stack of parts, and a pixel no part holds is
|
|
59
|
-
* missing from both. When `motion.json` holds an animation
|
|
60
|
-
*
|
|
61
|
-
*
|
|
59
|
+
* missing from both. When `motion.json` holds an animation, one more bar,
|
|
60
|
+
* `ANIMATIONS_DO_NOT_FOLD` (issues #320, #333): rigc's own `render --folds`
|
|
61
|
+
* over every animation, the idle included, physics constraints left out
|
|
62
62
|
* ({@link FOLD_LINE}). The part-alone renders, the shut-eye still, the keyed
|
|
63
63
|
* build and the fold render are scratch (`_isolated/`, `_still/`, `_loop/`,
|
|
64
64
|
* `_folds/`), removed before the stage returns.
|
|
@@ -317,10 +317,58 @@ export interface RigcCall {
|
|
|
317
317
|
status: number;
|
|
318
318
|
/** stdout then stderr. */
|
|
319
319
|
out: string;
|
|
320
|
+
/**
|
|
321
|
+
* Which rigc ran the call, as {@link rigcIdentity} words it (`rigc <version> at <path>`), from a runner that knows
|
|
322
|
+
* its binary ({@link rigcRunnerAt}): set on every call that exited non-zero and on every `--version` call, so a
|
|
323
|
+
* refusal quoting the call names the binary and its version (issue #337). Absent from a runner that does not know.
|
|
324
|
+
*/
|
|
325
|
+
ran?: string;
|
|
320
326
|
}
|
|
321
327
|
|
|
322
328
|
export type RigcRunner = (args: readonly string[]) => RigcCall;
|
|
323
329
|
|
|
330
|
+
/** The process primitive {@link rigcRunnerAt} is given: `src/` spawns nothing, so the caller's spawn is injected. */
|
|
331
|
+
export type RigcSpawn = (bin: string, args: readonly string[]) => RigcCall;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* A rigc binary as what it says it is: `rigc <version> at <path>`, the version being the first line `--version`
|
|
335
|
+
* printed — the line `check` prints before it builds (`checkStage`). A `--version` run that exited non-zero or
|
|
336
|
+
* printed nothing is said so, with the path, rather than read as a version.
|
|
337
|
+
*/
|
|
338
|
+
export function rigcIdentity(bin: string, version: RigcCall): string {
|
|
339
|
+
const first = version.out.trim().split('\n')[0] ?? '';
|
|
340
|
+
if (version.status === 0 && first !== '') return `rigc ${first} at ${bin}`;
|
|
341
|
+
return `rigc (\`--version\` exited ${version.status}${first === '' ? ' and printed nothing' : `, printing ${JSON.stringify(first)}`}) at ${bin}`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** The clause a refusal quoting a rigc call ends with: which rigc ran it ({@link RigcCall.ran}), or nothing when the runner did not say. */
|
|
345
|
+
export function rigcRanClause(call: { ran?: string }): string {
|
|
346
|
+
return call.ran === undefined ? '' : `; ${call.ran} ran it`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* The runner every stage is given (issue #337): `bin` spawned through `spawn`, each call that exited non-zero, and
|
|
351
|
+
* each `--version` call, carrying {@link RigcCall.ran}. The identity is read once per runner, off its first
|
|
352
|
+
* `--version` call or, when a call fails before any, off one `--version` run then — so a green run spawns nothing
|
|
353
|
+
* extra, and a failure names the binary and version that produced it rather than leaving the reader to guess
|
|
354
|
+
* which rigc a path resolved to.
|
|
355
|
+
*/
|
|
356
|
+
export function rigcRunnerAt(bin: string, spawn: RigcSpawn): RigcRunner {
|
|
357
|
+
let identity: string | null = null;
|
|
358
|
+
return (args) => {
|
|
359
|
+
const call = spawn(bin, args);
|
|
360
|
+
const version = args.length === 1 && args[0] === '--version';
|
|
361
|
+
if (version) {
|
|
362
|
+
const own = rigcIdentity(bin, call);
|
|
363
|
+
identity ??= own;
|
|
364
|
+
return { ...call, ran: own };
|
|
365
|
+
}
|
|
366
|
+
if (call.status === 0) return call;
|
|
367
|
+
identity ??= rigcIdentity(bin, spawn(bin, ['--version']));
|
|
368
|
+
return { ...call, ran: identity };
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
324
372
|
/** A line in which a rigc run that stopped says why: Bun's own `error: …`, rigc's `rigc <command>: …` refusals, a `usage: …` line. */
|
|
325
373
|
const CAUSE = /^(error: |rigc[\w -]*: |usage: )/;
|
|
326
374
|
|
|
@@ -396,6 +444,27 @@ export function readRigcEntry(versionOut: string): RigcEntryRecord {
|
|
|
396
444
|
throw new Error('unreachable');
|
|
397
445
|
}
|
|
398
446
|
|
|
447
|
+
/**
|
|
448
|
+
* This package's own rigc: `node_modules/.bin/rigc` under `root` and nowhere else — no directory above it and no
|
|
449
|
+
* PATH, which {@link findRigc} walks (issue #337). The selftest's suites run rigc through it from the package root,
|
|
450
|
+
* so a fixture in a temp directory, or a tree nested inside another checkout (a harness worktree under
|
|
451
|
+
* `.claude/worktrees/`) whose own install is missing, cannot reach another tree's rigc. A miss is refused naming
|
|
452
|
+
* the one path looked at.
|
|
453
|
+
*/
|
|
454
|
+
export function packageRigc(root: string): string {
|
|
455
|
+
const bin = join(resolve(root), 'node_modules', '.bin', 'rigc');
|
|
456
|
+
if (!existsSync(bin)) {
|
|
457
|
+
refuseIfAny([
|
|
458
|
+
{
|
|
459
|
+
code: 'CHECK_RIGC_PACKAGE',
|
|
460
|
+
object: bin,
|
|
461
|
+
detail: `does not exist; this package's own rigc is required here and no other is looked for — not in a directory above ${resolve(root)}, not on PATH (\`bun install\` puts it there)`,
|
|
462
|
+
},
|
|
463
|
+
]);
|
|
464
|
+
}
|
|
465
|
+
return bin;
|
|
466
|
+
}
|
|
467
|
+
|
|
399
468
|
/**
|
|
400
469
|
* Where the `rigc` binary is: `node_modules/.bin/rigc` in `from` or any
|
|
401
470
|
* directory above it (a clone has it beside the package; an install has it in
|
|
@@ -1646,7 +1715,7 @@ export interface CheckFigures {
|
|
|
1646
1715
|
TIP_OVER_ROOT: JudgementLine;
|
|
1647
1716
|
STILL_REGIONS_DARK: JudgementLine;
|
|
1648
1717
|
TEXTURE_STRETCH: JudgementLine;
|
|
1649
|
-
/** Written
|
|
1718
|
+
/** Written whenever motion.json holds an animation, the idle included (issues #320, #333): {@link FoldsLine}. */
|
|
1650
1719
|
[FOLD_LINE]?: FoldsLine;
|
|
1651
1720
|
RECOMPOSITE_HOLES: ReportedLine;
|
|
1652
1721
|
/** Written only under `--source` (issue #77): the setup pose against the painting, REPORTED. */
|
|
@@ -1672,8 +1741,9 @@ export interface RequirementsBlock {
|
|
|
1672
1741
|
}
|
|
1673
1742
|
|
|
1674
1743
|
/**
|
|
1675
|
-
* The bar over
|
|
1676
|
-
* turned over against the setup pose on any frame, on rig-c's own
|
|
1744
|
+
* The bar over every animation, the idle included (issues #320, #333): no mesh
|
|
1745
|
+
* triangle turned over against the setup pose on any frame, on rig-c's own
|
|
1746
|
+
* reading —
|
|
1677
1747
|
* `rigc render --folds`, one run over every animation the build holds — and
|
|
1678
1748
|
* not a reading of this package's. A fold the art means is declared in the
|
|
1679
1749
|
* requirements file's `may_fold`, by slot and animation with its why, and is
|
|
@@ -1689,8 +1759,12 @@ export interface RequirementsBlock {
|
|
|
1689
1759
|
* not tracked]. The keys and weights fold at every rate or at none that the
|
|
1690
1760
|
* sampling resolves; the rate a consumer steps physics at is the scene's.
|
|
1691
1761
|
*
|
|
1692
|
-
* The idle is
|
|
1693
|
-
*
|
|
1762
|
+
* The idle is read like any other animation (issue #333): `TEXTURE_STRETCH`
|
|
1763
|
+
* reads how far the idle's triangles stretch, and a triangle that turns inside
|
|
1764
|
+
* out is this bar's. Written whenever the motion holds an animation, so a rig
|
|
1765
|
+
* of the idle alone writes it too; on the public examples and the trailer no
|
|
1766
|
+
* idle folds, keys only [measured for issue #333, rigc 2.47.1, a scratch run,
|
|
1767
|
+
* not tracked].
|
|
1694
1768
|
*/
|
|
1695
1769
|
export const FOLD_LINE = 'ANIMATIONS_DO_NOT_FOLD';
|
|
1696
1770
|
|
|
@@ -1777,7 +1851,7 @@ export function besideIdleLine(names: readonly string[]): string | null {
|
|
|
1777
1851
|
}
|
|
1778
1852
|
|
|
1779
1853
|
// ---------------------------------------------------------------------------
|
|
1780
|
-
//
|
|
1854
|
+
// every animation, the idle included, read for folds (issues #320, #333)
|
|
1781
1855
|
// ---------------------------------------------------------------------------
|
|
1782
1856
|
|
|
1783
1857
|
/**
|
|
@@ -1792,7 +1866,7 @@ export const FOLD_RENDER_MAX = 16;
|
|
|
1792
1866
|
/** The fold render, under `--out`; removed before the stage returns. */
|
|
1793
1867
|
const FOLD_SCRATCH = '_folds';
|
|
1794
1868
|
|
|
1795
|
-
/** One mesh that folds in one animation
|
|
1869
|
+
/** One mesh that folds in one animation: rigc's figures, the declaration that takes it if any, and rigc's own line. */
|
|
1796
1870
|
export interface FoldRow {
|
|
1797
1871
|
animation: string;
|
|
1798
1872
|
slot: string;
|
|
@@ -1811,10 +1885,10 @@ export interface FoldRow {
|
|
|
1811
1885
|
line: string;
|
|
1812
1886
|
}
|
|
1813
1887
|
|
|
1814
|
-
/** `check.json`'s `ANIMATIONS_DO_NOT_FOLD`: written
|
|
1888
|
+
/** `check.json`'s `ANIMATIONS_DO_NOT_FOLD`: written whenever the motion holds an animation, the idle included. */
|
|
1815
1889
|
export interface FoldsLine {
|
|
1816
1890
|
status: 'PASS' | 'FAIL';
|
|
1817
|
-
/** Every animation
|
|
1891
|
+
/** Every animation, the idle included, in motion.json's order: each one read. */
|
|
1818
1892
|
animations: string[];
|
|
1819
1893
|
fps: number;
|
|
1820
1894
|
/** How many physics constraints the read build left out ({@link withoutPhysics}); 0 when the rig has none. */
|
|
@@ -1957,8 +2031,9 @@ export function rigcFailed(what: string, call: RigcCall, lines: readonly string[
|
|
|
1957
2031
|
const named = lines.filter((l) => l.includes('FAIL') || l.includes('compile error'));
|
|
1958
2032
|
const quoted = named.length > 0 ? named.map((l) => l.trim()) : causeLines(call.out);
|
|
1959
2033
|
const object = `\`rigc ${what}\``;
|
|
1960
|
-
|
|
1961
|
-
|
|
2034
|
+
const ran = rigcRanClause(call);
|
|
2035
|
+
if (quoted.length === 0) return [{ code: 'CHECK_RIGC_GREEN', object, detail: `exit ${call.status}, and it printed nothing${ran}` }];
|
|
2036
|
+
return quoted.map((l) => ({ code: 'CHECK_RIGC_GREEN', object, detail: `exit ${call.status}: ${l}${ran}` }));
|
|
1962
2037
|
}
|
|
1963
2038
|
|
|
1964
2039
|
/**
|
|
@@ -2065,7 +2140,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
2065
2140
|
let loopSet: FrameSet | null = null;
|
|
2066
2141
|
let loopPhysics: LoopPhysics[] | null = null;
|
|
2067
2142
|
// The keyed build (issues #183, #320): with a physics constraint, the same rig and motion with every one left out,
|
|
2068
|
-
// built once the same way, which the loop and the folds of
|
|
2143
|
+
// built once the same way, which the loop and the folds of every animation are both read from.
|
|
2069
2144
|
const stripped = withoutPhysics(inp.rig, inp.motion);
|
|
2070
2145
|
let keyed: string | null = null;
|
|
2071
2146
|
const keyedBuild = (what: string): string => {
|
|
@@ -2080,7 +2155,8 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
2080
2155
|
keyed = join(loopDir, 'build');
|
|
2081
2156
|
return keyed;
|
|
2082
2157
|
};
|
|
2083
|
-
const
|
|
2158
|
+
const animationNames = isRecord(inp.motion.animations) ? Object.keys(inp.motion.animations) : [];
|
|
2159
|
+
const named = animationNames.filter((n) => n !== 'idle');
|
|
2084
2160
|
let folds: FoldsLine | null = null;
|
|
2085
2161
|
const foldProblems: Problem[] = [];
|
|
2086
2162
|
try {
|
|
@@ -2133,15 +2209,15 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
2133
2209
|
written.push('motion_heat.png');
|
|
2134
2210
|
}
|
|
2135
2211
|
|
|
2136
|
-
// 3a.
|
|
2137
|
-
// rate whatever --requirements says, so a requirements file moves only what it declares (CK78), from the keyed build
|
|
2138
|
-
if (
|
|
2212
|
+
// 3a. every animation, the idle included, read for folds by rigc (issues #320, #333) — only when there is one; at
|
|
2213
|
+
// the idle's rate whatever --requirements says, so a requirements file moves only what it declares (CK78), from the keyed build
|
|
2214
|
+
if (animationNames.length > 0) {
|
|
2139
2215
|
const fps = IDLE_FPS;
|
|
2140
2216
|
try {
|
|
2141
|
-
const fr = rigc(['render', '--candidate', keyedBuild('the animations
|
|
2217
|
+
const fr = rigc(['render', '--candidate', keyedBuild('the animations with their physics constraint(s) left out'), '--folds', '--fps', String(fps), '--max', String(FOLD_RENDER_MAX), '--out', foldDir]);
|
|
2142
2218
|
if (fr.status !== 0) refuseIfAny(rigcFailed(`render --folds --fps ${fps}${stripped === null ? '' : ' (physics constraint(s) left out)'}`, fr, fr.out.split('\n').filter((l) => l.includes('FAIL') || l.includes('rigc:'))));
|
|
2143
|
-
const readings = readFoldSets(foldDir,
|
|
2144
|
-
folds = foldsLine(
|
|
2219
|
+
const readings = readFoldSets(foldDir, animationNames, fps, fr.out);
|
|
2220
|
+
folds = foldsLine(animationNames, fps, stripped === null ? 0 : stripped.physics.length, readings, reqFile === null ? [] : reqFile.may_fold, reqFile === null ? null : reqFile.path, foldProblems);
|
|
2145
2221
|
} finally {
|
|
2146
2222
|
rmSync(foldDir, { recursive: true, force: true });
|
|
2147
2223
|
}
|
|
@@ -2334,7 +2410,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
2334
2410
|
const l = figures[n];
|
|
2335
2411
|
tally(n, l.status === 'SKIP' ? l.reason : null);
|
|
2336
2412
|
}
|
|
2337
|
-
//
|
|
2413
|
+
// Issues #320, #333: the fold bar is counted where it is written — on every rig whose motion holds an animation.
|
|
2338
2414
|
if (folds !== null) tally(FOLD_LINE, null);
|
|
2339
2415
|
return {
|
|
2340
2416
|
figures,
|
|
@@ -3511,7 +3587,7 @@ export function requireRigcVersion(rigc: RigcRunner): string {
|
|
|
3511
3587
|
{
|
|
3512
3588
|
code: 'CHECK_RIGC_VERSION',
|
|
3513
3589
|
object: '`rigc --version`',
|
|
3514
|
-
detail: `${got === null ? `exit ${v.status}, printed ${JSON.stringify(v.out.trim().split('\n')[0] ?? '')} — no version` : `is ${got.join('.')}`}; rig-c ${RIGC_GEOMETRY_VERSION} or later is required — its \`render --geometry\` writes the skinned mesh vertices TEXTURE_STRETCH is measured from (\`bun install\` puts this package's own rig-c at node_modules/.bin/rigc)`,
|
|
3590
|
+
detail: `${got === null ? `exit ${v.status}, printed ${JSON.stringify(v.out.trim().split('\n')[0] ?? '')} — no version` : `is ${got.join('.')}`}${rigcRanClause(v)}; rig-c ${RIGC_GEOMETRY_VERSION} or later is required — its \`render --geometry\` writes the skinned mesh vertices TEXTURE_STRETCH is measured from (\`bun install\` puts this package's own rig-c at node_modules/.bin/rigc)`,
|
|
3515
3591
|
},
|
|
3516
3592
|
]);
|
|
3517
3593
|
}
|
package/src/contour.ts
CHANGED
|
@@ -380,8 +380,31 @@ export interface ContourBridges {
|
|
|
380
380
|
units: number[][];
|
|
381
381
|
/** One per unit but unit 0, in the order they were joined. */
|
|
382
382
|
joins: ContourBridge[];
|
|
383
|
+
/**
|
|
384
|
+
* Only when one was left out (HQ's ruling on #326): each faint island — every pixel's alpha under
|
|
385
|
+
* {@link FAINT_ALPHA} — largest first. Its pixels stay in the image and are not meshed, so not drawn, as `stray`
|
|
386
|
+
* leaves dust; `artPixels` does not count them.
|
|
387
|
+
*/
|
|
388
|
+
faint?: ContourFaintIsland[];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** A faint island a bridged mesh left out (issue #326): its art px, its brightest pixel's alpha, and its box in part-image px `[x, y, w, h]`. */
|
|
392
|
+
export interface ContourFaintIsland {
|
|
393
|
+
art: number;
|
|
394
|
+
maxAlpha: number;
|
|
395
|
+
box: [number, number, number, number];
|
|
383
396
|
}
|
|
384
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Issue #326, HQ's ruling: under `bridges`, an island whose every pixel has alpha below this is faint, left out and
|
|
400
|
+
* named; any island with one pixel at or above it is visible and bridged, whatever the bridge costs. 64/255 is a
|
|
401
|
+
* quarter of full opacity — a faint pixel moves the composite by at most 63/255 of the colour beneath it — and it
|
|
402
|
+
* sits below the 128 that assemble and `check` read as covered (`FRONT_ALPHA`, `COVERED_ALPHA`, check's coverage),
|
|
403
|
+
* so nothing those readings count as drawn is left out. Read over what a bridged draft reaches on the public figures
|
|
404
|
+
* (the four examples and rig-c/trailer 6e2de45), the faint islands peak at 11–17 and the visible ones at 255.
|
|
405
|
+
*/
|
|
406
|
+
export const FAINT_ALPHA = 64;
|
|
407
|
+
|
|
385
408
|
export interface ContourMesh {
|
|
386
409
|
/** Part-image px, y down, every value a multiple of 1/{@link GRID}; the first `hull` are the outline, in walk order. */
|
|
387
410
|
vertices: Array<[number, number]>;
|
|
@@ -2010,8 +2033,11 @@ interface BridgeUnit {
|
|
|
2010
2033
|
* The bridged mesh (issue #326, Stage B; `bridges: true`). Stage A (`docs/evidence/island-draft.md`) measured the
|
|
2011
2034
|
* join this builds by hand: rig-c reads one closed outline per mesh, and a bridge is what a join costs.
|
|
2012
2035
|
*
|
|
2013
|
-
* 1. **Islands
|
|
2014
|
-
*
|
|
2036
|
+
* 1. **Islands**, in this order. Dust first: an island that is not the largest (nor tied with it) and holds `stray`
|
|
2037
|
+
* art px or fewer is left out as the one-island rule leaves it out. Then faint islands: one that is not the
|
|
2038
|
+
* largest and whose every pixel has alpha under {@link FAINT_ALPHA} is left out the same way and named in
|
|
2039
|
+
* `bridges.faint`. Every other island is kept, and step 3 bridges it whatever its bridge costs — overdraw is
|
|
2040
|
+
* reported (`joins[].area`), never a reason to drop art.
|
|
2015
2041
|
* 2. **Silhouettes, in the trace.** The kept islands with their holes filled grow together by the margin
|
|
2016
2042
|
* ({@link growSilhouette}, its pinch fill included), and each 4-connected piece of the grown set is one unit,
|
|
2017
2043
|
* traced on its own. Islands closer than about twice the margin grow into one unit and are traced as one
|
|
@@ -2044,17 +2070,26 @@ function bridgedMesh(part: string, mask: AlphaMask, params: ContourParams, comp:
|
|
|
2044
2070
|
for (let l = 1; l <= n; l++) largest = Math.max(largest, comp.stats[l].area);
|
|
2045
2071
|
const leave: number[] = [];
|
|
2046
2072
|
for (let l = 1; l <= n; l++) if (comp.stats[l].area !== largest && params.stray !== undefined && comp.stats[l].area <= params.stray) leave.push(l);
|
|
2073
|
+
// Issue #326 (HQ's ruling): after dust, a faint island — every pixel's alpha under FAINT_ALPHA — is left out and
|
|
2074
|
+
// named; every other island is bridged whatever its bridge costs. The largest island (or a tie for it) is never left out.
|
|
2075
|
+
const peak = new Array<number>(n + 1).fill(0);
|
|
2076
|
+
for (let i = 0; i < comp.labels.length; i++) if (comp.labels[i] > 0) peak[comp.labels[i]] = Math.max(peak[comp.labels[i]], mask.alpha[i]);
|
|
2077
|
+
const faint: number[] = [];
|
|
2078
|
+
for (let l = 1; l <= n; l++) if (comp.stats[l].area !== largest && !leave.includes(l) && peak[l] < FAINT_ALPHA) faint.push(l);
|
|
2047
2079
|
let meshed = mask;
|
|
2048
2080
|
let strayPixels = 0;
|
|
2049
|
-
if (leave.length > 0) {
|
|
2050
|
-
const out = new Set(leave);
|
|
2081
|
+
if (leave.length > 0 || faint.length > 0) {
|
|
2082
|
+
const out = new Set([...leave, ...faint]);
|
|
2051
2083
|
const alpha = new Uint8Array(mask.alpha);
|
|
2052
2084
|
for (let i = 0; i < alpha.length; i++) if (out.has(comp.labels[i])) alpha[i] = 0;
|
|
2053
2085
|
meshed = { width: w, height: h, alpha };
|
|
2054
2086
|
for (const l of leave) strayPixels += comp.stats[l].area;
|
|
2055
2087
|
}
|
|
2056
2088
|
const keptIslands = new Set<number>();
|
|
2057
|
-
for (let l = 1; l <= n; l++) if (!leave.includes(l)) keptIslands.add(l);
|
|
2089
|
+
for (let l = 1; l <= n; l++) if (!leave.includes(l) && !faint.includes(l)) keptIslands.add(l);
|
|
2090
|
+
const left: ContourFaintIsland[] = faint
|
|
2091
|
+
.map((l) => ({ art: comp.stats[l].area, maxAlpha: peak[l], box: [comp.stats[l].left, comp.stats[l].top, comp.stats[l].width, comp.stats[l].height] as [number, number, number, number] }))
|
|
2092
|
+
.sort((a, b) => b.art - a.art || a.box[1] - b.box[1] || a.box[0] - b.box[0]);
|
|
2058
2093
|
|
|
2059
2094
|
const island = artMask(meshed, threshold);
|
|
2060
2095
|
const filled = fillHoles(island);
|
|
@@ -2269,6 +2304,7 @@ function bridgedMesh(part: string, mask: AlphaMask, params: ContourParams, comp:
|
|
|
2269
2304
|
islands: [...keptIslands].map((l) => comp.stats[l].area).sort((a, b) => b - a),
|
|
2270
2305
|
units: units.map((u) => u.islands),
|
|
2271
2306
|
joins: joins.map((j, i) => ({ ...j, vertices: quads[i].map(at) as [number, number, number, number] })),
|
|
2307
|
+
...(left.length === 0 ? {} : { faint: left }),
|
|
2272
2308
|
},
|
|
2273
2309
|
},
|
|
2274
2310
|
};
|
package/src/errors.ts
CHANGED
|
@@ -23,6 +23,22 @@ export function problemLine(p: Problem): string {
|
|
|
23
23
|
return `${p.code}: ${p.object} — ${p.detail}`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* The end of every FAIL line: the command that prints the guide's row for its
|
|
28
|
+
* rule (issue #343). This is the one place that writes it, so every line has the
|
|
29
|
+
* same form, and rig-c's lines have the same form with its own command. A line
|
|
30
|
+
* that quotes a rigc failure keeps rigc's pointer inside the quote, and this one
|
|
31
|
+
* goes after it.
|
|
32
|
+
*/
|
|
33
|
+
export function guidePointer(code: string): string {
|
|
34
|
+
return ` (see rig-parts guide ${code})`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A refusal as it is printed: ` FAIL RULE: object — detail (see rig-parts guide RULE)`. */
|
|
38
|
+
export function failLine(p: Problem): string {
|
|
39
|
+
return ` FAIL ${problemLine(p)}${guidePointer(p.code)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
export class PartsError extends Error {
|
|
27
43
|
readonly problems: readonly Problem[];
|
|
28
44
|
|