rig-parts 1.49.2 → 1.50.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/cli.ts CHANGED
@@ -714,6 +714,11 @@ usage:
714
714
  and check's artifact, take them). --requirements is forwarded to check
715
715
  (as for check); the file's own shape is read before assemble runs, and
716
716
  its names resolve against the rig in the check, before it builds.
717
+ Without the flag, config.requirements (a path beside the config) is
718
+ read in its place; a flag naming other bytes than the declaration is
719
+ BUILD_REQUIREMENTS_CONFLICT, before assemble. check.json's
720
+ requirements.file records the path as typed or declared, its sha256
721
+ and the file's keys.
717
722
  --environment is forwarded to check (as for check); the schedule is read
718
723
  by rig-c's reader before assemble runs, so a malformed one is refused,
719
724
  CHECK_ENVIRONMENT, under [assemble] before anything is assembled.
package/docs/AUTHORING.md CHANGED
@@ -130,6 +130,7 @@ read by no CPU stage).
130
130
  | `scene` | assemble (the recomposite, its holes and the figure's mask); check, through `parts.json`'s `scene` block (`RECOMPOSITE_HOLES`, the seam) | **authored**, optional; `propose` writes none (issue #242). `{}` declares the painting a scene: the figure and its surroundings in one image, See-through's full run holding only the figure. With it, `assemble` reads the recomposite figures, the hole list and the `uncovered:` lines inside the figure — the rig pixels where any full-run layer's alpha is above 8 (`OPAQUE_ALPHA_ABOVE`, the cut every layer's painted pixels are read by) — each line starting `inside the figure (config.scene: …, <n> of <m> rig px)`; writes that figure as `figure_rig.png` (white on black) beside the recomposite, and a `scene` block in `parts.json`; flags error pixels on the error map inside it only (outside, the painting grey); and `check` leaves out of the seam every frame pixel whose composite sample reaches the painting's outermost row or column, saying how many (`seam_border` in `check.json`, and on the seam line). It takes no field: the figure and its threshold are derived. A key in it other than a record or an annotation is `CONFIG_KEY_KNOWN`. Absent, no byte of any output moves |
131
131
  | `cloth` | `propose --cloth` only; no other stage reads it, so it moves no byte of any build | **authored**, optional (issue #364): one recipe per tag, `{"bottomwear": {…}, "back hair": {…}, "front hair": {…}}`, each `{chains, links, wind, gravity, split?, length_ratio?, strength?, damping?, alternate_damping?, translate?, swing?, part?, influence?}`. `influence` (issue #518) — `{across, max, shape, radius?}`: the parts this recipe meshes are drafted in the influence mode (`meshes.<part>.influence` above) at their lattice's cell, with `across`, `max` and `shape` as declared (each required — the reading behind the radius did not vary them) and `radius` as declared or, absent, drafted as the farthest vertex's distance plus 3 cells (the `influence.radius` row); without it the parts keep their lattice. `chains` — trees across the piece (for `back hair`, per side of the eye axis), a whole number, 1 or more; `links` — links from root to tip on every path, 1 or more; `split` — `{at, into}`: the first `at` links are the trunk, which forks into `into` (2 or more) branches; `at` below `links` (`CONFIG_CLOTH_SPLIT_BEFORE_TIP`); `length_ratio` — a path's last link over its first, above 0, default 0.5; `strength` — `{trunk, branch, edge}`, each above 0 and below the spring's stability bound (`CONFIG_CLOTH_SPRING_STABLE`), default 100 / 100 / 50; `damping` — in [0, 1], absent: rig-c's default; `alternate_damping` — the odd trees' damping, in [0, 1], beside `damping` and different from it (`CONFIG_CLOTH_ALTERNATE_DIFFERS`); `wind`, `gravity` — `[root, tip]`, each 0 or more, tip at or above root (`CONFIG_CLOTH_RESPONSE_GROWS_TO_TIP`); `translate` — the x and y mix of a dangling end's flutter, above 0 and at most 1; `swing` — `bottomwear` only, `{scales}`, one positive scale per tree (`CONFIG_CLOTH_SWING_SCALE_PER_TREE`); `part` — `bottomwear` only (`CONFIG_CLOTH_PART_ON_BOTTOMWEAR`), a non-empty part or cut-piece name: the `topwear` part that holds a skirt See-through returned inside it (a dress), or its skirt piece cut by `assemble.cuts`, which takes the skirt's trees (issue #402, *A skirt inside topwear* below; the part is checked by `propose`, `PROPOSE_CLOTH_PART`). The two defaults are the recipe's own, measured on its synthetic fixture (the recipe below). The recipe below says what each field drafts. The config written: `propose --cloth <config.json> --write-config <out.json>` (issue #537) writes this config's own text back with the sections the recipe drafts spliced in — `bones` and `constraints` whole, `meshes.<part>`, `regions.<part>` and `motion.tracks` entry by entry — and every other byte as read (*The config written*, under the recipe below) |
132
132
  | `proposed_rig_scale` | assemble, rig, build, `propose --from-config`/`--compare` | **proposed** by `propose` (issue #354): `parts.json`'s `scale_rig_per_source`, the `assemble.rig_scale` the rig sections (`bones`, `meshes`, `limbs`, `motion`) were drafted at. Held equal to `assemble.rig_scale` when present (`CONFIG_PROPOSED_RIG_SCALE`; 0, negative or not a number: `CONFIG_FIELD_TYPE`). Absent on a hand-authored config, where no byte of any output moves |
133
+ | `requirements` | build (read in place of `--requirements` when the flag is not given), and so its check | **authored**, optional, never proposed (issue #556): a path, relative to the config file's directory, to the `spine-parts-requirements/1` file the build's check reads (§7, *Declared requirements*) — so a rig that needs one (a `not_swinging` part, a `may_fold`) rebuilds from its config alone. `build` reads it before assemble, as it reads the flag's file, and `check.json`'s `requirements.file` records it with `path` as declared. Given beside `--requirements`, the two must name the same file once resolved, or a file of the same bytes, and the declaration is read and recorded (a build given both writes what the config alone writes); otherwise `BUILD_REQUIREMENTS_CONFLICT`, naming both. Not a non-empty string: `CONFIG_FIELD_TYPE`; no such file, where the config is read from a path: `CONFIG_FILE_PRESENT`. `check`, `compose`, `mesh-draft`, `remesh` and `retune` do not read it: their `--requirements` is the only door. Absent, nothing is read and no byte of any output moves |
133
134
 
134
135
  **One drawing, one band; beyond it, another drawing (issue #472).** A single front-facing
135
136
  `face` plate has one painted outline, one nose ridge, and two eyes in front-view topology.
@@ -2284,7 +2285,7 @@ move off it (`AS245`).
2284
2285
  | `compare` | one line per pair (origin, parent, tip, length, direction — or SKIP and why), then the per-row summary, the unmapped bones of each side, `required:`, each side's roles and the `controls:` line | proposal against the config you corrected: the rows you changed and no others. Config against the `rig.json` `build` writes for it: measured on both examples, every origin within 6.19e-7 px (the rig's offsets are written to 6 places) and every chain link's tip and length within 4.99e-4 px (`length` is written to 3), printed to three places as `0.000` (a signed figure as `+0.000` or `-0.000`); `parent … is the right's ancestor at depth 2, <bone>_ctl between` for every keyed mesh bone, no `DIFFERENT` and no `NOT MAPPED` | `parent DIFFERENT: left a, right b` — a bone hangs from another parent than the other side's; `frames not related` — a rig with no stage (`skeleton.width`/`height`), two configs at different `rig_scale`: the distance rows SKIP, so declare the frame in a `--map` file if the author knows it; `FAIL STRUCTURE_REQUIRED_PRESENT` — a bone the map requires is missing through its pairs |
2285
2286
  | `rig` (inside `build`) | one line per mesh: vertices, triangles, bones, influences, `cover`; the `bones` line; the `idle keys` line; then rigc's gate lines (with A15's declared SKIP under `--idle-keys direct`); in `rig.json`, each chain link's (or its `_ctl`'s) `length` and `rotation` | `cover 1.00000` on every mesh, both gate summaries `0 failed` (the compile and the packed pages); every link's `length` the distance to the next link and `rotation` its direction (Spine degrees, counter-clockwise, y up, local to the parent), every offset under it — a child bone's `x, y`, a weight's bind `x, y`, a region's `x, y` — in that turned frame, and a region on a link carrying `rotation` that turns it back upright. Nothing moved: `flattenRig` (`src/rig.ts`) turns every offset back and gives the unturned numbers | `RIG_LATTICE_ONE_LOOP`: change that mesh's `grid` |
2286
2287
  | `check` (inside `build`) | the gate lines verbatim, the pack line, `loop:` (and `loop physics:` when the rig declares a physics constraint, §7), `seam:`, the six judgement lines, then — when `motion.json` holds an animation, the idle included — `ANIMATIONS_DO_NOT_FOLD` and one `folds` line per folding mesh, rigc's own (issues #320, #333), then — where a `footwear` part rides a bone that hangs from another — `FEET_STAY_LEVEL` and one `foot` line per such part (issue #348), then — when `parts.json` names a `handwear` part and there is an idle — `STRESS_OFF_CHAIN` (issue #347), then — where `parts.json` holds a limb segment's part — `LIMB_ONE_BONE` and one `limb` line per such part (issue #349), then — where a limb record carries a `range` — `SEAM_OVER_RANGE` and one `joint` line per joint and angle (issue #360), then `SEAM_OVER_MOTION` — a SKIP naming why on a rig that declares no neighbour pair, else the pairs read in every animation and one `WARN seam` line per pair and animation over the one-cell floor (issue #493, a warning and never a bar), then `CLOTH_SETTLES` — a SKIP naming why on a rig with no physics constraint, else the chains read and one `WARN chain` line per chain whose tip comes to rest before the idle's loop length (issue #501, a warning and never a bar), `RECOMPOSITE_HOLES` and, under `--source`, `SETUP_POSE_VS_SOURCE` (§7), `check.json`; the last line says how many of the bars measured — nine, ten with the fold bar, one more each with the feet-level bar, the stress bar, the limb bar and the range bar — and names the ones that said SKIP, then — where `SEAM_OVER_MOTION` read a pair — how many warnings it printed, `; N warning(s) (SEAM_OVER_MOTION, not a bar)` — with `CLOTH_SETTLES` named beside it, and N their sum, where the rig declares a physics constraint — which no bar counts | `check: PASS; 11 of 11 bar(s) measured, 0 skipped` on `demo` and `scarf`, whose sleeves each ride one chain; `check: PASS; 10 of 11 bar(s) measured, 1 skipped (STRESS_OFF_CHAIN)` on `reference-demo` and `reference-sample`, whose one `sleeves` mesh rides both sleeve chains (issue #347) — no example writes `FEET_STAY_LEVEL`, every shoe riding `root`; a judgement line SKIP only where the character lacks what it reads; on a rig spec with no `parts.json` or no `idle` (a merged rig, §7 *Measuring a rig rig-parts did not assemble*), PASS with the skipped bars named | `CHECK_SEAM_WITHIN_BAR` or `CHECK_LOOP_CLOSES` (§6); `CHECK_ANIMATIONS_DO_NOT_FOLD` — a clip turns a mesh over (§6); `CHECK_STRESS_OFF_CHAIN` — a sleeve weighted off its chain (§6); `CHECK_LIMB_ONE_BONE` — a limb part on a second bone (§6); `CHECK_SEAM_OVER_RANGE` — a joint that opens when bent (§6) |
2287
- | `check --requirements` (and `build --requirements`) | after the lines above, one line per declared requirement, `NAME: PASS`, `FAIL` or `NOT MEASURABLE — …` with its figures, the bar as declared and the worst frame; then `requirements: N declared — M measured (P PASS, F FAIL), K NOT MEASURABLE; not declared: <kinds>`; `check.json`'s `requirements` block; the renders under `requirements/` (§7, *Declared requirements*) | every line PASS and `0 NOT MEASURABLE`; a follow's `released_copy` says whether rigc's consumer-driven door was taken on the throwaway copy; a `stretch` line's `held_by` (issue #321), right after `worst`, names the bones the worst edge's two vertices hang on and their idle chains, in `CHECK_TEXTURE_STRETCH`'s words (§6) and by the same function, on a PASS as on a FAIL | `CHECK_REQUIREMENT_MET` (a figure past its bar; the line names the frame) or `CHECK_REQUIREMENT_MEASURABLE` (the quantity is undefined in the frames: a tip or an axis on a bone of length 0, an aim with no line, a follow no frame of which reaches its least drive); the last line then reads `check: FAIL — B bar(s) not met, R declared requirement(s) not PASS` |
2288
+ | `check --requirements` (and `build --requirements`) | after the lines above, one line per declared requirement, `NAME: PASS`, `FAIL` or `NOT MEASURABLE — …` with its figures, the bar as declared and the worst frame; then `requirements: N declared — M measured (P PASS, F FAIL), K NOT MEASURABLE; not declared: <kinds>`; `check.json`'s `requirements` block, first its `file` record (issue #556); the renders under `requirements/` (§7, *Declared requirements*) — `build` reads `config.requirements` in place of the flag when it is not given | every line PASS and `0 NOT MEASURABLE`; a follow's `released_copy` says whether rigc's consumer-driven door was taken on the throwaway copy; a `stretch` line's `held_by` (issue #321), right after `worst`, names the bones the worst edge's two vertices hang on and their idle chains, in `CHECK_TEXTURE_STRETCH`'s words (§6) and by the same function, on a PASS as on a FAIL | `CHECK_REQUIREMENT_MET` (a figure past its bar; the line names the frame) or `CHECK_REQUIREMENT_MEASURABLE` (the quantity is undefined in the frames: a tip or an axis on a bone of length 0, an aim with no line, a follow no frame of which reaches its least drive); the last line then reads `check: FAIL — B bar(s) not met, R declared requirement(s) not PASS` |
2288
2289
  | `compose` | the `[compose]` lines: one per character (its offset, its scale, its placed bounds, the shift and how many first-level bones, regions on `root` and weights bound to `root` it moved, its bone, slot, track and constraint counts, its `rig_scale` and idle), the declared order and the slot count it expands to, rigc's gate lines; then the `[check]` lines as for `check`, and `scene.json` | both gate summaries `0 failed`, `check: PASS` with the bars that measured and the ones that said SKIP named — on a composed rig `4 of 9`: the gate, the loop, `CHAIN_LAG` and `TEXTURE_STRETCH` (there is no `parts.json`; each character's own `build` measured all nine). `scene.json`'s `order.slots` is the order the compiled skeleton draws | a `SCENE_*` refusal (§6, *compose*) — the scene file, a build, or the order; `COMPOSE_RIGC_GREEN` — rigc's own line, quoted |
2289
2290
  | `loop` (inside `build --loop`, or `loop --frames … --out …`) | the dropped-duplicate line, or, when the end frame is not frame 0, the jump line: its max \|d\| and px over 40, then why — under `build`, `the rig holds N physics constraint(s), and rigc's render starts every one at rest at frame 0` with the remedy `build --loop --lead-in <n>`, or `the rig holds no physics constraint, so the idle itself does not close` (`CHECK_LOOP_CLOSES`); under `loop --frames`, which reads frames alone, both causes (issue #325); under `build --lead-in`, the lead-in line in its place (the frames kept and the jump at the wrap); each file's line, then `loop: idle.png N B (lossless); idle-indexed.png N B (max …, mean …); idle.gif N B (max …, mean …)` | `f0048.png equals f0000.png byte for byte, so it is dropped` | `LOOP_ENCODE` (§6) |
2290
2291
 
@@ -3134,6 +3135,7 @@ documents do not ship). Each is an input this package built; none is an author's
3134
3135
  | <a id="CHECK_PACK_PAGE_EDGES"></a>`CHECK_PACK_PAGE_EDGES` | the pack line disagrees with the `--page-edges` the build was run with — its `, page edges free` clause is there under `pot` or missing under `free`, or a `pot` page is not a power of two on both edges | a rigc problem; report it, with `rigc --version` |
3135
3136
  | <a id="CHECK_PACK_SHAPE"></a>`CHECK_PACK_SHAPE` | the pack line disagrees with the `--pack-shape` the build was run with — it ends `, shape rect` under `polygon` or `, shape polygon` under `rect`, or it has no shape clause (the 1.5–2.0 form, read as `rect`) under `polygon` | a rigc older than 2.1.0 on `PATH`: rigc 2.0.3 takes `--pack-shape` without a word, packs by rectangles and prints the form with no shape clause, so this refusal is what catches it — `bun install` (this package depends on rig-c ^2.20.4); otherwise a rigc problem, report it with `rigc --version` |
3136
3137
  | <a id="BUILD_ARTIFACT_PRESENT"></a>`BUILD_ARTIFACT_PRESENT` | the packed build lacks its `.json`, `.atlas` or page | a rigc problem; report it |
3138
+ | <a id="BUILD_REQUIREMENTS_CONFLICT"></a>`BUILD_REQUIREMENTS_CONFLICT` | `build --requirements <file>` names a file with other bytes than the one `config.requirements` declares (issue #556): the detail names both, each resolved (the flag against the working directory, the declaration against the config's) with its sha256 — or `no such file`. Refused before assemble writes anything. The same file by both, or a copy of its bytes, is not refused: the declaration is read and recorded | drop the flag to build from the config's file, or point `config.requirements` at the flag's |
3137
3139
  | <a id="REQUIREMENTS_FILE"></a>`REQUIREMENTS_FILE` | `--requirements` names no file, or one that does not parse as a JSON object. Refused before anything is built (by `build`, before assemble) | the path of a `spine-parts-requirements/1` file |
3138
3140
  | <a id="REQUIREMENTS_FIELD"></a>`REQUIREMENTS_FIELD` | a field of the file is missing, of the wrong type, or not one the format reads: `spec`, `fps` (no default), `requirements`, each requirement's `name` (letters, digits, underscores; once each), `kind`, `animation` and its kind's fields — every bar among them (`within_px`, `fraction`, `tolerance`, `least_drive` above 0, `within_degrees` from 0 to 180, `lo_degrees` ≤ `hi_degrees`, `within_ratio` ≥ 1) — and each target's `bone`, `animation` and exactly one of `at` and `keys` (times strictly increasing), and `not_planted`, `not_swinging`, `rises` and `rests` when given (each a non-empty list of part names, each once; no part both in `not_swinging` and in `rises` or `rests`), and `may_fold` when given (a non-empty list of `{ "slot", "animation", "why" }`, every one a non-blank string, each pair once; issue #320). `note`, `*_note` (strings) and `x-…` (any value) are read by nothing, as in the config | write the field; the bar is yours |
3139
3141
  | <a id="REQUIREMENTS_RESOLVES"></a>`REQUIREMENTS_RESOLVES` | a bone, an `ik` or `transform` constraint (by name and `constraint_type`, as rig-c resolves it), a slot, a mesh attachment or an animation the file names is not in `rig.json` or `motion.json`, or a seam's part or a `not_planted`, `not_swinging`, `rises` or `rests` name is not in `parts.json` — the detail lists the ones that are; or an animation whose name cannot be a directory. Refused before anything is built | the name as the rig spells it |
@@ -3834,6 +3836,21 @@ unit (the stage box maps to the world by a translation and the y flip,
3834
3836
  }
3835
3837
  ```
3836
3838
 
3839
+ **Where a rebuild finds the file** (issue #556). A rig that passes only under its
3840
+ requirements — a rigid shorts piece in `not_swinging`, a fold the art means in
3841
+ `may_fold` — rebuilds green only if the next caller knows the file. `check.json`'s
3842
+ `requirements` block opens with `file`: `path` as the caller wrote it (`--requirements`
3843
+ as typed, or `config.requirements` as declared — never resolved, so a build moved to
3844
+ another directory writes the same bytes), the `sha256` of the bytes read, and `keys`,
3845
+ the file's top-level keys in its order, so a reader sees what it asked without opening
3846
+ it. `config.requirements` (§3) declares the file beside the config, and `build` reads
3847
+ it when the flag is not given: a build with `--requirements reqs.json`, typed from the
3848
+ config's directory, and a rebuild from a config declaring `"requirements": "reqs.json"`
3849
+ write the same bytes, every file (selftest `BU178`). A flag naming other bytes than the
3850
+ declaration is `BUILD_REQUIREMENTS_CONFLICT` (`BU179`); the same file by both, or a copy
3851
+ of it, builds what the config alone builds (`BU180`). Without either, no `requirements`
3852
+ block is written and no byte moves.
3853
+
3837
3854
  **Feet that are not planted** (issue #231). The feet halves of `BREATH_VISIBLE` and
3838
3855
  `STILL_REGIONS_DARK` assume a figure that stands: every `footwear` part holds still.
3839
3856
  Whether the figure stands is the scene's knowledge, so a scene where it sits and its
package/docs/STABILITY.md CHANGED
@@ -114,7 +114,7 @@ The fields, object by object (a bold path is where the object sits; `<part>` is
114
114
  part name, `[i]` an entry of a list):
115
115
 
116
116
  <!-- stable:config -->
117
- - **config** — required by the full loader, in front of rig and build: `key`, `assemble`, `bones`, `meshes`, `regions`, `motion`; optional: `generation`, `seethrough`, `limbs`, `constraints`, `stops`, `turn`, `scene`, `cloth`, `proposed_rig_scale`
117
+ - **config** — required by the full loader, in front of rig and build: `key`, `assemble`, `bones`, `meshes`, `regions`, `motion`; optional: `generation`, `seethrough`, `limbs`, `constraints`, `stops`, `turn`, `scene`, `cloth`, `proposed_rig_scale`, `requirements` (issue #556, additive: a path beside the config that build reads in place of its requirements flag, whose behaviour is unchanged)
118
118
  - **generation** — required: `checkpoint`, `loras`, `trigger`, `identity`, `sampler`, `costume`, `negative_extra`, `style`, `negative_pose`, `latent`, `seed`; optional: `pose`, `control`
119
119
  - **generation.loras[i]** — required: `name`, `strength`; optional: `strength_clip`
120
120
  - **generation.sampler** — required: `steps`, `cfg`, `sampler`, `scheduler`
@@ -193,7 +193,7 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
193
193
  | `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), `limb` on a limb segment's part only (issue #349: `name`, `segment`, `of`, `bone`, and below a joint `joint`, `range` when the config segment declares one (issue #360), and `cap` — `at`, `width`, `radius`, `box`, `layer_px`, `image_px` with a cap image, `image_layer` after it when that image was a hidden-art layer — issue #359, `<run>:<layer>:hidden` — `missing_px`, then `shape` and `upper_px` under the segment's `cap_shape`, and `edge`, `feather_radius` and `feathered_px` under its `edge`, and `cut_band`, `cut_offset`, `cut_at`, `cut_crossed`, `joint_crossed`, `band_px` and `stroke_luma_max` (when the band has a split) under its `cut_band`, issue #360), 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); `single_run_pad` (`file_size`, `side`, `left`, `top`) last, only for one run whose file is not the `seethrough.resolution` square (issue #366) |
194
194
  | `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; a bone carries `inherit` only where config.bones declares one, rig-c's mode name as written (issue #348) |
195
195
  | `rig/mesh_report.json` | `rig`, `build` | one row per mesh: `part`, `vertices`, `triangles`, `hull`, `bones` (the sorted names the attachment's weights bind in the written `rig.json`, in every mode — since 1.49.1 (issue #536) a rig with `turn` or `stops` names the twins and `<link>_d` bones there, where the row named the segment bones they replace: a fix of a wrong name, not a format change; a rig with neither writes the bytes it wrote), `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` "influence" with `influence` (as declared), `reach` (`farthest`, `at`, `nearest`, `capped`, `widest`) and `lattice` (rig-c's `SegmentsLatticeReport`: `cols`, `rows`, `artCells`, `keptCells`, `passes`, `islands`) (issue #378); 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) |
196
- | `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`, `environment` after it under `--environment` only (issue #538, additive: `path` as given, `sha256`, `steps`), `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 (issues #320, #363: `status`, `animations`, `fps`, `physics_left_out`, `folding` — each `animation`, `slot`, `attachment`, `frames`, `worst_frame`, `worst_triangles`, `triangles`, `physics_on`, `physics_off`, `physics_constraints`, `declared`, `line` — `folding_undeclared`, `declared_not_folding`, `bar`), `FEET_STAY_LEVEL` after it when a `footwear` part rides a bone that hangs from another and `motion.json` holds an animation (issue #348: `status`, `reason` on a SKIP only, `animations`, `fps`, `physics_left_out`, `bar_degrees`, `bar`, `feet` — each `part`, `bone`, `hangs_from`, `status`, `frames_posed`, `worst`), `LIMB_ONE_BONE` after it (and after `STRESS_OFF_CHAIN` where that is written) when `parts.json` holds a limb segment's part (issue #349: `status`, `parts` — each `part`, `limb`, `bone`, `status`, `others`), `SEAM_OVER_RANGE` after it when a limb record carries a `range` (issue #360: `status`, `frame_scale`, `physics_left_out`, `bar` — `mean`, `rig_px_over_40`, `level` — and `joints`, each `part`, `limb`, `segment`, `joint`, `bone`, `range`, `at`, `radius`, `under_px`, `status`, `poses` — each `degrees`, `status`, `mean`, `px_over_40`, `px_over_80`, `rig_px_over_40` — and `unread` on a joint not read), `SEAM_OVER_MOTION` after it when `parts.json` holds a limb segment or `--requirements` a `seam` (issue #493, a warning and not a bar: `status` — `WARN`, `CLEAR` or `SKIP` — `reason` on a SKIP only, `fps`, `floor`, `pairs` — each `pair`, `a`, `b`, `declared`, and `unread` on a pair not read — `animations`, `warnings`, `rows` — each `pair`, `animation`, `status`, `drawn`, `banded`, `opens`, `px`, `max_px`, `under_floor`), `CLOTH_SETTLES` after it when the rig declares a physics constraint and has an idle (issue #501, a warning and not a bar: `status` — `WARN` or `CLEAR` — `fps`, `frames`, `duration`, `floor_rig_px_per_s`, `floor`, `levers`, `warnings`, `rows` — each `tip`, `root`, `links`, `point`, `settles_at`, `max_rig_px_per_s`, `min_rig_px_per_s`), `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 |
196
+ | `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`, `environment` after it under `--environment` only (issue #538, additive: `path` as given, `sha256`, `steps`), `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 (issues #320, #363: `status`, `animations`, `fps`, `physics_left_out`, `folding` — each `animation`, `slot`, `attachment`, `frames`, `worst_frame`, `worst_triangles`, `triangles`, `physics_on`, `physics_off`, `physics_constraints`, `declared`, `line` — `folding_undeclared`, `declared_not_folding`, `bar`), `FEET_STAY_LEVEL` after it when a `footwear` part rides a bone that hangs from another and `motion.json` holds an animation (issue #348: `status`, `reason` on a SKIP only, `animations`, `fps`, `physics_left_out`, `bar_degrees`, `bar`, `feet` — each `part`, `bone`, `hangs_from`, `status`, `frames_posed`, `worst`), `LIMB_ONE_BONE` after it (and after `STRESS_OFF_CHAIN` where that is written) when `parts.json` holds a limb segment's part (issue #349: `status`, `parts` — each `part`, `limb`, `bone`, `status`, `others`), `SEAM_OVER_RANGE` after it when a limb record carries a `range` (issue #360: `status`, `frame_scale`, `physics_left_out`, `bar` — `mean`, `rig_px_over_40`, `level` — and `joints`, each `part`, `limb`, `segment`, `joint`, `bone`, `range`, `at`, `radius`, `under_px`, `status`, `poses` — each `degrees`, `status`, `mean`, `px_over_40`, `px_over_80`, `rig_px_over_40` — and `unread` on a joint not read), `SEAM_OVER_MOTION` after it when `parts.json` holds a limb segment or `--requirements` a `seam` (issue #493, a warning and not a bar: `status` — `WARN`, `CLEAR` or `SKIP` — `reason` on a SKIP only, `fps`, `floor`, `pairs` — each `pair`, `a`, `b`, `declared`, and `unread` on a pair not read — `animations`, `warnings`, `rows` — each `pair`, `animation`, `status`, `drawn`, `banded`, `opens`, `px`, `max_px`, `under_floor`), `CLOTH_SETTLES` after it when the rig declares a physics constraint and has an idle (issue #501, a warning and not a bar: `status` — `WARN` or `CLEAR` — `fps`, `frames`, `duration`, `floor_rig_px_per_s`, `floor`, `levers`, `warnings`, `rows` — each `tip`, `root`, `links`, `point`, `settles_at`, `max_rig_px_per_s`, `min_rig_px_per_s`), `RECOMPOSITE_HOLES` (its first figure `declared_scene` when `parts.json` declares a scene), `SETUP_POSE_VS_SOURCE`, `skipped`, a `requirements` block under `--requirements` (or `build`'s `config.requirements`) — `file` first from issue #556, additive: `path` as typed or declared, `sha256`, `keys` — and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes |
197
197
  | `layers.json`, `<index>_<name>.png` | `layers --export` | the wrapper form `layers` reads (issue #359): `width`, `height`, then `layers` — each `name`, `filename`, `left`, `top`, `right`, `bottom`, `depth_median` (the input's own; `null` for a PSD, the manifest's order then being the draw order) — drawn layers in draw order, then the hidden-art layers; a PNG's name is its index and its layer's name, and the manifest, not the file name, says which layer it is |
198
198
  | the template `.psd` | `layers --template` | one empty layer per name `templateLayerNames` (`src/layers.ts`) holds, bottom first; the same canvas writes the same bytes |
199
199
  | `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections, `constraints` with `--cloth` (issue #364: a physics constraint per tree link named `<bone>_physics`, a dangling end's `<bone>_translate`, and a skirt swing's transforms `<swing>_<tree>`), `turn` with `--full`/`--head`, `proposed_rig_scale` (issue #354), and `notes` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rig-parts",
3
- "version": "1.49.2",
3
+ "version": "1.50.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": {
package/src/build.ts CHANGED
@@ -40,6 +40,7 @@
40
40
  * no child process. rig-c runs as a process, but the process is injected
41
41
  * as a {@link RigcRunner}, exactly as `src/check.ts` takes it.
42
42
  */
43
+ import { createHash } from 'node:crypto';
43
44
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
44
45
  import { dirname, join, resolve } from 'node:path';
45
46
  import { type AnimFrame, EncodeError, encodeApng, encodeIndexedApng, INDEXED_DEFAULTS } from './apng.ts';
@@ -1347,6 +1348,8 @@ export interface CheckStageInput {
1347
1348
  packShape?: PackShape;
1348
1349
  /** The scene's declared requirements (`--requirements`, issue #93); absent, nothing is read, written or printed for them. */
1349
1350
  requirements?: string;
1351
+ /** The path `check.json`'s `requirements.file` records (issue #556): `config.requirements` as declared when build read it from the config; absent, `requirements` as given. */
1352
+ requirementsAs?: string;
1350
1353
  /**
1351
1354
  * `check --seam-pairs` (issue #202): split the seam's counted pixels by pair
1352
1355
  * and draw each pair's picture even when the bar passes. A failing seam is
@@ -1567,7 +1570,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
1567
1570
  const entryLine = versionLines.find((l) => l.startsWith('entry:'));
1568
1571
  log(` ${rigcIdentity(bin, v)}${entryLine === undefined ? '' : `; ${entryLine}`}`);
1569
1572
  const mode: PackMode = { pageEdges: input.pageEdges ?? DEFAULT_PAGE_EDGES, packShape: input.packShape ?? DEFAULT_PACK_SHAPE };
1570
- const r = runCheck(input.rig, input.out, rigc, input.parts, mode, input.source, input.requirements, input.seamPairs === true, input.keepStills === true, input.set, input.allSets === true, input.environment);
1573
+ const r = runCheck(input.rig, input.out, rigc, input.parts, mode, input.source, input.requirements, input.seamPairs === true, input.keepStills === true, input.set, input.allSets === true, input.environment, input.requirementsAs);
1571
1574
  if (r.figures.environment !== undefined) log(` ${environmentLine(r.figures.environment)}`);
1572
1575
  log(` gate rig-play (rigc ${packedBuildLabel(mode)}), verbatim:`);
1573
1576
  for (const l of r.gateHtml) log(l);
@@ -1885,7 +1888,7 @@ export interface BuildInput {
1885
1888
  pageEdges: PageEdges;
1886
1889
  /** `--pack-shape` for both packed builds, as `pageEdges`; {@link DEFAULT_PACK_SHAPE} when absent. */
1887
1890
  packShape?: PackShape;
1888
- /** `--requirements`, forwarded to the check (issue #93): read before assemble runs, so a file check would refuse is refused first; absent, nothing moves. */
1891
+ /** `--requirements`, forwarded to the check (issue #93): read before assemble runs, so a file check would refuse is refused first; absent, `config.requirements` is read in its place (issue #556, {@link buildRequirements}), and with neither nothing moves. */
1889
1892
  requirements?: string;
1890
1893
  /** `--idle-keys`, forwarded to the rig stage as `rig` takes it (issue #95); {@link DEFAULT_IDLE_KEYS} when absent. */
1891
1894
  idleKeys?: IdleKeys;
@@ -1936,6 +1939,39 @@ export const CHECK_FLAGS_IN_BUILD: Readonly<Record<string, CheckFlagInBuild>> =
1936
1939
  '--environment': { as: 'forwarded' },
1937
1940
  };
1938
1941
 
1942
+ /** The requirements file build's check reads (issue #556): `read` is where it is opened, `recorded` what `check.json` names it by. */
1943
+ export interface BuildRequirements {
1944
+ read: string;
1945
+ recorded: string;
1946
+ }
1947
+
1948
+ /**
1949
+ * Which requirements file a build reads (issue #556): the `--requirements`
1950
+ * flag as typed, else `config.requirements` resolved against the config's
1951
+ * directory and recorded as declared, else none. Both given, they must name
1952
+ * one file — the same path once resolved, or a file of the same bytes — and the
1953
+ * declaration is the one read and recorded, so the build writes what a rebuild
1954
+ * from the config alone writes; two files of different bytes are
1955
+ * `BUILD_REQUIREMENTS_CONFLICT`, naming both.
1956
+ */
1957
+ export function buildRequirements(flag: string | undefined, declared: string | undefined, configPath: string): BuildRequirements | null {
1958
+ if (declared === undefined) return flag === undefined ? null : { read: flag, recorded: flag };
1959
+ const fromConfig = resolve(dirname(resolve(configPath)), declared);
1960
+ if (flag === undefined || resolve(flag) === fromConfig) return { read: fromConfig, recorded: declared };
1961
+ const digestOf = (path: string): string | null => (existsSync(path) && statSync(path).isFile() ? createHash('sha256').update(readFileSync(path)).digest('hex') : null);
1962
+ const flagDigest = digestOf(resolve(flag));
1963
+ const configDigest = digestOf(fromConfig);
1964
+ if (flagDigest !== null && flagDigest === configDigest) return { read: fromConfig, recorded: declared };
1965
+ const shown = (d: string | null): string => (d === null ? 'no such file' : `sha256 ${d.slice(0, 16)}`);
1966
+ throw new PartsError([
1967
+ {
1968
+ code: 'BUILD_REQUIREMENTS_CONFLICT',
1969
+ object: `--requirements ${flag} and config.requirements "${declared}"`,
1970
+ detail: `the flag names ${resolve(flag)} (${shown(flagDigest)}) and the config declares ${fromConfig} (${shown(configDigest)}); a build reads one requirements file and records it, so the two must name the same file or the same bytes — drop the flag to build from the config's file, or point config.requirements at the flag's`,
1971
+ },
1972
+ ]);
1973
+ }
1974
+
1939
1975
  /** The `build` refusal of a `check` flag build supplies itself ({@link CHECK_FLAGS_IN_BUILD}), or null for any other flag. */
1940
1976
  export function suppliedCheckFlag(flag: string): string | null {
1941
1977
  const row = Object.hasOwn(CHECK_FLAGS_IN_BUILD, flag) ? CHECK_FLAGS_IN_BUILD[flag] : undefined;
@@ -2019,14 +2055,16 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
2019
2055
  const idleKeys = input.idleKeys ?? DEFAULT_IDLE_KEYS;
2020
2056
  log(`rig-parts build: ${input.config} -> ${out}, seam rule ${input.seam}, projection rule ${input.project}, page edges ${input.pageEdges}, pack shape ${input.packShape ?? DEFAULT_PACK_SHAPE}${idleKeys === DEFAULT_IDLE_KEYS ? '' : `, idle keys ${idleKeys}`}${input.loop ? `, with the idle loop${input.leadIn === undefined ? '' : ` after a lead-in of ${input.leadIn} period(s)`}` : ''}`);
2021
2057
 
2058
+ let req: BuildRequirements | null = null;
2022
2059
  try {
2023
2060
  // build runs rig next, which needs the whole config, so the full loader is
2024
2061
  // asked first: a config rig would refuse is refused before assemble writes
2025
2062
  // anything, and under [assemble], as it was before the stage had its own
2026
2063
  // narrower door.
2027
- loadConfig(input.config);
2064
+ // Issue #556: the flag, or the config's declaration when there is none; both naming other bytes is refused here.
2065
+ req = buildRequirements(input.requirements, loadConfig(input.config).requirements, input.config);
2028
2066
  // The same for the requirements file's own shape; its names resolve against the rig, in the check, before it builds.
2029
- if (input.requirements !== undefined) readRequirements(input.requirements);
2067
+ if (req !== null) readRequirements(req.read);
2030
2068
  // And the schedule (issue #538), by rigc's own reader, as check reads it.
2031
2069
  if (input.environment !== undefined) readCheckEnvironment(input.environment);
2032
2070
  assembleStage(
@@ -2046,7 +2084,7 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
2046
2084
 
2047
2085
  let report: CheckReport;
2048
2086
  try {
2049
- report = checkStage({ rig: join(out, 'rig'), parts: out, out: join(out, 'check'), pageEdges: input.pageEdges, packShape: input.packShape, ...(input.requirements === undefined ? {} : { requirements: input.requirements }), seamPairs: input.seamPairs === true, keepStills: input.keepStills === true, ...(input.set === undefined ? {} : { set: input.set }), allSets: input.allSets === true, ...(input.environment === undefined ? {} : { environment: input.environment }) }, run.check, run.checkBin, prefixed('check'));
2087
+ report = checkStage({ rig: join(out, 'rig'), parts: out, out: join(out, 'check'), pageEdges: input.pageEdges, packShape: input.packShape, ...(req === null ? {} : { requirements: req.read, requirementsAs: req.recorded }), seamPairs: input.seamPairs === true, keepStills: input.keepStills === true, ...(input.set === undefined ? {} : { set: input.set }), allSets: input.allSets === true, ...(input.environment === undefined ? {} : { environment: input.environment }) }, run.check, run.checkBin, prefixed('check'));
2050
2088
  } catch (err) {
2051
2089
  return refused('check', err);
2052
2090
  }
package/src/check.ts CHANGED
@@ -1905,8 +1905,9 @@ export interface SetCheckFigures {
1905
1905
  PASS: boolean;
1906
1906
  }
1907
1907
 
1908
- /** `check.json`'s `requirements` block: the fps every named animation was sampled at, the summary, and each line by its name, in the file's order. */
1908
+ /** `check.json`'s `requirements` block: which file was read, the fps every named animation was sampled at, the summary, and each line by its name, in the file's order. */
1909
1909
  export interface RequirementsBlock {
1910
+ file: RequirementsFileRecord;
1910
1911
  fps: number;
1911
1912
  summary: RequirementsSummary;
1912
1913
  lines: Record<string, RequirementLine>;
@@ -3722,9 +3723,10 @@ export function environmentLine(env: CheckEnvironment): string {
3722
3723
  * MEASURABLE is a problem. Without it nothing here reads, writes or prints
3723
3724
  * anything it did not before.
3724
3725
  */
3725
- export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, partsHome?: string, mode: PackMode = DEFAULT_PACK_MODE, source?: string, requirements?: string, seamPairsAsked = false, keepStills = false, set?: string, allSets = false, environment?: string): CheckReport {
3726
+ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, partsHome?: string, mode: PackMode = DEFAULT_PACK_MODE, source?: string, requirements?: string, seamPairsAsked = false, keepStills = false, set?: string, allSets = false, environment?: string, requirementsAs: string | undefined = requirements): CheckReport {
3726
3727
  let inp: CheckInputs;
3727
3728
  let reqFile: RequirementsFile | null = null;
3729
+ let reqRecord: RequirementsFileRecord | null = null;
3728
3730
  let seams: Map<string, SeamPair[]> | null = null;
3729
3731
  // Every reader collects every problem; one refusal names every set.
3730
3732
  const refused: Problem[] = [];
@@ -3747,6 +3749,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
3747
3749
  const got = take(() => readCheckInputs(rigDir, partsHome, source, set, allSets));
3748
3750
  reqFile = take(() => readRequirements(requirements));
3749
3751
  refuseIfAny(refused);
3752
+ reqRecord = requirementsFileRecord(requirements, requirementsAs ?? requirements);
3750
3753
  inp = got as CheckInputs;
3751
3754
  const file = reqFile as RequirementsFile;
3752
3755
  const ready = inp;
@@ -4331,7 +4334,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
4331
4334
 
4332
4335
  // 5. the scene's declared requirements (issue #93), only under --requirements
4333
4336
  const reqProblems: Problem[] = [];
4334
- const reqBlock = reqFile === null ? null : measureRequirements(inp, reqFile, buildDir, out, rigc, reqProblems, seams ?? new Map(), measured);
4337
+ const reqBlock = reqFile === null || reqRecord === null ? null : { file: reqRecord, ...measureRequirements(inp, reqFile, buildDir, out, rigc, reqProblems, seams ?? new Map(), measured) };
4335
4338
 
4336
4339
  const skipped: { loop?: string; seam?: string } = {};
4337
4340
  if (inp.noIdle !== null) skipped.loop = inp.noIdle;
@@ -4457,6 +4460,27 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
4457
4460
  /** Where `check` writes the requirement renders, under `--out`: `as-declared/<animation>/`, `released/<name>/`, `full/<name>/`. */
4458
4461
  export const REQUIREMENTS_DIR = 'requirements';
4459
4462
 
4463
+ /**
4464
+ * Issue #556: the requirements file a check read, first in `check.json`'s
4465
+ * `requirements` block — the path as the caller wrote it (the `--requirements`
4466
+ * flag as typed, or `config.requirements` as declared; never resolved, so the
4467
+ * bytes do not move with the directory the build ran in), the sha256 of the
4468
+ * bytes read, and the file's top-level keys in its order, so a reader sees
4469
+ * what the file asked (a `not_swinging`, a `may_fold`) without opening it.
4470
+ */
4471
+ export interface RequirementsFileRecord {
4472
+ path: string;
4473
+ sha256: string;
4474
+ keys: string[];
4475
+ }
4476
+
4477
+ /** The {@link RequirementsFileRecord} of a file {@link readRequirements} has passed, read at `path` and recorded as `recorded`. */
4478
+ export function requirementsFileRecord(path: string, recorded: string): RequirementsFileRecord {
4479
+ const bytes = readFileSync(resolve(path));
4480
+ const raw = JSON.parse(bytes.toString('utf8')) as Record<string, unknown>;
4481
+ return { path: recorded, sha256: createHash('sha256').update(bytes).digest('hex'), keys: Object.keys(raw) };
4482
+ }
4483
+
4460
4484
  /** The throwaway copies' specs and builds, under `--out`; removed before the stage returns. */
4461
4485
  const REQUIREMENTS_SCRATCH = '_requirements';
4462
4486
 
@@ -4478,7 +4502,7 @@ const REQUIREMENTS_SCRATCH = '_requirements';
4478
4502
  * `CHECK_RIGC_GREEN`, quoting rigc. Lines go into the returned block; each
4479
4503
  * FAIL and NOT MEASURABLE into `problems`.
4480
4504
  */
4481
- export function measureRequirements(inp: CheckInputs, file: RequirementsFile, buildDir: string, out: string, rigc: RigcRunner, problems: Problem[], seams: ReadonlyMap<string, readonly SeamPair[]> = new Map(), render: RigcRunner = rigc): RequirementsBlock {
4505
+ export function measureRequirements(inp: CheckInputs, file: RequirementsFile, buildDir: string, out: string, rigc: RigcRunner, problems: Problem[], seams: ReadonlyMap<string, readonly SeamPair[]> = new Map(), render: RigcRunner = rigc): Omit<RequirementsBlock, 'file'> {
4482
4506
  const reqDir = join(out, REQUIREMENTS_DIR);
4483
4507
  const scratch = join(out, REQUIREMENTS_SCRATCH);
4484
4508
  for (const d of [reqDir, scratch]) rmSync(d, { recursive: true, force: true });
package/src/config.ts CHANGED
@@ -88,6 +88,7 @@ import { existsSync, readFileSync, statSync } from 'node:fs';
88
88
  import { dirname, resolve } from 'node:path';
89
89
  import { RIG_BONE_INHERIT, RIG_SKIN_CONSTRAINT_KEYS, type RigBoneInherit, type RigConstraint, type RigSkinConstraintKey, type RigStop } from 'rig-c/src/rig.ts';
90
90
  import { pyRound } from './round.ts';
91
+ import { REQUIREMENTS_SPEC } from './requirements.ts';
91
92
  import { CLOTH_TARGETS, type ClothRecipes, RECIPE_STRENGTH, RIGC_DEFAULT_DAMPING, springStable } from './cloth.ts';
92
93
  import { GRID, MAX_SIDE } from './contour.ts';
93
94
  import { type Problem, refuseIfAny } from './errors.ts';
@@ -951,6 +952,13 @@ export interface CharacterConfig {
951
952
  proposed_rig_scale?: number;
952
953
  /** Alternate painting sets (issue #454): absent, no alternate sets are assembled or emitted. */
953
954
  alternates?: AlternateSetConfig[];
955
+ /**
956
+ * The requirements file the build's check reads (issue #556), a path relative
957
+ * to the config's directory: `build` reads it when no `--requirements` is
958
+ * given, and refuses a flag naming a file with other bytes. Absent, nothing
959
+ * is read and no byte of any output moves.
960
+ */
961
+ requirements?: string;
954
962
  }
955
963
 
956
964
  /** `config.scene` (issue #242): the declaration that the painting is a scene. It holds no field today. */
@@ -1259,7 +1267,7 @@ export interface AssembleConfig {
1259
1267
  proposed_rig_scale?: number;
1260
1268
  }
1261
1269
 
1262
- const TOP_KEYS = ['key', 'generation', 'seethrough', 'assemble', 'limbs', 'bones', 'meshes', 'regions', 'motion', 'constraints', 'stops', 'turn', 'scene', 'cloth', 'proposed_rig_scale'] as const;
1270
+ const TOP_KEYS = ['key', 'generation', 'seethrough', 'assemble', 'limbs', 'bones', 'meshes', 'regions', 'motion', 'constraints', 'stops', 'turn', 'scene', 'cloth', 'proposed_rig_scale', 'requirements'] as const;
1263
1271
 
1264
1272
  /**
1265
1273
  * The one partial entry point. It validates exactly what its door requires —
@@ -1527,6 +1535,7 @@ function parseConfigFrom(raw: Json, base: string | null): { config: CharacterCon
1527
1535
  if ('turn' in t) checkTurn(c, t.turn, attached, names.bones, hasAltFace);
1528
1536
  if ('scene' in t) checkScene(c, t.scene);
1529
1537
  if ('cloth' in t) checkCloth(c, t.cloth);
1538
+ if ('requirements' in t && c.string('config.requirements', t.requirements) && base !== null) checkRequirementsFile(c, resolve(base, t.requirements));
1530
1539
  const from = isPlainObject(t.motion) ? t.motion.animations_from : undefined;
1531
1540
  const animations = base !== null && typeof from === 'string' && from !== '' ? readAnimationsFrom(c, resolve(base, from)) : null;
1532
1541
  if (animations !== null) {
@@ -1568,6 +1577,17 @@ function isPlainObject(v: Json): v is Record<string, Json> {
1568
1577
  return typeof v === 'object' && v !== null && !Array.isArray(v);
1569
1578
  }
1570
1579
 
1580
+ /**
1581
+ * Issue #556: the file `config.requirements` names is on disk —
1582
+ * `CONFIG_FILE_PRESENT` otherwise, as `motion.animations_from`'s. Its shape is
1583
+ * the requirements reader's (`src/requirements.ts`), which `build` runs on it
1584
+ * before assemble, as on the flag's file.
1585
+ */
1586
+ function checkRequirementsFile(c: Check, file: string): void {
1587
+ if (existsSync(file) && statSync(file).isFile()) return;
1588
+ c.fail('CONFIG_FILE_PRESENT', 'config.requirements', `names ${file}, which is no such file; a ${REQUIREMENTS_SPEC} file, its path relative to the config's directory, is required`);
1589
+ }
1590
+
1571
1591
  /**
1572
1592
  * Read the file `motion.animations_from` names (issue #183) and vouch for what
1573
1593
  * merging its animations beside the idle needs, every problem collected in `c`: