@rig-c/parts 1.13.1 → 1.15.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 +1 -1
- package/cli.ts +14 -8
- package/docs/AUTHORING.md +139 -34
- package/docs/MIGRATION.md +62 -0
- package/docs/STABILITY.md +4 -4
- package/package.json +1 -1
- package/src/build.ts +76 -17
- package/src/check.ts +496 -80
- package/src/config.ts +7 -1
- package/src/contour.ts +386 -14
- package/src/instruments.ts +6 -5
- package/src/meshdraft.ts +130 -9
- package/src/requirements.ts +68 -5
- package/src/rig.ts +4 -4
- package/src/scene.ts +2 -2
package/docs/STABILITY.md
CHANGED
|
@@ -127,7 +127,7 @@ part name, `[i]` an entry of a list):
|
|
|
127
127
|
- **meshes.<part>** — exactly one of `grid`, `contour`, `auto`; required in every mode: `r`, `segments`; optional in every mode: `exponent`, `rule`; optional with `contour` or `auto`: `ribs`; optional with `grid` or `contour`: `influences` (issue #284; beside `auto` it is refused, CONFIG_INFLUENCES_MODE); optional with `grid`: `stray` (issue #303; beside `contour` or `auto` it is refused, CONFIG_STRAY_MODE)
|
|
128
128
|
- **meshes.<part>.influences** — optional, at least one: `maxInfluences` (a whole number, 1 or more, or null for no cap), `floor` ("cut" or "continuous")
|
|
129
129
|
- **meshes.<part>.ribs.<chain>** — required: `stations`
|
|
130
|
-
- **meshes.<part>.contour** — required: `tolerance`, `margin`, `spacing`; optional: `maxEdge`, `budget`, `stray`, `regions`
|
|
130
|
+
- **meshes.<part>.contour** — required: `tolerance`, `margin`, `spacing`; optional: `maxEdge`, `budget`, `stray`, `bridges`, `regions`
|
|
131
131
|
- **meshes.<part>.contour.regions[i]** — required: `name`, `shape`, `bone`, `spacing`, `band`, and `cx`, `cy`, `r` for a circle or `points` for a polygon
|
|
132
132
|
- **meshes.<part>.auto** — required: `source`, `sourceBounds`, `targets`, `influences`, `budget`, `minArtSamples`, `motion`; optional: `protect`, `regions`, the three Stage B opt-ins `boundaryRuns`, `retriangulate`, `removalOrder`, and `stimulus`
|
|
133
133
|
- **meshes.<part>.auto.source** — required: `tolerance`, `margin`, `spacing`; optional: `stray`
|
|
@@ -171,11 +171,11 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
|
|
|
171
171
|
| --- | --- | --- |
|
|
172
172
|
| `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
173
|
| `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`); 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`), `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 |
|
|
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`, 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) |
|
|
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, 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
176
|
| `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections, `turn` with `--full`/`--head`, and `notes` |
|
|
177
177
|
| `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`, `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 |
|
|
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`, 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
179
|
| the scene report | `compose` | spec `spine-parts-scene-report/1` |
|
|
180
180
|
| `idle.png`, `idle-indexed.png`, `idle.gif` | `loop`, `build --loop` | APNG and GIF encodings of the rendered idle |
|
|
181
181
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/parts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.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
|
@@ -70,7 +70,7 @@ import {
|
|
|
70
70
|
splitSchedule,
|
|
71
71
|
untestedIntervals,
|
|
72
72
|
} from './autoreplay.ts';
|
|
73
|
-
import {
|
|
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
75
|
import { PartsError, type Problem, problemLine, refuseIfAny } from './errors.ts';
|
|
76
76
|
import type { MeshQualityReport, MotionSchedule } from 'rig-c/mesh';
|
|
@@ -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
|
}
|
|
@@ -972,13 +974,15 @@ function showFigure(v: unknown): string {
|
|
|
972
974
|
}
|
|
973
975
|
|
|
974
976
|
/**
|
|
975
|
-
* How many of the
|
|
977
|
+
* How many of the bars measured and how many said SKIP, by name — the
|
|
976
978
|
* summary's second half (issue #77): `PASS` reads only the bars that measured,
|
|
977
|
-
* so a run that measured one bar says so on the line that says PASS.
|
|
979
|
+
* so a run that measured one bar says so on the line that says PASS. The
|
|
980
|
+
* count is every bar `check` tallied: the `BARS` of `src/check.ts`, and
|
|
981
|
+
* `ANIMATIONS_DO_NOT_FOLD` where it is written (issue #320).
|
|
978
982
|
*/
|
|
979
983
|
export function barsSummary(r: CheckReport): string {
|
|
980
984
|
const { measured, skipped } = r.bars;
|
|
981
|
-
return `${measured.length} of ${
|
|
985
|
+
return `${measured.length} of ${measured.length + skipped.length} bar(s) measured, ${skipped.length} skipped${skipped.length === 0 ? '' : ` (${skipped.map((s) => s.bar).join(', ')})`}`;
|
|
982
986
|
}
|
|
983
987
|
|
|
984
988
|
/**
|
|
@@ -1028,6 +1032,20 @@ export function judgementLine(name: string, line: JudgementLine | ReportedLine):
|
|
|
1028
1032
|
.join('; ')}`;
|
|
1029
1033
|
}
|
|
1030
1034
|
|
|
1035
|
+
/**
|
|
1036
|
+
* The fold line (issues #320, #333) as the console prints it: the status, what was
|
|
1037
|
+
* read and at what rate, and then each folding mesh on a line of its own,
|
|
1038
|
+
* rigc's own words, with the declaration's why where `may_fold` takes it.
|
|
1039
|
+
*/
|
|
1040
|
+
export function foldsLines(line: FoldsLine): string[] {
|
|
1041
|
+
const physics = line.physics_left_out === 0 ? '' : `, its ${line.physics_left_out} physics constraint(s) left out`;
|
|
1042
|
+
const head =
|
|
1043
|
+
`${FOLD_LINE}: ${line.status} — ${line.animations.length} animation(s) read by rigc render --folds at ${line.fps} fps${physics} (${line.animations.join(', ')}): ` +
|
|
1044
|
+
`${line.folding.length} folding mesh(es), ${line.folding_undeclared} not declared in may_fold (<= ${line.bar})` +
|
|
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`);
|
|
1046
|
+
return [head, ...line.folding.map((r) => ` folds ${r.line}${r.declared === null ? '' : ` — declared in may_fold: ${r.declared}`}`)];
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1031
1049
|
/** A requirement's line as the console prints it: its name and status, the reason first when NOT MEASURABLE, then its figures as check.json holds them. */
|
|
1032
1050
|
export function requirementText(name: string, line: RequirementLine): string {
|
|
1033
1051
|
const rest = Object.entries(line).filter(([k]) => k !== 'status' && k !== 'reason');
|
|
@@ -1044,7 +1062,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1044
1062
|
log(`rig-parts check: ${input.rig} -> ${input.out}`);
|
|
1045
1063
|
const versionLines = v.out.trim().split('\n');
|
|
1046
1064
|
const entryLine = versionLines.find((l) => l.startsWith('entry:'));
|
|
1047
|
-
log(`
|
|
1065
|
+
log(` ${rigcIdentity(bin, v)}${entryLine === undefined ? '' : `; ${entryLine}`}`);
|
|
1048
1066
|
const mode: PackMode = { pageEdges: input.pageEdges ?? DEFAULT_PAGE_EDGES, packShape: input.packShape ?? DEFAULT_PACK_SHAPE };
|
|
1049
1067
|
const r = runCheck(input.rig, input.out, rigc, input.parts, mode, input.source, input.requirements, input.seamPairs === true, input.keepStills === true);
|
|
1050
1068
|
log(` gate spine-html (rigc ${packedBuildLabel(mode)}), verbatim:`);
|
|
@@ -1052,7 +1070,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1052
1070
|
for (const l of packLines(r)) log(` ${l}`);
|
|
1053
1071
|
const fig = r.figures;
|
|
1054
1072
|
if (r.idle === null) log(` loop: SKIP — ${fig.skipped?.loop ?? ''}`);
|
|
1055
|
-
else log(`
|
|
1073
|
+
else log(` ${loopCheckLine(r.idle, fig.loop_max_diff, fig.loop_physics?.length ?? 0)}`);
|
|
1056
1074
|
if (r.loopPhysics !== null) log(` ${r.loopPhysics}`);
|
|
1057
1075
|
const beside = besideIdleLine(r.besideIdle);
|
|
1058
1076
|
if (beside !== null) log(` ${beside}`);
|
|
@@ -1066,6 +1084,8 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1066
1084
|
pairs.forEach((row, i) => log(` seam pair ${i + 1} of ${pairs.length}: ${seamPairText(row)}`));
|
|
1067
1085
|
}
|
|
1068
1086
|
for (const name of JUDGEMENT_LINES) log(` ${judgementLine(name, fig[name])}`);
|
|
1087
|
+
const folds = fig[FOLD_LINE];
|
|
1088
|
+
if (folds !== undefined) for (const l of foldsLines(folds)) log(` ${l}`);
|
|
1069
1089
|
for (const name of REPORTED_LINES) log(` ${judgementLine(name, fig[name])}`);
|
|
1070
1090
|
const vsSource = fig[SOURCE_LINE];
|
|
1071
1091
|
if (vsSource !== undefined) log(` ${judgementLine(SOURCE_LINE, vsSource)}`);
|
|
@@ -1216,12 +1236,49 @@ function pixelsOver(a: Raster, b: Raster, level: number): number {
|
|
|
1216
1236
|
return n;
|
|
1217
1237
|
}
|
|
1218
1238
|
|
|
1239
|
+
/**
|
|
1240
|
+
* `check`'s `loop:` line (issue #325). With physics constraints in the rig the
|
|
1241
|
+
* comparison it reports was made with them left out (issue #183), and the line
|
|
1242
|
+
* says so itself rather than leaving it to the `loop physics:` line below: the
|
|
1243
|
+
* loop stage compares the frames rendered with them, and on a rig with physics
|
|
1244
|
+
* those ends differ although this line reads max |d| 0.
|
|
1245
|
+
*/
|
|
1246
|
+
export function loopCheckLine(idle: { frames: number; fps: number; duration: number; lastIndex: number }, max: number | null, physics: number): string {
|
|
1247
|
+
const left = physics === 0 ? '' : ` with the rig's ${physics} physics constraint(s) left out (named on the \`loop physics:\` line; rendered with them, the ends need not meet — \`build --loop --lead-in <n>\`)`;
|
|
1248
|
+
return `loop: idle ${idle.frames} frame(s) at ${idle.fps} fps, f0000 vs f${String(idle.lastIndex).padStart(4, '0')} (t = ${idle.duration}s)${left}: max |d| ${String(max)} (0 required)`;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* The loop stage's line when the end frame is not frame 0 (issue #325): the
|
|
1253
|
+
* jump as measured (max |d| over R, G and B, and pixels over 40 — the lead-in
|
|
1254
|
+
* line's figures), then why. `physics` is how many physics constraints the rig
|
|
1255
|
+
* behind the frames holds; `build --loop` knows it from `check`, while
|
|
1256
|
+
* `loop --frames` reads frames alone and does not, so its line names both
|
|
1257
|
+
* causes. With physics, rigc's render starts every constraint at rest at frame
|
|
1258
|
+
* 0, so the idle does not end where it starts, and `--lead-in` is the remedy;
|
|
1259
|
+
* with none, the idle itself does not close, which `check`'s `loop:` line
|
|
1260
|
+
* measures — under `build`, which stops at that bar, the branch is reached
|
|
1261
|
+
* only by a difference the bar does not read.
|
|
1262
|
+
*/
|
|
1263
|
+
export function loopJumpLine(lastName: string, max: number, over40: number, physics: number | undefined): string {
|
|
1264
|
+
const jump = `${lastName} differs from f0000.png by max |d| ${max}, ${over40} px over 40, so every frame is kept and the loop will jump at the wrap`;
|
|
1265
|
+
const remedy = '`build --loop --lead-in <n>` plays the idle n periods ahead and keeps the last (docs/AUTHORING.md §5, *build --loop --lead-in*)';
|
|
1266
|
+
if (physics === undefined) {
|
|
1267
|
+
return `${jump}; the frames alone do not say why — if their rig holds a physics constraint, rigc's render starts it at rest at frame 0 and ${remedy}; if it holds none, the idle itself does not close, which \`check\`'s \`loop:\` line (CHECK_LOOP_CLOSES) measures`;
|
|
1268
|
+
}
|
|
1269
|
+
if (physics === 0) {
|
|
1270
|
+
return `${jump}: the rig holds no physics constraint, so the idle itself does not close — \`check\`'s \`loop:\` line (CHECK_LOOP_CLOSES) measures the same two frames, and \`--lead-in\`, which settles physics, is not the remedy`;
|
|
1271
|
+
}
|
|
1272
|
+
return `${jump}: the rig holds ${physics} physics constraint(s), and rigc's render starts every one at rest at frame 0, so the idle does not end where it starts — \`check\`'s \`loop:\` line read its ends with them left out; ${remedy}`;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1219
1275
|
/**
|
|
1220
1276
|
* Encode a rigc frame set as a looping APNG, indexed APNG or GIF and write it.
|
|
1221
1277
|
* An encoder refusal is a PartsError coded `LOOP_ENCODE`, so it prints like
|
|
1222
|
-
* every other.
|
|
1278
|
+
* every other. `physics` is how many physics constraints the frames' rig holds,
|
|
1279
|
+
* when the caller knows it ({@link loopJumpLine}).
|
|
1223
1280
|
*/
|
|
1224
|
-
export function loopStage(frames: string, out: string, format: LoopFormat, log: Log, window?: LoopWindow): LoopResult {
|
|
1281
|
+
export function loopStage(frames: string, out: string, format: LoopFormat, log: Log, window?: LoopWindow, physics?: number): LoopResult {
|
|
1225
1282
|
const set = readFrameSet(frames);
|
|
1226
1283
|
const images = set.frames.map((fr) => fr.image);
|
|
1227
1284
|
log(`rig-parts loop: ${set.dir} -> ${out}`);
|
|
@@ -1243,7 +1300,8 @@ export function loopStage(frames: string, out: string, format: LoopFormat, log:
|
|
|
1243
1300
|
used = images.slice(0, -1);
|
|
1244
1301
|
log(` ${lastName} equals f0000.png byte for byte, so it is dropped: the loop wraps onto frame 0, and showing it twice would hold that pose for two ticks`);
|
|
1245
1302
|
} else if (images.length > 1) {
|
|
1246
|
-
|
|
1303
|
+
const last = images[images.length - 1];
|
|
1304
|
+
log(` ${loopJumpLine(lastName, maxRgbDiff(images[0], last).max, pixelsOver(images[0], last, 40), physics)}`);
|
|
1247
1305
|
}
|
|
1248
1306
|
const anim: AnimFrame[] = used.map((image) => ({ image, ticks: 1 }));
|
|
1249
1307
|
log(` ${anim.length} frame(s) encoded, ${(anim.length / set.fps).toFixed(3)}s per loop, looping forever`);
|
|
@@ -1462,9 +1520,10 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
|
|
|
1462
1520
|
const window = input.leadIn === undefined ? undefined : { periods: input.leadIn };
|
|
1463
1521
|
if (window !== undefined) rmSync(run.scratch, { recursive: true, force: true });
|
|
1464
1522
|
const frames = window === undefined ? join(out, 'check', 'idle_frames') : leadInFrames(join(out, 'rig'), run.scratch, window.periods, run.check, { pageEdges: input.pageEdges, packShape: input.packShape ?? DEFAULT_PACK_SHAPE });
|
|
1465
|
-
const
|
|
1466
|
-
const
|
|
1467
|
-
const
|
|
1523
|
+
const physics = report.figures.loop_physics?.length ?? 0;
|
|
1524
|
+
const lossless = loopStage(frames, join(out, 'idle.png'), 'apng', say, window, physics);
|
|
1525
|
+
const indexed = loopStage(frames, join(out, 'idle-indexed.png'), 'indexed', say, window, physics);
|
|
1526
|
+
const gif = loopStage(frames, join(out, 'idle.gif'), 'gif', say, window, physics);
|
|
1468
1527
|
if (window !== undefined) rmSync(run.scratch, { recursive: true, force: true });
|
|
1469
1528
|
const fig = (e: PaletteError | null): string => (e === null ? 'lossless' : `max ${e.max}, mean ${e.mean.toFixed(3)}`);
|
|
1470
1529
|
say(`loop: idle.png ${lossless.bytes} B (${fig(lossless.error)}); idle-indexed.png ${indexed.bytes} B (${fig(indexed.error)}); idle.gif ${gif.bytes} B (${fig(gif.error)}) — palette error per channel over all frames`);
|