rig-parts 1.5.1 → 1.5.3

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 CHANGED
@@ -37,10 +37,10 @@ is taken from the full run, because the twin tails leave the head crop sideways
37
37
  (a plan edit). <code>check</code> printed, verbatim:<br/>
38
38
  <code>50 assertions: 24 measured (24 passed, 0 failed), 26 skipped, 0 not in profile "spine-html"</code><br/>
39
39
  <code>loop: idle 49 frame(s) at 12 fps, f0000 vs f0048 (t = 4s): max |d| 0 (0 required)</code><br/>
40
- <code>seam: setup pose at 714x1216, scale 0.9422: mean |d| 0.324 (&lt;= 1.0), 2 px over 40 (&lt;= 50), 0 px over 80 (reported)</code><br/>
41
- <code>pack: skeleton.png 922x1348, 22 region(s), 95.0% covered, padding 2, page edges free, shape polygon</code><br/>
40
+ <code>seam: setup pose at 714x1216, scale 0.9422: mean |d| 0.234 (&lt;= 1.0), 2 px over 40, 2.25 rig px (the count / s²; &lt;= 50), 0 px over 80 (reported)</code><br/>
41
+ <code>pack: skeleton.png 922x1348, 22 region(s), 94.9% covered, padding 2, page edges free, shape polygon</code><br/>
42
42
  <code>--seam silhouette</code> repaired the navy blobs the default rule leaves on the white
43
- blouse (recomposite error pixels 11,050 → 9,540); the default stays
43
+ blouse (recomposite error pixels 10,340 → 8,644); the default stays
44
44
  <code>near-white</code>, the reference implementation's rule, so the examples stay
45
45
  comparable with it. The loop is <code>rig-parts loop --palette</code>'s indexed APNG, 48 frames at
46
46
  12 fps, 1,706,719 bytes, one 256-entry palette at a measured error of max 57, mean 1.601
@@ -60,7 +60,7 @@ made the contact sheet.
60
60
  <p align="center"><em>
61
61
  The plain fixture, <code>examples/sample</code>: same checkpoint, no LoRA, and a rig
62
62
  built from its own proposal with no hand edit at all —
63
- <code>seam: … mean |d| 0.207 (&lt;= 1.0), 0 px over 40 (&lt;= 50)</code>, loop max |d| 0.
63
+ <code>seam: setup pose at 379x1216, scale 1.0116: mean |d| 0.204 (&lt;= 1.0), 2 px over 40, 1.95 rig px (the count / s²; &lt;= 50), 0 px over 80 (reported)</code>, loop max |d| 0.
64
64
  </em></p>
65
65
 
66
66
  <p align="center"><em>
package/cli.ts CHANGED
@@ -225,12 +225,17 @@ usage:
225
225
  contact.png, motion_heat.png and check.json (with rigc_entry, the rigc
226
226
  entry that gated the build, and pack_mode, the two pack flags). PASS needs every gate summary
227
227
  "0 failed", the seam (setup pose vs the flat composite of parts/) at mean
228
- |d| <= ${SEAM_MEAN_BAR.toFixed(1)} with <= ${SEAM_PX_BAR} px over ${SEAM_PX_LEVEL}, and the loop (idle frame 0 vs the
229
- frame at t = duration) at max |d| 0. A failing seam, or any seam under
230
- --seam-pairs, is split by the pair of parts each counted pixel lies
231
- between: one "seam pair" line each, worst first, with its count, frame
232
- and rig boxes and a picture under seam/, and check.json's seam_pairs
233
- (AUTHORING §7). --keep-stills leaves the setup-pose still the seam reads,
228
+ |d| <= ${SEAM_MEAN_BAR.toFixed(1)} with <= ${SEAM_PX_BAR} rig px over ${SEAM_PX_LEVEL} (the frame pixels over ${SEAM_PX_LEVEL}
229
+ divided by s², s the frame scale the seam: line prints and check.json's
230
+ seam_frame_scale holds, so the bar means the same at every scale), and
231
+ the loop (idle frame 0 vs the frame at t = duration) at max |d| 0. A
232
+ failing seam, or any seam under --seam-pairs, is split by the pair of
233
+ parts each counted pixel lies between: one "seam pair" line each, worst
234
+ first, with its count in frame px, frame and rig boxes and a picture
235
+ under seam/, and check.json's seam_pairs (AUTHORING §7). A seam picture
236
+ is the setup-pose frame with that pair's counted pixels painted magenta
237
+ (255, 0, 255) — magenta = a counted pixel — and a cyan (0, 255, 255)
238
+ box one pixel outside them. --keep-stills leaves the setup-pose still the seam reads,
234
239
  rigc's render directory with its frames.json (viewport and background),
235
240
  under setup_still/. Then six judgement lines, each in
236
241
  check.json and on the console as NAME: PASS|FAIL|SKIP with its figures and
@@ -545,6 +550,39 @@ function printLayerTable(set: LayerSet): void {
545
550
  console.log(` ${warned} WARN line(s)`);
546
551
  }
547
552
 
553
+ /**
554
+ * Every flag each command takes: `valued` flags carry a value, `switches` are bare. Each parser reads its row to
555
+ * decide what it accepts, and {@link notTaken} prints the same row in the unknown-flag refusal, so the refusal
556
+ * lists exactly the flags the parser accepts (CL26). `layers` takes one positional path and no flag, so it has no row.
557
+ */
558
+ const TAKES = {
559
+ sheet: { valued: ['--source', '--layers', '--out', '--cell', '--cols'], switches: [] },
560
+ rig: { valued: ['--config', '--parts', '--out', '--idle-keys', '--page-edges', '--pack-shape'], switches: [] },
561
+ propose: { valued: ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--canvas', '--keypoints', '--person', '--wrist-snap'], switches: ['--head-box'] },
562
+ compare: { valued: ['--left', '--right', '--map'], switches: [] },
563
+ check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements'], switches: ['--seam-pairs', '--keep-stills'] },
564
+ loop: { valued: ['--frames', '--out'], switches: ['--palette'] },
565
+ assemble: { valued: ['--source', '--full', '--head', '--config', '--out', '--seam', '--project'], switches: ['--propose-plan'] },
566
+ 'comfy seethrough': { valued: ['--image', '--out', '--resolution', '--steps', '--seed', '--prefix', '--host', '--wait', '--timeout', '--poll'], switches: ['--offload', '--lama', '--nf4'] },
567
+ 'comfy paint': { valued: ['--config', '--out', '--seeds', '--seed0', '--host', '--wait', '--timeout', '--poll'], switches: [] },
568
+ inputs: { valued: ['--source', '--config', '--out'], switches: [] },
569
+ build: { valued: ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges', '--pack-shape', '--requirements', '--idle-keys'], switches: ['--loop'] },
570
+ compose: { valued: ['--scene', '--out', '--requirements', '--accept'], switches: [] },
571
+ } as const satisfies Record<string, { valued: readonly string[]; switches: readonly string[] }>;
572
+
573
+ type Command = keyof typeof TAKES;
574
+
575
+ /** Whether `command` takes `flag` as a valued flag, read off its {@link TAKES} row. */
576
+ function takesValued(command: Command, flag: string): boolean {
577
+ return (TAKES[command].valued as readonly string[]).includes(flag);
578
+ }
579
+
580
+ /** The unknown-flag refusal: the flag, and every flag the command's {@link TAKES} row holds, valued then bare. */
581
+ function notTaken(command: Command, flag: string): string {
582
+ const row = TAKES[command];
583
+ return `${command} does not take "${flag}"; it takes ${[...row.valued, ...row.switches].join(', ')}`;
584
+ }
585
+
548
586
  function cmdLayers(args: string[]): number {
549
587
  if (args.length !== 1) return usage(`layers takes one path; got ${args.length}`);
550
588
  try {
@@ -564,7 +602,7 @@ function cmdSheet(args: string[]): number {
564
602
  for (let i = 0; i < args.length; i++) {
565
603
  const flag = args[i];
566
604
  const value = args[i + 1];
567
- if (!['--source', '--layers', '--out', '--cell', '--cols'].includes(flag)) return usage(`sheet does not take "${flag}"`);
605
+ if (!takesValued('sheet', flag)) return usage(notTaken('sheet', flag));
568
606
  if (value === undefined) return usage(`${flag} needs a value`);
569
607
  i++;
570
608
  if (flag === '--source') source = value;
@@ -634,7 +672,7 @@ function cmdRig(args: string[]): number {
634
672
  for (let i = 0; i < args.length; i++) {
635
673
  const flag = args[i];
636
674
  const value = args[i + 1];
637
- if (!['--config', '--parts', '--out', '--idle-keys', '--page-edges', '--pack-shape'].includes(flag)) return usage(`rig does not take "${flag}"`);
675
+ if (!takesValued('rig', flag)) return usage(notTaken('rig', flag));
638
676
  if (value === undefined) return usage(`${flag} needs a value`);
639
677
  i++;
640
678
  if (flag === '--config') config = value;
@@ -694,17 +732,17 @@ function rigcRunner(bin: string): RigcRunner {
694
732
  };
695
733
  }
696
734
 
697
- function flags(args: string[], known: readonly string[], command: string, optional: readonly string[] = []): Map<string, string> | string {
735
+ /** The command's valued flags as `--flag value` pairs, each one its {@link TAKES} row holds; `required` must all be given. Its bare switches are taken out by the caller before this reads the rest. A string is a usage error. */
736
+ function flags(args: string[], command: Command, required: readonly string[]): Map<string, string> | string {
698
737
  const got = new Map<string, string>();
699
- const all = [...known, ...optional];
700
738
  for (let i = 0; i < args.length; i += 2) {
701
739
  const flag = args[i];
702
- if (!all.includes(flag)) return `${command} does not take "${flag}"; it takes ${all.join(', ')}`;
740
+ if (!takesValued(command, flag)) return notTaken(command, flag);
703
741
  if (args[i + 1] === undefined) return `${flag} needs a value`;
704
742
  if (got.has(flag)) return `${flag} is given twice`;
705
743
  got.set(flag, args[i + 1]);
706
744
  }
707
- for (const k of known) if (!got.has(k)) return `${command} needs ${k} <value>`;
745
+ for (const k of required) if (!got.has(k)) return `${command} needs ${k} <value>`;
708
746
  return got;
709
747
  }
710
748
 
@@ -716,9 +754,8 @@ function cmdCheck(args: string[]): number {
716
754
  if (keepStills > 1) return usage('--keep-stills is given twice');
717
755
  const f = flags(
718
756
  args.filter((a) => a !== '--seam-pairs' && a !== '--keep-stills'),
719
- ['--rig', '--out'],
720
757
  'check',
721
- ['--parts', '--source', '--page-edges', '--pack-shape', '--requirements'],
758
+ ['--rig', '--out'],
722
759
  );
723
760
  if (typeof f === 'string') return usage(f);
724
761
  const rig = f.get('--rig') as string;
@@ -768,14 +805,13 @@ function printLint(P: PartSet, spec: { bones: CharacterConfig['bones']; meshes:
768
805
  function cmdPropose(args: string[]): number {
769
806
  const flags = new Map<string, string>();
770
807
  let headBox = false;
771
- const valued = ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--canvas', '--keypoints', '--person', '--wrist-snap'];
772
808
  for (let i = 0; i < args.length; i++) {
773
809
  const flag = args[i];
774
810
  if (flag === '--head-box') {
775
811
  headBox = true;
776
812
  continue;
777
813
  }
778
- if (!valued.includes(flag)) return usage(`propose does not take "${flag}"; it takes --head-box and ${valued.join(', ')}`);
814
+ if (!takesValued('propose', flag)) return usage(notTaken('propose', flag));
779
815
  const value = args[i + 1];
780
816
  if (value === undefined) return usage(`${flag} needs a value`);
781
817
  if (flags.has(flag)) return usage(`${flag} is given twice`);
@@ -873,10 +909,9 @@ function cmdPropose(args: string[]): number {
873
909
 
874
910
  function cmdCompare(args: string[]): number {
875
911
  const flags = new Map<string, string>();
876
- const valued = ['--left', '--right', '--map'];
877
912
  for (let i = 0; i < args.length; i++) {
878
913
  const flag = args[i];
879
- if (!valued.includes(flag)) return usage(`compare does not take "${flag}"; it takes ${valued.join(', ')}`);
914
+ if (!takesValued('compare', flag)) return usage(notTaken('compare', flag));
880
915
  const value = args[i + 1];
881
916
  if (value === undefined) return usage(`${flag} needs a value`);
882
917
  if (flags.has(flag)) return usage(`${flag} is given twice`);
@@ -905,8 +940,8 @@ function cmdLoop(args: string[]): number {
905
940
  if (palettes > 1) return usage('--palette is given twice');
906
941
  const f = flags(
907
942
  args.filter((a) => a !== '--palette'),
908
- ['--frames', '--out'],
909
943
  'loop',
944
+ ['--frames', '--out'],
910
945
  );
911
946
  if (typeof f === 'string') return usage(f);
912
947
  const out = f.get('--out') as string;
@@ -925,14 +960,13 @@ function cmdLoop(args: string[]): number {
925
960
  function cmdAssemble(args: string[]): number {
926
961
  const flags = new Map<string, string>();
927
962
  let propose = false;
928
- const valued = ['--source', '--full', '--head', '--config', '--out', '--seam', '--project'];
929
963
  for (let i = 0; i < args.length; i++) {
930
964
  const flag = args[i];
931
965
  if (flag === '--propose-plan') {
932
966
  propose = true;
933
967
  continue;
934
968
  }
935
- if (!valued.includes(flag)) return usage(`assemble does not take "${flag}"`);
969
+ if (!takesValued('assemble', flag)) return usage(notTaken('assemble', flag));
936
970
  const value = args[i + 1];
937
971
  if (value === undefined) return usage(`${flag} needs a value`);
938
972
  if (flags.has(flag)) return usage(`${flag} is given twice`);
@@ -968,8 +1002,9 @@ function cmdAssemble(args: string[]): number {
968
1002
  }
969
1003
  }
970
1004
 
971
- /** Parse `--flag value` pairs and bare `--switch`es; a string is a usage error. */
972
- function comfyFlags(args: string[], valued: readonly string[], switches: readonly string[], command: string): { v: Map<string, string>; on: Set<string> } | string {
1005
+ /** Parse `--flag value` pairs and bare `--switch`es, as the command's {@link TAKES} row names them; a string is a usage error. */
1006
+ function comfyFlags(args: string[], command: 'comfy seethrough' | 'comfy paint'): { v: Map<string, string>; on: Set<string> } | string {
1007
+ const switches: readonly string[] = TAKES[command].switches;
973
1008
  const v = new Map<string, string>();
974
1009
  const on = new Set<string>();
975
1010
  for (let i = 0; i < args.length; i++) {
@@ -979,7 +1014,7 @@ function comfyFlags(args: string[], valued: readonly string[], switches: readonl
979
1014
  on.add(flag);
980
1015
  continue;
981
1016
  }
982
- if (!valued.includes(flag)) return `${command} does not take "${flag}"; it takes ${[...valued, ...switches].join(', ')}`;
1017
+ if (!takesValued(command, flag)) return notTaken(command, flag);
983
1018
  const value = args[i + 1];
984
1019
  if (value === undefined) return `${flag} needs a value`;
985
1020
  if (v.has(flag)) return `${flag} is given twice`;
@@ -1005,14 +1040,10 @@ function secondsFlag(v: Map<string, string>, flag: string, fallback: number): nu
1005
1040
  return Number.isFinite(n) && n > 0 ? n : `${flag} ${raw} is not a number of seconds above 0`;
1006
1041
  }
1007
1042
 
1008
- const COMFY_TIMING = ['--host', '--wait', '--timeout', '--poll'] as const;
1009
-
1010
1043
  async function cmdComfy(args: string[]): Promise<number> {
1011
1044
  const [sub, ...rest] = args;
1012
1045
  if (sub !== 'seethrough' && sub !== 'paint') return usage(`comfy takes seethrough or paint; got ${sub === undefined ? 'nothing' : `"${sub}"`}`);
1013
- const valued = sub === 'seethrough' ? ['--image', '--out', '--resolution', '--steps', '--seed', '--prefix', ...COMFY_TIMING] : ['--config', '--out', '--seeds', '--seed0', ...COMFY_TIMING];
1014
- const switches = sub === 'seethrough' ? ['--offload', '--lama', '--nf4'] : [];
1015
- const f = comfyFlags(rest, valued, switches, `comfy ${sub}`);
1046
+ const f = comfyFlags(rest, `comfy ${sub}`);
1016
1047
  if (typeof f === 'string') return usage(f);
1017
1048
  const { v, on } = f;
1018
1049
  const wait = secondsFlag(v, '--wait', 1800);
@@ -1075,7 +1106,7 @@ async function cmdComfy(args: string[]): Promise<number> {
1075
1106
  }
1076
1107
 
1077
1108
  function cmdInputs(args: string[]): number {
1078
- const f = flags(args, ['--source', '--config', '--out'], 'inputs');
1109
+ const f = flags(args, 'inputs', ['--source', '--config', '--out']);
1079
1110
  if (typeof f === 'string') return usage(f);
1080
1111
  const source = f.get('--source') as string;
1081
1112
  const out = f.get('--out') as string;
@@ -1107,7 +1138,6 @@ function cmdInputs(args: string[]): number {
1107
1138
  function cmdBuild(args: string[]): number {
1108
1139
  const flags = new Map<string, string>();
1109
1140
  let loop = false;
1110
- const valued = ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges', '--pack-shape', '--requirements', '--idle-keys'];
1111
1141
  for (let i = 0; i < args.length; i++) {
1112
1142
  const flag = args[i];
1113
1143
  if (flag === '--loop') {
@@ -1115,7 +1145,7 @@ function cmdBuild(args: string[]): number {
1115
1145
  loop = true;
1116
1146
  continue;
1117
1147
  }
1118
- if (!valued.includes(flag)) return usage(`build does not take "${flag}"; it takes --loop and ${valued.join(', ')}`);
1148
+ if (!takesValued('build', flag)) return usage(notTaken('build', flag));
1119
1149
  const value = args[i + 1];
1120
1150
  if (value === undefined) return usage(`${flag} needs a value`);
1121
1151
  if (flags.has(flag)) return usage(`${flag} is given twice`);
@@ -1154,7 +1184,7 @@ function cmdBuild(args: string[]): number {
1154
1184
  }
1155
1185
 
1156
1186
  function cmdCompose(args: string[]): number {
1157
- const f = flags(args, ['--scene', '--out'], 'compose', ['--requirements', '--accept']);
1187
+ const f = flags(args, 'compose', ['--scene', '--out']);
1158
1188
  if (typeof f === 'string') return usage(f);
1159
1189
  let bin: string;
1160
1190
  try {
package/docs/AUTHORING.md CHANGED
@@ -1230,8 +1230,10 @@ names it — `[29]` on both examples.
1230
1230
  flat stack of parts differs from the painting by more than 60, the top part takes the
1231
1231
  painting's colour — except where the painting is near-white, which protects the page
1232
1232
  and also every white garment. `--seam silhouette` protects near-white pixels only
1233
- outside the figure. On the demo it lowered recomposite error pixels from 11,050 to
1234
- 9,540 and changed no check bar; on the sample it changed nothing. The default stays
1233
+ outside the figure. On the demo it lowers recomposite error pixels from 10,340 to
1234
+ 8,644 (`assemble`'s `error px > 40`, at v1.5.2); on the sample it changes nothing
1235
+ (4,359 under both). At v0.14.0 the demo's were 11,050 and 9,540, and no check bar
1236
+ changed. The default stays
1235
1237
  the reference's so the examples stay comparable with it.
1236
1238
 
1237
1239
  **The fringe push-back** (issue #119; under either seam rule, no flag). Both places
@@ -1253,9 +1255,9 @@ and a cleared bridge to opaque art is put back, so no part gains an island
1253
1255
  names them. On the examples (default flags, measured against the v0.14.0 build of
1254
1256
  the same inputs): `demo` 1,193 px over 16 parts, recomposite error pixels 11,050 →
1255
1257
  10,340; `sample` 276 px over 15 parts, 4,512 → 4,359; uncovered error pixels and
1256
- holes unchanged on both, `check` 9 of 9 on both. The `--seam` and `--project`
1257
- comparisons in this section and the `SETUP_POSE_VS_SOURCE` example figures in §7
1258
- were measured before it, at v0.14.0.
1258
+ holes unchanged on both, `check` 9 of 9 on both. The `--project` comparison in
1259
+ this section and the `SETUP_POSE_VS_SOURCE` example figures in §7 were measured
1260
+ before it, at v0.14.0; the `--seam` figures above are v1.5.2's.
1259
1261
 
1260
1262
  **Page edges.** `rig`, `check` and `build` pack through `rigc build --pack
1261
1263
  --page-edges <v>`, and hand the value to rigc verbatim. The default is `free`,
@@ -1685,7 +1687,7 @@ See-through with another seed.
1685
1687
  | `CHECK_RIGC_GREEN` | a rigc step failed; its line is quoted | as `RIG_RIGC_GREEN` |
1686
1688
  | `CHECK_LOOP_LAST_FRAME_AT_DURATION` | the idle's last frame does not sit at `duration` | `motion.duration` — a whole number of 1/12 s |
1687
1689
  | `CHECK_LOOP_CLOSES` | frame 0 and the frame at `duration` differ (max and first pixel quoted); with a physics constraint, the frames of the rig with every physics constraint left out, and the object says so (§7, *loop*) | a track whose last key is not its first |
1688
- | `CHECK_SEAM_WITHIN_BAR` | the setup pose does not reproduce the flat stack of parts | read the `seam pair` lines and `check/seam/` pictures (§7): the pair, its boxes and the pixels counted. Usually a region or mesh placed off its part; compare with `recomposite_rig.png`. A pair of one layer cut in two with `differing_copies_px` is the cut line: move the polygon's edge, not `overlap` |
1690
+ | `CHECK_SEAM_WITHIN_BAR` | the setup pose does not reproduce the flat stack of parts | read the `seam pair` lines and `check/seam/` pictures (§7; magenta = a counted pixel, cyan = the box round them): the pair, its boxes and the pixels counted. Usually a region or mesh placed off its part; compare with `recomposite_rig.png`. A pair of one layer cut in two with `differing_copies_px` is the cut line: move the polygon's edge, not `overlap` |
1689
1691
  | `CHECK_BREATH_VISIBLE` | the torso (`topwear`), rendered alone, barely moves over the idle — or the feet (`footwear`), rendered alone, move at all | the chest's breath tracks (`motion.tracks` on `chest`), or the torso mesh's `segments`; for the feet, the bone their region rides (`regions.<part>`, `root` in both examples) |
1690
1692
  | `CHECK_BLINK_NO_HOLE` | with the blink held shut, the eyewhite box shows the page where the open eye had art | the layer under the eye: the `face` part has no art there. Take the face from the other run, or add a part under the eye; `motion.blink.squash` only hides the hole less |
1691
1693
  | `CHECK_CHAIN_LAG` | a rotate track leads (or does not lag) the keyed bone above it, or a chain link swings less than the link above | that chain track's `phase`/`lag` (a positive `lag`, a child `phase` above its parent's) or its `amps` (non-decreasing toward the tip) |
@@ -1753,7 +1755,19 @@ assemble*, at the end of this section):
1753
1755
  `A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` on the packed pass, 16 not in `spine`; `CH09`
1754
1756
  holds it on every fetched example);
1755
1757
  - **seam**: the setup-pose render against the flat composite of `parts/`: mean
1756
- max-channel |d| ≤ 1.0 of 255, and at most 50 pixels over 40. The bar reads one
1758
+ max-channel |d| ≤ 1.0 of 255, and at most 50 **rig** pixels over 40 — the
1759
+ frame pixels over 40 divided by s², `s` the frame scale the `seam:` line
1760
+ prints and `check.json`'s `seam_frame_scale` holds (issue #216). The frame
1761
+ holds s² pixels per rig pixel, so a stack that parts from the render over a
1762
+ fixed area of the rig reads s² times as many frame pixels: on the public
1763
+ `sample` a part moved 2 rig px read 400, 891 and 1,560 frame px over 40 at
1764
+ s = 1.0, 1.5 and 2.0, and 400, 396 and 390 rig px; the sample as built read
1765
+ 3, 3 and 7 frame px [measured, issue #216]. Divided by s² the bar reads the
1766
+ same rig at every scale, and at s = 1 it is the reference's count. The mean
1767
+ is a share of the frame and needs no such division (0.242, 0.188 and 0.226 on
1768
+ the sample as built; 0.353, 0.305 and 0.340 with the part moved).
1769
+ `seam_px_over_40` and `seam_px_over_80` stay frame pixels, and so does each
1770
+ pair's count below. The bar reads one
1757
1771
  frame, the setup-pose still, never the idle. When it fails (or when `check
1758
1772
  --seam-pairs` asks, pass or fail), the pixels over 40 are split by the pair of
1759
1773
  parts that answers for each (issue #202): the two parts drawn last among those
@@ -1761,7 +1775,8 @@ assemble*, at the end of this section):
1761
1775
  sample reads for that frame pixel, lower first, `(no art)` standing in where
1762
1776
  fewer than two do. Each pair is a console line `seam pair <i> of <n>: "<a>" /
1763
1777
  "<b>" at the setup pose: <count> px over 40 (<count> over 80), frame box x
1764
- <x0>..<x1>, y <y0>..<y1>, rig box …, picture seam/<i>_<a>_<b>.png`, worst first,
1778
+ <x0>..<x1>, y <y0>..<y1>, rig box …, picture seam/<i>_<a>_<b>.png` (in the
1779
+ picture, **magenta = a counted pixel**, cyan = the box round them), worst first,
1765
1780
  and a row of `check.json`'s `seam_pairs` (`parts`, `px_over_40`, `px_over_80`,
1766
1781
  `frame_box` in frame pixels, `rig_box` in the rig pixels of `parts.json` and
1767
1782
  `cuts[].polygon`, `picture`); `CHECK_SEAM_WITHIN_BAR`'s line quotes the worst.
package/docs/STABILITY.md CHANGED
@@ -158,7 +158,7 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
158
158
  | `parts.json`, `parts/<name>.png` | `assemble`, `build` | one record per part: `from`, 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` |
159
159
  | `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 |
160
160
  | `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`; 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`); `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) |
161
- | `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), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `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`, `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 |
161
+ | `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_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`, `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 |
162
162
  | `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections and `notes` |
163
163
  | `basis.json` | `propose` | spec `spine-parts-basis/1` |
164
164
  | the scene report | `compose` | spec `spine-parts-scene-report/1` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rig-parts",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
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": {
@@ -125,7 +125,7 @@ it and the order as `scene.json` records it (AUTHORING §7, *Composing several
125
125
  characters*).
126
126
 
127
127
  `--seam silhouette` repairs more of the seam on white garments than the default
128
- `near-white` (the demo: 11,050 → 9,540 recomposite error pixels); the default is
128
+ `near-white` (the demo: 10,340 → 8,644 recomposite error pixels); the default is
129
129
  the reference implementation's rule. `--project visible` lets thin visible parts
130
130
  (lashes, brows, irises) take the painting's pixels, which the default `core` rule
131
131
  erodes away. Name both rules you used in your report, and quote the assemble
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, causeLines, 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, REPORTED_LINES, type ReportedLine, type RigcRunner, runCheck, SEAM_DIR, SEAM_MEAN_BAR, seamPairText, SOURCE_LINE, SEAM_PX_BAR, SEAM_PX_LEVEL, SEAM_PX_LEVEL_HIGH, SPINEBOY_YARDSTICK, STILL_DIR } from './check.ts';
73
+ import { BARS, besideIdleLine, causeLines, 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, 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';
@@ -981,7 +981,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
981
981
  else {
982
982
  log(
983
983
  ` seam: setup pose at ${r.seamViewport.pixelWidth}x${r.seamViewport.pixelHeight}, scale ${r.seamViewport.scale.toFixed(4)}: mean |d| ${fig.seam_mean} (<= ${SEAM_MEAN_BAR.toFixed(1)}), ` +
984
- `${fig.seam_px_over_40} px over ${SEAM_PX_LEVEL} (<= ${SEAM_PX_BAR}), ${fig.seam_px_over_80} px over ${SEAM_PX_LEVEL_HIGH} (reported)`,
984
+ `${fig.seam_px_over_40} px over ${SEAM_PX_LEVEL}, ${fig.seam_frame_scale === null || fig.seam_px_over_40 === null ? '' : seamRigPxText(fig.seam_px_over_40, fig.seam_frame_scale)} rig px (the count / s²; <= ${SEAM_PX_BAR}), ${fig.seam_px_over_80} px over ${SEAM_PX_LEVEL_HIGH} (reported)`,
985
985
  );
986
986
  const pairs = fig.seam_pairs ?? [];
987
987
  pairs.forEach((row, i) => log(` seam pair ${i + 1} of ${pairs.length}: ${seamPairText(row)}`));
package/src/check.ts CHANGED
@@ -153,7 +153,17 @@ import { type ScaleTranslate, warpAffine } from './raster/warp.ts';
153
153
 
154
154
  /** Seam: mean max-channel |d| over the frame, in levels of 255, at most this. */
155
155
  export const SEAM_MEAN_BAR = 1.0;
156
- /** Seam: pixels whose max-channel |d| exceeds {@link SEAM_PX_LEVEL}, at most this many. */
156
+ /**
157
+ * Seam: pixels whose max-channel |d| exceeds {@link SEAM_PX_LEVEL}, at most
158
+ * this many **rig** pixels — the frame count divided by s², s the frame scale
159
+ * the setup-pose still renders at (issue #216). The frame holds s² pixels per
160
+ * rig pixel, so a stack that parts from the render over a fixed area of the
161
+ * rig reads s² times as many frame pixels: on the public sample a part moved
162
+ * 2 rig px read 400, 891 and 1,560 frame px over 40 at s = 1.0, 1.5 and 2.0,
163
+ * and 400, 396 and 390 divided by s²; the uncut sample 3, 3 and 7 frame px
164
+ * [measured for issue #216, a scratch run, not tracked]. At s = 1 the bar is
165
+ * the reference's frame count.
166
+ */
157
167
  export const SEAM_PX_BAR = 50;
158
168
  export const SEAM_PX_LEVEL = 40;
159
169
  /** Seam: the second, reported-only level. */
@@ -1372,9 +1382,29 @@ function round3(x: number): number {
1372
1382
  return Number(x.toFixed(3));
1373
1383
  }
1374
1384
 
1375
- /** The seam bar, on the figures as check.json writes them: the mean (rounded to three places) over {@link SEAM_MEAN_BAR}, or more than {@link SEAM_PX_BAR} pixels over {@link SEAM_PX_LEVEL}. */
1376
- function seamFails(mean: number, over40: number): boolean {
1377
- return mean > SEAM_MEAN_BAR || over40 > SEAM_PX_BAR;
1385
+ /** Python's `round(x, 6)` for the frame scale written: to six places, on the value's exact decimal expansion. */
1386
+ function round6(x: number): number {
1387
+ return Number(x.toFixed(6));
1388
+ }
1389
+
1390
+ /** The seam's count in rig pixels (issue #216): the frame pixels over {@link SEAM_PX_LEVEL} divided by the square of the frame scale. */
1391
+ export function seamRigPx(over40: number, scale: number): number {
1392
+ return over40 / (scale * scale);
1393
+ }
1394
+
1395
+ /** {@link seamRigPx} as the console and the refusal print it, to two places. */
1396
+ export function seamRigPxText(over40: number, scale: number): string {
1397
+ return seamRigPx(over40, scale).toFixed(2);
1398
+ }
1399
+
1400
+ /**
1401
+ * The seam bar, on the figures as check.json writes them: the mean (rounded
1402
+ * to three places) over {@link SEAM_MEAN_BAR}, or more than
1403
+ * {@link SEAM_PX_BAR} rig pixels over {@link SEAM_PX_LEVEL} — the frame count
1404
+ * over the square of the written frame scale (issue #216).
1405
+ */
1406
+ function seamFails(mean: number, over40: number, scale: number): boolean {
1407
+ return mean > SEAM_MEAN_BAR || seamRigPx(over40, scale) > SEAM_PX_BAR;
1378
1408
  }
1379
1409
 
1380
1410
  /** A seam row as the console prints it: the pair, the frame, the counts, both boxes and the picture. */
@@ -1536,10 +1566,16 @@ export interface CheckFigures {
1536
1566
  * read from. Reported, not judged; a rig without one writes the keys it always wrote.
1537
1567
  */
1538
1568
  loop_physics?: LoopPhysics[];
1539
- /** The three seam figures: null when there is no parts.json (issue #77); `skipped.seam` then says why. */
1569
+ /** The seam figures: null when there is no parts.json (issue #77); `skipped.seam` then says why. The two counts are frame pixels. */
1540
1570
  seam_mean: number | null;
1541
1571
  seam_px_over_40: number | null;
1542
1572
  seam_px_over_80: number | null;
1573
+ /**
1574
+ * The frame scale s the setup-pose still rendered at (rigc's viewport
1575
+ * scale, to six places), issue #216: the seam bar reads `seam_px_over_40`
1576
+ * divided by its square ({@link seamRigPx}). Null with the other three.
1577
+ */
1578
+ seam_frame_scale: number | null;
1543
1579
  /**
1544
1580
  * Written only when the seam bar fails, or when it was asked for (`check
1545
1581
  * --seam-pairs`), issue #202: the counted pixels split by the pair of parts
@@ -1600,7 +1636,9 @@ export function barsNotMet(fig: Readonly<Record<string, unknown>>): string[] {
1600
1636
  const out: string[] = [];
1601
1637
  if (fig.gate_spine_html_green !== true) out.push('gate');
1602
1638
  if (typeof fig.loop_max_diff === 'number' && fig.loop_max_diff !== LOOP_MAX_BAR) out.push('loop');
1603
- if (typeof fig.seam_mean === 'number' && typeof fig.seam_px_over_40 === 'number' && seamFails(fig.seam_mean, fig.seam_px_over_40)) out.push('seam');
1639
+ // A check.json written before issue #216 has no seam_frame_scale; it is read by the frame count, the bar it was written under.
1640
+ const scale = typeof fig.seam_frame_scale === 'number' ? fig.seam_frame_scale : 1;
1641
+ if (typeof fig.seam_mean === 'number' && typeof fig.seam_px_over_40 === 'number' && seamFails(fig.seam_mean, fig.seam_px_over_40, scale)) out.push('seam');
1604
1642
  for (const n of JUDGEMENT_LINES) {
1605
1643
  const l = fig[n];
1606
1644
  if (typeof l === 'object' && l !== null && (l as { status?: unknown }).status === 'FAIL') out.push(n);
@@ -1895,7 +1933,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
1895
1933
  if (inp.parts !== null) {
1896
1934
  const composite = flatComposite(inp.parts, inp.partsDir, stillSet.background);
1897
1935
  seam = seamFigures(composite, stillSet.frames[0].image, inp.stage, stillSet.viewport, stillSet.background);
1898
- if (seamPairsAsked || seamFails(round3(seam.mean), seam.over40)) {
1936
+ if (seamPairsAsked || seamFails(round3(seam.mean), seam.over40, round6(stillSet.viewport.scale))) {
1899
1937
  const placed = inp.parts.parts.map((p) => ({ name: p.name, from: p.from, x: p.x, y: p.y, image: readPng(join(inp.partsDir, `${p.name}.png`)) }));
1900
1938
  const split = seamPairRows(seam, placed, inp.parts.rig_size, stillSet.viewport);
1901
1939
  mkdirSync(seamDir, { recursive: true });
@@ -1955,6 +1993,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
1955
1993
  seam_mean: seam === null ? null : round3(seam.mean),
1956
1994
  seam_px_over_40: seam === null ? null : seam.over40,
1957
1995
  seam_px_over_80: seam === null ? null : seam.over80,
1996
+ seam_frame_scale: seam === null || seamViewport === null ? null : round6(seamViewport.scale),
1958
1997
  ...(seamPairs === null ? {} : { seam_pairs: seamPairs }),
1959
1998
  BREATH_VISIBLE: breath,
1960
1999
  BLINK_NO_HOLE: blink,
@@ -1978,13 +2017,13 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
1978
2017
  detail: `max |d| ${loop.max}/255, first at pixel ${loop.x},${loop.y} of ${loopSet.viewport.pixelWidth}x${loopSet.viewport.pixelHeight}; ${LOOP_MAX_BAR} is required — the idle's last key must equal its first`,
1979
2018
  });
1980
2019
  }
1981
- if (figures.seam_mean !== null && figures.seam_px_over_40 !== null && seamFails(figures.seam_mean, figures.seam_px_over_40)) {
2020
+ if (figures.seam_mean !== null && figures.seam_px_over_40 !== null && figures.seam_frame_scale !== null && seamFails(figures.seam_mean, figures.seam_px_over_40, figures.seam_frame_scale)) {
1982
2021
  const worst = seamPairs === null || seamPairs.length === 0 ? null : seamPairs[0];
1983
2022
  barProblems.push({
1984
2023
  code: 'CHECK_SEAM_WITHIN_BAR',
1985
2024
  object: 'the setup-pose render vs the flat composite of parts/',
1986
2025
  detail:
1987
- `mean |d| ${figures.seam_mean}/255 and ${figures.seam_px_over_40} px over ${SEAM_PX_LEVEL} (${figures.seam_px_over_80} over ${SEAM_PX_LEVEL_HIGH}); mean <= ${SEAM_MEAN_BAR.toFixed(1)} and <= ${SEAM_PX_BAR} px over ${SEAM_PX_LEVEL} are required` +
2026
+ `mean |d| ${figures.seam_mean}/255 and ${figures.seam_px_over_40} frame px over ${SEAM_PX_LEVEL} (${figures.seam_px_over_80} over ${SEAM_PX_LEVEL_HIGH}) at frame scale ${figures.seam_frame_scale}, ${seamRigPxText(figures.seam_px_over_40, figures.seam_frame_scale)} rig px (the count / s²); mean <= ${SEAM_MEAN_BAR.toFixed(1)} and <= ${SEAM_PX_BAR} rig px over ${SEAM_PX_LEVEL} are required` +
1988
2027
  (worst === null
1989
2028
  ? `; no pixel is over ${SEAM_PX_LEVEL}, so the mean alone fails and there is no pair to name`
1990
2029
  : `; worst of ${(seamPairs as SeamPairRow[]).length} pair(s): ${seamPairText(worst)} — every pair is a \`seam pair\` line above and a row of check.json's seam_pairs`),