@rig-c/parts 1.13.1 → 1.14.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/src/build.ts CHANGED
@@ -70,7 +70,7 @@ import {
70
70
  splitSchedule,
71
71
  untestedIntervals,
72
72
  } from './autoreplay.ts';
73
- import { BARS, besideIdleLine, 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, 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';
@@ -972,13 +972,15 @@ function showFigure(v: unknown): string {
972
972
  }
973
973
 
974
974
  /**
975
- * How many of the {@link BARS} measured and how many said SKIP, by name — the
975
+ * How many of the bars measured and how many said SKIP, by name — the
976
976
  * 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.
977
+ * so a run that measured one bar says so on the line that says PASS. The
978
+ * count is every bar `check` tallied: the `BARS` of `src/check.ts`, and
979
+ * `ANIMATIONS_DO_NOT_FOLD` where it is written (issue #320).
978
980
  */
979
981
  export function barsSummary(r: CheckReport): string {
980
982
  const { measured, skipped } = r.bars;
981
- return `${measured.length} of ${BARS.length} bar(s) measured, ${skipped.length} skipped${skipped.length === 0 ? '' : ` (${skipped.map((s) => s.bar).join(', ')})`}`;
983
+ return `${measured.length} of ${measured.length + skipped.length} bar(s) measured, ${skipped.length} skipped${skipped.length === 0 ? '' : ` (${skipped.map((s) => s.bar).join(', ')})`}`;
982
984
  }
983
985
 
984
986
  /**
@@ -1028,6 +1030,20 @@ export function judgementLine(name: string, line: JudgementLine | ReportedLine):
1028
1030
  .join('; ')}`;
1029
1031
  }
1030
1032
 
1033
+ /**
1034
+ * The fold line (issue #320) as the console prints it: the status, what was
1035
+ * read and at what rate, and then each folding mesh on a line of its own,
1036
+ * rigc's own words, with the declaration's why where `may_fold` takes it.
1037
+ */
1038
+ export function foldsLines(line: FoldsLine): string[] {
1039
+ const physics = line.physics_left_out === 0 ? '' : `, its ${line.physics_left_out} physics constraint(s) left out`;
1040
+ const head =
1041
+ `${FOLD_LINE}: ${line.status} — ${line.animations.length} animation(s) beside the idle read by rigc render --folds at ${line.fps} fps${physics} (${line.animations.join(', ')}): ` +
1042
+ `${line.folding.length} folding mesh(es), ${line.folding_undeclared} not declared in may_fold (<= ${line.bar})` +
1043
+ (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
+ return [head, ...line.folding.map((r) => ` folds ${r.line}${r.declared === null ? '' : ` — declared in may_fold: ${r.declared}`}`)];
1045
+ }
1046
+
1031
1047
  /** 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
1048
  export function requirementText(name: string, line: RequirementLine): string {
1033
1049
  const rest = Object.entries(line).filter(([k]) => k !== 'status' && k !== 'reason');
@@ -1052,7 +1068,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
1052
1068
  for (const l of packLines(r)) log(` ${l}`);
1053
1069
  const fig = r.figures;
1054
1070
  if (r.idle === null) log(` loop: SKIP — ${fig.skipped?.loop ?? ''}`);
1055
- else log(` loop: idle ${r.idle.frames} frame(s) at ${r.idle.fps} fps, f0000 vs f${String(r.idle.lastIndex).padStart(4, '0')} (t = ${r.idle.duration}s): max |d| ${fig.loop_max_diff} (0 required)`);
1071
+ else log(` ${loopCheckLine(r.idle, fig.loop_max_diff, fig.loop_physics?.length ?? 0)}`);
1056
1072
  if (r.loopPhysics !== null) log(` ${r.loopPhysics}`);
1057
1073
  const beside = besideIdleLine(r.besideIdle);
1058
1074
  if (beside !== null) log(` ${beside}`);
@@ -1066,6 +1082,8 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
1066
1082
  pairs.forEach((row, i) => log(` seam pair ${i + 1} of ${pairs.length}: ${seamPairText(row)}`));
1067
1083
  }
1068
1084
  for (const name of JUDGEMENT_LINES) log(` ${judgementLine(name, fig[name])}`);
1085
+ const folds = fig[FOLD_LINE];
1086
+ if (folds !== undefined) for (const l of foldsLines(folds)) log(` ${l}`);
1069
1087
  for (const name of REPORTED_LINES) log(` ${judgementLine(name, fig[name])}`);
1070
1088
  const vsSource = fig[SOURCE_LINE];
1071
1089
  if (vsSource !== undefined) log(` ${judgementLine(SOURCE_LINE, vsSource)}`);
@@ -1216,12 +1234,49 @@ function pixelsOver(a: Raster, b: Raster, level: number): number {
1216
1234
  return n;
1217
1235
  }
1218
1236
 
1237
+ /**
1238
+ * `check`'s `loop:` line (issue #325). With physics constraints in the rig the
1239
+ * comparison it reports was made with them left out (issue #183), and the line
1240
+ * says so itself rather than leaving it to the `loop physics:` line below: the
1241
+ * loop stage compares the frames rendered with them, and on a rig with physics
1242
+ * those ends differ although this line reads max |d| 0.
1243
+ */
1244
+ export function loopCheckLine(idle: { frames: number; fps: number; duration: number; lastIndex: number }, max: number | null, physics: number): string {
1245
+ 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>\`)`;
1246
+ 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)`;
1247
+ }
1248
+
1249
+ /**
1250
+ * The loop stage's line when the end frame is not frame 0 (issue #325): the
1251
+ * jump as measured (max |d| over R, G and B, and pixels over 40 — the lead-in
1252
+ * line's figures), then why. `physics` is how many physics constraints the rig
1253
+ * behind the frames holds; `build --loop` knows it from `check`, while
1254
+ * `loop --frames` reads frames alone and does not, so its line names both
1255
+ * causes. With physics, rigc's render starts every constraint at rest at frame
1256
+ * 0, so the idle does not end where it starts, and `--lead-in` is the remedy;
1257
+ * with none, the idle itself does not close, which `check`'s `loop:` line
1258
+ * measures — under `build`, which stops at that bar, the branch is reached
1259
+ * only by a difference the bar does not read.
1260
+ */
1261
+ export function loopJumpLine(lastName: string, max: number, over40: number, physics: number | undefined): string {
1262
+ 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`;
1263
+ 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*)';
1264
+ if (physics === undefined) {
1265
+ 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`;
1266
+ }
1267
+ if (physics === 0) {
1268
+ 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`;
1269
+ }
1270
+ 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}`;
1271
+ }
1272
+
1219
1273
  /**
1220
1274
  * Encode a rigc frame set as a looping APNG, indexed APNG or GIF and write it.
1221
1275
  * An encoder refusal is a PartsError coded `LOOP_ENCODE`, so it prints like
1222
- * every other.
1276
+ * every other. `physics` is how many physics constraints the frames' rig holds,
1277
+ * when the caller knows it ({@link loopJumpLine}).
1223
1278
  */
1224
- export function loopStage(frames: string, out: string, format: LoopFormat, log: Log, window?: LoopWindow): LoopResult {
1279
+ export function loopStage(frames: string, out: string, format: LoopFormat, log: Log, window?: LoopWindow, physics?: number): LoopResult {
1225
1280
  const set = readFrameSet(frames);
1226
1281
  const images = set.frames.map((fr) => fr.image);
1227
1282
  log(`rig-parts loop: ${set.dir} -> ${out}`);
@@ -1243,7 +1298,8 @@ export function loopStage(frames: string, out: string, format: LoopFormat, log:
1243
1298
  used = images.slice(0, -1);
1244
1299
  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
1300
  } else if (images.length > 1) {
1246
- log(` ${lastName} differs from f0000.png, so every frame is kept and the loop will jump at the wrap`);
1301
+ const last = images[images.length - 1];
1302
+ log(` ${loopJumpLine(lastName, maxRgbDiff(images[0], last).max, pixelsOver(images[0], last, 40), physics)}`);
1247
1303
  }
1248
1304
  const anim: AnimFrame[] = used.map((image) => ({ image, ticks: 1 }));
1249
1305
  log(` ${anim.length} frame(s) encoded, ${(anim.length / set.fps).toFixed(3)}s per loop, looping forever`);
@@ -1462,9 +1518,10 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
1462
1518
  const window = input.leadIn === undefined ? undefined : { periods: input.leadIn };
1463
1519
  if (window !== undefined) rmSync(run.scratch, { recursive: true, force: true });
1464
1520
  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 lossless = loopStage(frames, join(out, 'idle.png'), 'apng', say, window);
1466
- const indexed = loopStage(frames, join(out, 'idle-indexed.png'), 'indexed', say, window);
1467
- const gif = loopStage(frames, join(out, 'idle.gif'), 'gif', say, window);
1521
+ const physics = report.figures.loop_physics?.length ?? 0;
1522
+ const lossless = loopStage(frames, join(out, 'idle.png'), 'apng', say, window, physics);
1523
+ const indexed = loopStage(frames, join(out, 'idle-indexed.png'), 'indexed', say, window, physics);
1524
+ const gif = loopStage(frames, join(out, 'idle.gif'), 'gif', say, window, physics);
1468
1525
  if (window !== undefined) rmSync(run.scratch, { recursive: true, force: true });
1469
1526
  const fig = (e: PaletteError | null): string => (e === null ? 'lossless' : `max ${e.max}, mean ${e.mean.toFixed(3)}`);
1470
1527
  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`);