rig-parts 2.0.5 → 2.1.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
@@ -40,7 +40,7 @@ import { meshStatsStage } from './src/meshstats.ts';
40
40
  import { chainsLeftBehind, clothConfigEdits, editPaths, plainCount, previousCount, writtenClothConfig } from './src/clothconfig.ts';
41
41
  import { INPUT_CHECK_FILE, INPUT_CHECK_SPEC, PROVENANCE_FILE, REMESH_SPEC, remeshStage } from './src/remesh.ts';
42
42
  import { RETUNE_FILE, RETUNE_SPEC, retuneStage } from './src/retune.ts';
43
- import { checkPainting } from './src/paintcheck.ts';
43
+ import { checkPainting, DEFAULT_PAINTING_FORM, isPaintingForm, PAINTING_FORMS, paintingFormOf } from './src/paintcheck.ts';
44
44
  import { readPng, writePng } from './src/raster/png.ts';
45
45
  import { REQUIREMENTS_SENTENCE } from './src/requirements.ts';
46
46
  import { composeStage, IMAGE_SEP, PLATE, PROVENANCES, SCENE_REPORT_FILE, SCENE_REPORT_SPEC, SCENE_SPEC } from './src/scene.ts';
@@ -947,20 +947,28 @@ usage:
947
947
  line and ${SCENE_REPORT_FILE} name what was accepted, and the run exits 1.
948
948
  Exit 0 when the check is PASS and nothing was accepted, 1 otherwise.
949
949
 
950
- rig-parts painting --keypoints <keypoints.json> [--person <id>] <painting.png>
950
+ rig-parts painting --keypoints <keypoints.json> [--person <id>] [--form standee|illustration] <painting.png>
951
951
  Before See-through: read the painting against its body-18 joints (the
952
952
  file propose --keypoints reads, measured on this painting) and refuse,
953
- by name, what a rig cannot be cut from. Background is near-white
954
- (every channel above 235) or unpainted (alpha 8 or less) ground
955
- connected to the edge; the figure is the rest; a gap is one background
956
- pixel on the line between two joints. PAINTING_BACKGROUND_PLAIN (a
957
- border pixel that is not ground), PAINTING_JOINTS_STATED (a wrist,
958
- knee or ankle not observed, or a hip neither observed nor occluded
959
- with a position), PAINTING_JOINT_ON_FIGURE (an observed joint, or an
960
- occluded hip, on the background), PAINTING_ARM_CLEAR (no gap from a
961
- wrist to its hip), PAINTING_HANDS_APART (none between the wrists),
962
- PAINTING_LEGS_APART (none between the knees, or the ankles). Prints the
963
- figure, every joint and every gap with its count; writes nothing.
953
+ by name, what a rig cannot be cut from. --form defaults to
954
+ ${DEFAULT_PAINTING_FORM} (${PAINTING_FORMS.join(', ')}). Under standee,
955
+ background is near-white (every channel above 235) or unpainted (alpha 8
956
+ or less) ground connected to the edge; the figure is the rest; a gap is
957
+ one background pixel on the line between two joints.
958
+ PAINTING_BACKGROUND_PLAIN (a border pixel that is not ground),
959
+ PAINTING_JOINTS_STATED (a wrist, knee or ankle not observed, or a hip
960
+ neither observed nor occluded with a position), PAINTING_JOINT_ON_FIGURE
961
+ (an observed joint, or an occluded hip, on the background),
962
+ PAINTING_ARM_CLEAR (no gap from a wrist to its hip),
963
+ PAINTING_HANDS_APART (none between the wrists), PAINTING_LEGS_APART
964
+ (none between the knees, or the ankles). Under --form illustration (a
965
+ framed illustration with a painted room or a figure cut by the crop),
966
+ the source, keypoint canvas and bounds checks still run,
967
+ PAINTING_JOINTS_STATED requires at least one joint with a position and
968
+ PAINTING_JOINT_ON_FIGURE requires every stated joint's pixel to be
969
+ painted (alpha above 8); the standee border, 8-joint and gap rules do
970
+ not apply and print SKIP with the reason, never PASS. Prints the
971
+ figure, every joint and every gap or SKIP line; writes nothing.
964
972
  docs/PAINTING.md says why each one matters. Exit 0 PASS, 1 refused.
965
973
 
966
974
  rig-parts guide <RULE | §<n> | §<n>.<m> | §<n>.<m>.<k>>
@@ -1108,7 +1116,7 @@ const TAKES = {
1108
1116
  'mesh-stats': { valued: ['--config', '--parts'], switches: [] },
1109
1117
  remesh: { valued: ['--rig', '--config', '--parts', '--part', '--mesh', '--delta', '--out', '--requirements', '--idle-keys', '--page-edges', '--pack-shape', '--input-check'], switches: ['--record-input-check'] },
1110
1118
  retune: { valued: ['--rig', '--config', '--parts', '--out', '--requirements', '--idle-keys', '--page-edges', '--pack-shape', '--input-check'], switches: ['--record-input-check'] },
1111
- painting: { valued: ['--keypoints', '--person'], switches: [] },
1119
+ painting: { valued: ['--keypoints', '--person', '--form'], switches: [] },
1112
1120
  } as const satisfies Record<string, { valued: readonly string[]; switches: readonly string[] }>;
1113
1121
 
1114
1122
  type Command = keyof typeof TAKES;
@@ -2153,8 +2161,9 @@ function cmdRetune(args: string[]): number {
2153
2161
  }
2154
2162
 
2155
2163
  /**
2156
- * `painting --keypoints <k.json> [--person <id>] <painting.png>` (issue #350): what the painting must be before
2157
- * See-through runs on it, read off its pixels and the body-18 joints `propose --keypoints` reads. Writes nothing.
2164
+ * `painting --keypoints <k.json> [--person <id>] [--form standee|illustration] <painting.png>` (issues #350, #931):
2165
+ * what the painting must be before See-through runs on it, read off its pixels and the body-18 joints
2166
+ * `propose --keypoints` reads. Writes nothing.
2158
2167
  */
2159
2168
  function cmdPainting(args: string[]): number {
2160
2169
  const flags = new Map<string, string>();
@@ -2174,6 +2183,8 @@ function cmdPainting(args: string[]): number {
2174
2183
  }
2175
2184
  const kpPath = flags.get('--keypoints');
2176
2185
  if (kpPath === undefined) return usage('painting needs --keypoints <file>, the body-18 joints of the figure in painting px (the file propose --keypoints reads)');
2186
+ const form = paintingFormOf(flags.get('--form'));
2187
+ if (!isPaintingForm(form)) return usage(form);
2177
2188
  if (paths.length !== 1) return usage(`painting takes one painting path; got ${paths.length}`);
2178
2189
  const source = paths[0];
2179
2190
  try {
@@ -2187,11 +2198,15 @@ function cmdPainting(args: string[]): number {
2187
2198
  const kp = loadKeypoints(kpPath);
2188
2199
  checkImageSize(kp, painting.width, painting.height, kpPath, source);
2189
2200
  const person = choosePerson(kp, flags.get('--person'), kpPath);
2190
- console.log(`rig-parts painting: ${source}, joints from ${kpPath} person "${person.id}" (source: ${kp.source})`);
2191
- const r = checkPainting(painting, person, source, kpPath);
2201
+ console.log(`rig-parts painting${form === 'illustration' ? ' (--form illustration)' : ''}: ${source}, joints from ${kpPath} person "${person.id}" (source: ${kp.source})`);
2202
+ const r = checkPainting(painting, person, source, kpPath, form);
2192
2203
  for (const l of r.lines) console.log(l);
2193
2204
  if (r.problems.length > 0) throw new PartsError(r.problems);
2194
- console.log(' PASS the painting holds every requirement this check reads (docs/PAINTING.md); what only an eye reads is listed there');
2205
+ console.log(
2206
+ form === 'illustration'
2207
+ ? ' PASS the painting holds the framed-illustration checks that apply (source, keypoint canvas and bounds, stated joints on painted pixels); standee background and gap rules reported SKIP above, not passed (docs/PAINTING.md)'
2208
+ : ' PASS the painting holds every requirement this check reads (docs/PAINTING.md); what only an eye reads is listed there',
2209
+ );
2195
2210
  return EXIT_OK;
2196
2211
  } catch (err) {
2197
2212
  return printRefusal(err);
package/docs/AUTHORING.md CHANGED
@@ -3657,7 +3657,7 @@ existing hardlinks are refused before artifact output; the ordinary mode names
3657
3657
  | `compose --scene <scene.json> --out [--requirements] [--accept]` | bind several characters' green builds into one rig (or, under `--accept`, builds failing only the bars named — for looking, not shipping): every name prefixed `<id>:` under one shared root, each character's offset added at its first-level bones, an optional background plate (its provenance recorded, judged by nothing) drawn first, and the draw order between the characters' slots as the scene file declares it — nothing inferred, nothing scaled; gated through rig-c as `rig` is, then checked ([AUTHORING §7](AUTHORING.md), *Composing several characters*) |
3658
3658
  | `mesh-draft --config <json> --parts <dir> --out <dir>` | draft contour meshes for configured lattices, keeping each only where it holds against its lattice through rig-c; write the revised config and measurement report |
3659
3659
  | `mesh-stats --config <json> --parts <dir>` | print each configured mesh's vertex and triangle counts and their total through the rig stage's construction; write nothing |
3660
- | `painting --keypoints <json> [--person <id>] <png>` | before See-through, read the painting against its declared joints and refuse named background, joint or limb-gap problems; write nothing |
3660
+ | `painting --keypoints <json> [--person <id>] [--form standee\|illustration] <png>` | before See-through, read the painting against its declared joints (`standee` by default, or `illustration` for a framed illustration where standee border, 8-joint and gap rules report `SKIP`) and refuse named problems; write nothing |
3661
3661
  | `remesh --rig <dir> --config <json> --parts <dir> --part <name> --out <dir>` | with exactly one of `--mesh <entry.json>` or `--delta <mesh-edit.json>`, re-fit one part of a gated rig into a fresh revision, preserving the input |
3662
3662
  | `retune --rig <dir> --config <json> --parts <dir> --out <dir>` | splice the config's constraints and idle onto a gated rig in a fresh revision, carrying its attachments over as bytes |
3663
3663
  | `guide <RULE \| §<n> \| §<n>.<m> \| §<n>.<m>.<k>>` | print the one row of [docs/AUTHORING.md](AUTHORING.md) that names a rule (a refusal and the bar it enforces, `CHECK_<LINE>` and `<LINE>`, print together), or one section, `###` subsection or `####` under it, read from the installed file when it runs |
@@ -4609,21 +4609,26 @@ See-through with another seed.
4609
4609
 
4610
4610
  ### painting
4611
4611
 
4612
- `rig-parts painting --keypoints <k.json> <painting.png>` (issue #350) reads the painting
4613
- before See-through runs on it. *Background* is ground (alpha 8 or less, or every channel
4612
+ `rig-parts painting --keypoints <k.json> [--person <id>] [--form standee|illustration] <painting.png>` (issues #350, #931) reads the painting
4613
+ before See-through runs on it. Under `--form standee` (the default), *background* is ground (alpha 8 or less, or every channel
4614
4614
  above 235) connected to the painting's edge; the *figure* is the rest; a *gap* is one
4615
- background pixel on the straight line between two joints. [PAINTING.md](PAINTING.md)
4615
+ background pixel on the straight line between two joints. Under `--form illustration`
4616
+ (a framed illustration with a painted room or a figure cut by the crop), the source
4617
+ and keypoint checks still run, `PAINTING_JOINTS_STATED` requires at least one joint
4618
+ with a position, `PAINTING_JOINT_ON_FIGURE` checks that every stated joint's pixel is
4619
+ painted (alpha above 8), and the standee border, 8-joint and gap rules report `SKIP`
4620
+ with their reason, never `PASS`. [PAINTING.md](PAINTING.md)
4616
4621
  says why each requirement exists and what breaks without it.
4617
4622
 
4618
4623
  | rule | means | change |
4619
4624
  | --- | --- | --- |
4620
4625
  | <a id="PAINTING_SOURCE_PRESENT"></a><a id="PAINTING_SOURCE_DECODES"></a>`PAINTING_SOURCE_PRESENT`, `PAINTING_SOURCE_DECODES` | the painting is missing or not a PNG | the path |
4621
- | <a id="PAINTING_BACKGROUND_PLAIN"></a>`PAINTING_BACKGROUND_PLAIN` | a border pixel is neither near-white nor unpainted: scenery, a floor, a gradient or a vignette reaches the edge, or the figure is cut by the frame; the detail counts them and names the first | a painting on a plain light background with the whole figure inside the frame (PAINTING.md, *Background*) |
4622
- | <a id="PAINTING_JOINTS_STATED"></a>`PAINTING_JOINTS_STATED` | a wrist, knee or ankle is `occluded` or `missing` in the keypoint file, or a hip has no position: the gap rules read those eight joints, and a hand or foot the painting does not show is a limb nothing can rig. A hip may be `occluded` (a skirt hides it on most clothed figures, issue #372) when its arm's gap is **well defined**: it carries a position — inside the image, which the keypoint reader holds (`KEYPOINTS_POSITION_INSIDE`), so the wrist-to-hip line's every sample is a pixel of the painting — and that pixel is figure (`PAINTING_JOINT_ON_FIGURE`). A hip `occluded` with no position, or `missing`, is refused: there is no line to read | a painting that shows it; or the joint, if the estimator missed one the painting shows; for a hip under a skirt, `occluded` with its estimated position (PAINTING.md, *An occluded hip*) |
4623
- | <a id="PAINTING_JOINT_ON_FIGURE"></a>`PAINTING_JOINT_ON_FIGURE` | an observed joint's pixel, or an occluded hip's, is background. For an occluded hip the detail adds that the arm's gap would be the estimate's own background, not one between the arm and the body | the keypoint file (measured on this painting?), or the painting; an occluded hip's estimate, moved onto the figure it is hidden under |
4624
- | <a id="PAINTING_ARM_CLEAR"></a>`PAINTING_ARM_CLEAR` | the line from a wrist to the hip of its side crosses no background: the arm is painted against the body. Hands that meet close a loop round the torso, so this fires with `PAINTING_HANDS_APART` on both sides | a painting with the arm held away from the body (PAINTING.md, *Arms*) |
4625
- | <a id="PAINTING_HANDS_APART"></a>`PAINTING_HANDS_APART` | the line between the two wrists crosses no background: hands clasped or touching | a painting with the hands apart and open (PAINTING.md, *Hands*) |
4626
- | <a id="PAINTING_LEGS_APART"></a>`PAINTING_LEGS_APART` | the line between the knees, or between the ankles, crosses no background: the legs are one column there (a long skirt over the knees is refused here too) | a painting with the legs apart and a visible gap at the knees and the ankles (PAINTING.md, *Legs*) |
4626
+ | <a id="PAINTING_BACKGROUND_PLAIN"></a>`PAINTING_BACKGROUND_PLAIN` | under `--form standee`, a border pixel is neither near-white nor unpainted: scenery, a floor, a gradient or a vignette reaches the edge, or the figure is cut by the frame; the detail counts them and names the first (`SKIP` under `--form illustration`) | a painting on a plain light background with the whole figure inside the frame, or `--form illustration` for a framed illustration (PAINTING.md, *Background* and *Framed illustrations*) |
4627
+ | <a id="PAINTING_JOINTS_STATED"></a>`PAINTING_JOINTS_STATED` | under `--form standee`, a wrist, knee or ankle is `occluded` or `missing` in the keypoint file, or a hip has no position: the gap rules read those eight joints, and a hand or foot the painting does not show is a limb nothing can rig. A hip may be `occluded` (a skirt hides it on most clothed figures, issue #372) when its arm's gap is **well defined**: it carries a position — inside the image, which the keypoint reader holds (`KEYPOINTS_POSITION_INSIDE`), so the wrist-to-hip line's every sample is a pixel of the painting — and that pixel is figure (`PAINTING_JOINT_ON_FIGURE`). A hip `occluded` with no position, or `missing`, is refused: there is no line to read. Under `--form illustration`, the eight standee gap joints report `SKIP`, and the rule refuses when the person states 0 joints with a position (`propose --keypoints` places bones only from joints with a position) | a painting that shows it; or the joint, if the estimator missed one the painting shows; for a hip under a skirt, `occluded` with its estimated position (PAINTING.md, *An occluded hip* and *Framed illustrations*) |
4628
+ | <a id="PAINTING_JOINT_ON_FIGURE"></a>`PAINTING_JOINT_ON_FIGURE` | under `--form standee`, an observed joint's pixel, or an occluded hip's, is background (for an occluded hip the detail adds that the arm's gap would be the estimate's own background, not one between the arm and the body). Under `--form illustration`, the near-white border flood reports `SKIP`, and any joint with a position whose pixel is unpainted (alpha 8 or less) is refused | the keypoint file (measured on this painting?), or the painting; an occluded hip's estimate, moved onto the figure it is hidden under |
4629
+ | <a id="PAINTING_ARM_CLEAR"></a>`PAINTING_ARM_CLEAR` | under `--form standee`, the line from a wrist to the hip of its side crosses no background: the arm is painted against the body. Hands that meet close a loop round the torso, so this fires with `PAINTING_HANDS_APART` on both sides (`SKIP` under `--form illustration`) | a painting with the arm held away from the body (PAINTING.md, *Arms*) |
4630
+ | <a id="PAINTING_HANDS_APART"></a>`PAINTING_HANDS_APART` | under `--form standee`, the line between the two wrists crosses no background: hands clasped or touching (`SKIP` under `--form illustration`) | a painting with the hands apart and open (PAINTING.md, *Hands*) |
4631
+ | <a id="PAINTING_LEGS_APART"></a>`PAINTING_LEGS_APART` | under `--form standee`, the line between the knees, or between the ankles, crosses no background: the legs are one column there (a long skirt over the knees is refused here too; `SKIP` under `--form illustration`) | a painting with the legs apart and a visible gap at the knees and the ankles (PAINTING.md, *Legs*) |
4627
4632
 
4628
4633
  ### The optional ComfyUI adapter (`comfy paint`, `comfy seethrough`)
4629
4634
 
package/docs/PAINTING.md CHANGED
@@ -47,14 +47,16 @@ Each requirement says what the pipeline needs, what fails without it, and whethe
47
47
  ## The check: `rig-parts painting`
48
48
 
49
49
  ```
50
- rig-parts painting --keypoints <keypoints.json> [--person <id>] <painting.png>
50
+ rig-parts painting --keypoints <keypoints.json> [--person <id>] [--form standee|illustration] <painting.png>
51
51
  ```
52
52
 
53
53
  It runs on the CPU, before See-through, on the painting and its body-18 joints:
54
54
  the same keypoint file `propose --keypoints` reads (AUTHORING §3, *A posed
55
55
  figure*), from any estimator or written by hand, measured on this painting (its
56
- `width` and `height` are the painting's, `KEYPOINTS_IMAGE_SIZE`). It writes
57
- nothing. It prints the figure, every joint and every gap with its count, then
56
+ `width` and `height` are the painting's, `KEYPOINTS_IMAGE_SIZE`). `--form`
57
+ defaults to `standee` (`standee` or `illustration`). It writes
58
+ nothing. It prints the figure, every joint and every gap (or under
59
+ `--form illustration`, the `SKIP` lines for rules that do not apply), then
58
60
  `PASS` and exit 0, or a `FAIL` line per refusal and exit 1. Each `FAIL` line ends
59
61
  with `(see rig-parts guide <RULE>)`, which prints the rule's row in
60
62
  [AUTHORING §6](AUTHORING.md#6-refusals-the-rule-and-what-has-to-change).
@@ -132,6 +134,63 @@ the joints and the silhouette hold the requirements it reads, not that the
132
134
  painting is good. A shadow or a floor enclosed by the background is figure to it;
133
135
  the border rule sees it only where it reaches the edge.
134
136
 
137
+ ### Framed illustrations (`--form illustration`, issue #931)
138
+
139
+ The default `--form standee` checks a full-body figure on a plain near-white or
140
+ unpainted ground, with gaps for articulated limb cuts (`config.limbs`). Measured
141
+ on the five 1664x2432 public examples (`demo`, `scarf`, `reference-demo`,
142
+ `reference-sample`, `wind` — all five of which build and pass `rig-parts check`),
143
+ only `wind` (the one example that declares `config.limbs`) passes the standee gap
144
+ rules; the four non-limb standees fail `PAINTING_ARM_CLEAR` and
145
+ `PAINTING_HANDS_APART` (on `demo` and `scarf` the wrists are 840 px and 392 px
146
+ apart on opposite sides of the figure, yet the straight wrist-to-wrist line
147
+ crosses the torso in between, so 0 of 841 and 0 of 393 sampled pixels are
148
+ background), and three (`demo`, `reference-demo`, `reference-sample`) fail
149
+ `PAINTING_LEGS_APART`:
150
+
151
+ | public example (1664x2432) | `borderNotGround` | `r_wrist..r_hip` | `l_wrist..l_hip` | `r_wrist..l_wrist` | `r_knee..l_knee` | `r_ankle..l_ankle` | `--form standee` | `--form illustration` |
152
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- |
153
+ | `demo` (`pose.json` x2) | 0 / 8188 | 0 / 345 (`FAIL`) | 0 / 345 (`FAIL`) | 0 / 841 (`FAIL`) | 0 / 129 (`FAIL`) | 0 / 113 (`FAIL`) | `FAIL` (5) | `PASS` (6 `SKIP`) |
154
+ | `scarf` (`stand_sides` x2) | 0 / 8188 | 0 / 121 (`FAIL`) | 0 / 121 (`FAIL`) | 0 / 393 (`FAIL`) | 34 / 129 (`PASS`) | 32 / 113 (`PASS`) | `FAIL` (3) | `PASS` (6 `SKIP`) |
155
+ | `reference-demo` (`stand_clasp` x2) | 0 / 8188 | 0 / 93 (`FAIL`) | 0 / 93 (`FAIL`) | 0 / 57 (`FAIL`) | 0 / 129 (`FAIL`) | 0 / 113 (`FAIL`) | `FAIL` (5) | `PASS` (6 `SKIP`) |
156
+ | `reference-sample` (`stand_clasp` x2) | 0 / 8188 | 0 / 93 (`FAIL`) | 0 / 93 (`FAIL`) | 0 / 57 (`FAIL`) | 0 / 129 (`FAIL`) | 0 / 113 (`FAIL`) | `FAIL` (5) | `PASS` (6 `SKIP`) |
157
+ | `wind` (`keypoints.json`) | 0 / 8188 | 60 / 429 (`PASS`) | 76 / 415 (`PASS`) | 157 / 1011 (`PASS`) | 137 / 261 (`PASS`) | 248 / 351 (`PASS`) | `PASS` | `PASS` (6 `SKIP`) |
158
+
159
+ On a framed illustration — a character in a painted room, or framed at the waist
160
+ or thighs so the figure touches the crop border and lower-body joints lie outside
161
+ the canvas — near-white border flood-fill (`backgroundMask`) fails in both
162
+ directions:
163
+
164
+ 1. **A painted room is not near-white ground.** Non-white room walls at the
165
+ border (`R,G,B <= 235`) are not seeded by `backgroundMask`, so `backgroundPx`
166
+ is 0 across the painted room and `PAINTING_JOINT_ON_FIGURE` cannot tell a room
167
+ wall from the figure before See-through runs.
168
+ 2. **White clothing touching the crop edge floods into the figure.** Measured on
169
+ `demo` cropped at `y = 1230` (1664x1230) and `wind` cropped at `y = 1250`
170
+ (1664x1250) over a painted room (`[80, 70, 65, 255]`): the character's white
171
+ dress/skirt touches the bottom border at `y = 1229` / `1249` with RGB channel
172
+ values above 235, so `backgroundMask` floods 31,806 px into `demo`'s dress
173
+ (covering `r_hip` at `[756, 1224]` RGB `[250, 241, 244]` and `l_hip` at
174
+ `[908, 1224]` RGB `[250, 239, 242]`) and 36,247 px into `wind`'s skirt
175
+ (covering `r_hip` at `[748, 1224]` RGB `[244, 245, 250]` and `l_hip` at
176
+ `[916, 1224]` RGB `[240, 241, 246]`), and `PAINTING_JOINT_ON_FIGURE` falsely
177
+ refuses both hips on the character.
178
+
179
+ Under `--form illustration`, every rule is either kept because it still predicts a
180
+ real downstream failure, or skipped with an explicit `SKIP` line naming the reason
181
+ (never a pass):
182
+
183
+ | rule | under `--form illustration` | why |
184
+ | --- | --- | --- |
185
+ | `PAINTING_SOURCE_PRESENT`, `PAINTING_SOURCE_DECODES` | **applies** | `inputs` and `assemble` read the same PNG; a missing or corrupt file fails every stage |
186
+ | `KEYPOINTS_*` (`KEYPOINTS_FILE_PRESENT`, `KEYPOINTS_IS_JSON`, `KEYPOINTS_SPEC_KNOWN`, `KEYPOINTS_SPACE_STATED`, `KEYPOINTS_IMAGE_SIZE`, `KEYPOINTS_PERSON_CHOSEN`, `KEYPOINTS_POSITION_INSIDE`, …) | **applies** | `propose --keypoints` runs the same reader: a keypoint file measured on an uncropped 1664x2432 canvas fails `KEYPOINTS_IMAGE_SIZE` on a 1664x1230 framed crop, and a joint whose coordinates lie past the crop edge fails `KEYPOINTS_POSITION_INSIDE` |
187
+ | `PAINTING_BACKGROUND_PLAIN` | **`SKIP`** | a painted room or a figure cut by the frame has non-ground border pixels (`5784 / 5784` on `demo` 1664x1230 and `5824 / 5824` on `wind` 1664x1250 over a painted room) |
188
+ | `PAINTING_JOINTS_STATED` | **partial (`SKIP` + check)** | the 8 standee gap-line joints (`r_wrist`, `l_wrist`, `r_hip`, `l_hip`, `r_knee`, `l_knee`, `r_ankle`, `l_ankle`) report `SKIP` because knees, ankles, hips or hands may lie outside the frame or under clothing; the rule still refuses a person that states **0 joints with a position**, because `propose --keypoints` places bones only from joints with a position |
189
+ | `PAINTING_JOINT_ON_FIGURE` | **partial (`SKIP` + check)** | the near-white border flood (`backgroundMask`) reports `SKIP` because a painted room is not near-white ground and white clothing at the crop edge floods into the figure (`r_hip` and `l_hip` on cropped `demo` and `wind`); the rule still checks every joint with a position against unpainted pixels (`alpha <= 8`, `OPAQUE_ALPHA_ABOVE`) and refuses a joint placed on an unpainted border or transparent hole (`inputs` also requires an opaque painting, `INPUTS_PAINTING_OPAQUE`) |
190
+ | `PAINTING_ARM_CLEAR` | **`SKIP`** | arm-to-hip lines cannot cross border-connected near-white background over a painted room, and an illustration without articulated arm cuts (`config.limbs`) does not require an A-pose gap (4 of 5 public examples build and pass `check` with 0 arm-gap pixels) |
191
+ | `PAINTING_HANDS_APART` | **`SKIP`** | the straight wrist-to-wrist line crosses the torso or painted room even when the hands are wide apart (`demo` 840 px apart, `scarf` 392 px apart: 0 background px) |
192
+ | `PAINTING_LEGS_APART` | **`SKIP`** | knees and ankles may lie outside the frame on a waist/thigh crop or under a skirt, and over a painted room there is no border-connected near-white background between the legs |
193
+
135
194
  ## A recipe that is one way, not the only way
136
195
 
137
196
  Anyone with a different generator reads the requirements above, not this. This is
package/docs/STABILITY.md CHANGED
@@ -70,7 +70,7 @@ defaults are the help's too, and none is guessed from the art.
70
70
  | `mesh-stats` | `--config`, `--parts` (issue #566, additive: prints, writes nothing) |
71
71
  | `remesh` | `--rig`, `--config`, `--parts`, `--part`, `--out`, and one of `--mesh` or `--delta` (issue #526); optional `--requirements`, `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon), `--record-input-check` and `--input-check` (issue #617, additive: without them nothing `remesh` prints or writes changes) |
72
72
  | `retune` | `--rig`, `--config`, `--parts`, `--out`; optional `--requirements`, `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon), `--record-input-check` and `--input-check` (as for `remesh`, issue #617, additive) |
73
- | `painting` | one positional path, the painting; `--keypoints`; optional `--person` |
73
+ | `painting` | one positional path, the painting; `--keypoints`; optional `--person`, `--form` (`standee` or `illustration`, issue #931) |
74
74
  | `guide` | one positional argument: a rule name, `§<n>`, `§<n>.<m>` or `§<n>.<m>.<k>` |
75
75
  | (none) | `--version`, `--help` |
76
76
  <!-- /stable:commands -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rig-parts",
3
- "version": "2.0.5",
3
+ "version": "2.1.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/paintcheck.ts CHANGED
@@ -54,6 +54,29 @@
54
54
  * - `PAINTING_LEGS_APART` — a gap between the two knees and a gap between the
55
55
  * two ankles. None is legs painted as one column at that joint.
56
56
  *
57
+ * **Framed illustrations (`--form illustration`, issue #931):**
58
+ *
59
+ * The default form (`--form standee`) checks a full-body standee on a plain
60
+ * near-white or unpainted ground. A framed illustration with a painted room or a
61
+ * figure cut by the crop edge fails the border and gap rules for reasons that do
62
+ * not touch whether it can be rigged, and near-white border flood-fill
63
+ * (`backgroundMask`) both fails to mark a painted room as background and floods
64
+ * from the crop border into white clothing on the character (`r_hip` / `l_hip`
65
+ * on `demo` cropped at y=1230 and `wind` cropped at y=1250). Under
66
+ * `--form illustration`:
67
+ *
68
+ * - `PAINTING_BACKGROUND_PLAIN`, `PAINTING_ARM_CLEAR`, `PAINTING_HANDS_APART`
69
+ * and `PAINTING_LEGS_APART` do not apply and report `SKIP` with the reason,
70
+ * never `PASS`.
71
+ * - `PAINTING_JOINTS_STATED` reports `SKIP` for the eight standee gap-line
72
+ * joints (wrists, hips, knees and ankles may lie outside the frame or under
73
+ * clothing), and still refuses a person that states zero joints with a
74
+ * position (`propose --keypoints` places bones only from joints with a
75
+ * position).
76
+ * - `PAINTING_JOINT_ON_FIGURE` reports `SKIP` for the near-white border flood
77
+ * and still refuses any joint with a position whose pixel is unpainted
78
+ * (`alpha <= OPAQUE_ALPHA_ABOVE`).
79
+ *
57
80
  * Pure: the caller reads the files and hands over a raster and a person.
58
81
  */
59
82
  import type { Problem } from './errors.ts';
@@ -62,18 +85,38 @@ import { NEAR_WHITE_MIN, OPAQUE_ALPHA_ABOVE } from './layers.ts';
62
85
  import type { Raster } from './raster/types.ts';
63
86
  import type { KeypointName } from './skeleton.ts';
64
87
 
88
+ /** The forms `painting --form` accepts: a full-body standee on a plain ground (the default) or a framed illustration. */
89
+ export const PAINTING_FORMS = ['standee', 'illustration'] as const;
90
+ export type PaintingForm = (typeof PAINTING_FORMS)[number];
91
+ export const DEFAULT_PAINTING_FORM: PaintingForm = 'standee';
92
+
93
+ /** Parse `--form`, returning a usage refusal detail when unknown. */
94
+ export function paintingFormOf(value: string | undefined): PaintingForm | string {
95
+ const f = value ?? DEFAULT_PAINTING_FORM;
96
+ return (PAINTING_FORMS as readonly string[]).includes(f) ? (f as PaintingForm) : `--form ${f}; one of ${PAINTING_FORMS.join(', ')} is required`;
97
+ }
98
+
99
+ export function isPaintingForm(v: PaintingForm | string): v is PaintingForm {
100
+ return (PAINTING_FORMS as readonly string[]).includes(v);
101
+ }
102
+
65
103
  /** The joints the gap rules read, in the order a refusal lists them. */
66
104
  export const PAINTING_JOINTS: readonly KeypointName[] = ['r_wrist', 'l_wrist', 'r_hip', 'l_hip', 'r_knee', 'l_knee', 'r_ankle', 'l_ankle'];
67
105
 
68
106
  /** The joints that may be `occluded` with a position and still be read (issue #372): a skirt hides the hips on most clothed figures. */
69
107
  export const PAINTING_OCCLUDABLE: readonly KeypointName[] = ['r_hip', 'l_hip'];
70
108
 
71
- /** Whether the check reads joint `name` as given: observed with a position, or an occludable joint occluded with one. */
109
+ /** Whether the standee check reads joint `name` as given: observed with a position, or an occludable joint occluded with one. */
72
110
  function readable(name: KeypointName, j: KeypointPerson['joints'][KeypointName]): j is KeypointPerson['joints'][KeypointName] & { at: [number, number] } {
73
111
  if (j.at === null) return false;
74
112
  return j.state === 'observed' || (j.state === 'occluded' && PAINTING_OCCLUDABLE.includes(name));
75
113
  }
76
114
 
115
+ /** Whether the illustration check reads joint `j`: any observed or occluded joint that carries a position. */
116
+ function statedWithPosition(j: KeypointPerson['joints'][KeypointName]): j is KeypointPerson['joints'][KeypointName] & { state: 'observed' | 'occluded'; at: [number, number] } {
117
+ return j.at !== null && (j.state === 'observed' || j.state === 'occluded');
118
+ }
119
+
77
120
  /** The four gap rules' pairs: the rule, the two joints, and what the pair is called in a line. */
78
121
  export const PAINTING_GAPS: ReadonlyArray<readonly [string, KeypointName, KeypointName, string]> = [
79
122
  ['PAINTING_ARM_CLEAR', 'r_wrist', 'r_hip', 'right arm (r_wrist to r_hip)'],
@@ -112,8 +155,10 @@ export interface PaintingReading {
112
155
  firstBorderNotGround: [number, number] | null;
113
156
  backgroundPx: number;
114
157
  figurePx: number;
115
- /** Every observed joint and every occluded hip with a position, its state, its pixel, and whether that pixel is figure. */
116
- joints: Array<{ name: KeypointName; state: 'observed' | 'occluded'; at: [number, number]; pixel: [number, number]; onFigure: boolean }>;
158
+ /** Pixels with alpha above {@link OPAQUE_ALPHA_ABOVE}. */
159
+ opaquePx: number;
160
+ /** In `standee`, every observed joint and occluded hip with a position (`onFigure` = not border-connected ground); in `illustration`, every joint with a position (`onFigure` = alpha above {@link OPAQUE_ALPHA_ABOVE}). */
161
+ joints: Array<{ name: KeypointName; state: 'observed' | 'occluded'; at: [number, number]; pixel: [number, number]; alpha: number; onFigure: boolean }>;
117
162
  /** The gap readings whose two joints are both read as given (observed with a position, or an occluded hip with one). */
118
163
  gaps: GapReading[];
119
164
  }
@@ -172,17 +217,20 @@ export function linePixels(a: readonly [number, number], b: readonly [number, nu
172
217
  }
173
218
 
174
219
  /** Read the painting against the person's joints: every figure the rules decide on, before any rule decides. */
175
- export function readPainting(r: Raster, person: KeypointPerson): PaintingReading {
220
+ export function readPainting(r: Raster, person: KeypointPerson, form: PaintingForm = DEFAULT_PAINTING_FORM): PaintingReading {
176
221
  const { width: w, height: h } = r;
177
222
  const bg = backgroundMask(r);
178
223
  let border = 0;
179
224
  let borderNotGround = 0;
180
225
  let firstBorderNotGround: [number, number] | null = null;
226
+ let opaquePx = 0;
181
227
  for (let y = 0; y < h; y++) {
182
228
  for (let x = 0; x < w; x++) {
229
+ const idx = y * w + x;
230
+ if (r.data[idx * 4 + 3] > OPAQUE_ALPHA_ABOVE) opaquePx++;
183
231
  if (y !== 0 && y !== h - 1 && x !== 0 && x !== w - 1) continue;
184
232
  border++;
185
- if (!isGround(r, y * w + x)) {
233
+ if (!isGround(r, idx)) {
186
234
  borderNotGround++;
187
235
  firstBorderNotGround ??= [x, y];
188
236
  }
@@ -192,28 +240,62 @@ export function readPainting(r: Raster, person: KeypointPerson): PaintingReading
192
240
  for (const v of bg) backgroundPx += v;
193
241
  const joints: PaintingReading['joints'] = [];
194
242
  for (const [name, j] of Object.entries(person.joints) as Array<[KeypointName, KeypointPerson['joints'][KeypointName]]>) {
243
+ if (form === 'illustration') {
244
+ if (!statedWithPosition(j)) continue;
245
+ const pixel = pixelOf(j.at, w, h);
246
+ const alpha = r.data[(pixel[1] * w + pixel[0]) * 4 + 3];
247
+ joints.push({ name, state: j.state, at: j.at, pixel, alpha, onFigure: alpha > OPAQUE_ALPHA_ABOVE });
248
+ continue;
249
+ }
195
250
  if (!readable(name, j)) continue;
196
251
  const pixel = pixelOf(j.at, w, h);
197
- joints.push({ name, state: j.state === 'observed' ? 'observed' : 'occluded', at: j.at, pixel, onFigure: bg[pixel[1] * w + pixel[0]] === 0 });
252
+ const alpha = r.data[(pixel[1] * w + pixel[0]) * 4 + 3];
253
+ joints.push({ name, state: j.state === 'observed' ? 'observed' : 'occluded', at: j.at, pixel, alpha, onFigure: bg[pixel[1] * w + pixel[0]] === 0 });
198
254
  }
199
255
  const gaps: GapReading[] = [];
200
- for (const [rule, a, b, what] of PAINTING_GAPS) {
201
- const ja = person.joints[a];
202
- const jb = person.joints[b];
203
- if (!readable(a, ja) || !readable(b, jb)) continue;
204
- const line = linePixels(ja.at, jb.at, w, h);
205
- let background = 0;
206
- for (const [x, y] of line) background += bg[y * w + x];
207
- gaps.push({ rule, a, b, what, from: ja.at, to: jb.at, samples: line.length, background });
256
+ if (form === 'standee') {
257
+ for (const [rule, a, b, what] of PAINTING_GAPS) {
258
+ const ja = person.joints[a];
259
+ const jb = person.joints[b];
260
+ if (!readable(a, ja) || !readable(b, jb)) continue;
261
+ const line = linePixels(ja.at, jb.at, w, h);
262
+ let background = 0;
263
+ for (const [x, y] of line) background += bg[y * w + x];
264
+ gaps.push({ rule, a, b, what, from: ja.at, to: jb.at, samples: line.length, background });
265
+ }
208
266
  }
209
- return { width: w, height: h, border, borderNotGround, firstBorderNotGround, backgroundPx, figurePx: w * h - backgroundPx, joints, gaps };
267
+ return { width: w, height: h, border, borderNotGround, firstBorderNotGround, backgroundPx, figurePx: w * h - backgroundPx, opaquePx, joints, gaps };
210
268
  }
211
269
 
212
270
  const pt = (p: readonly [number, number]): string => `[${p[0]}, ${p[1]}]`;
213
271
 
214
272
  /** The refusals a reading earns, every one at once, in the order the module's comment lists the rules. */
215
- export function paintingProblems(reading: PaintingReading, person: KeypointPerson, painting: string, keypoints: string): Problem[] {
273
+ export function paintingProblems(
274
+ reading: PaintingReading,
275
+ person: KeypointPerson,
276
+ painting: string,
277
+ keypoints: string,
278
+ form: PaintingForm = DEFAULT_PAINTING_FORM,
279
+ ): Problem[] {
216
280
  const problems: Problem[] = [];
281
+ if (form === 'illustration') {
282
+ if (reading.joints.length === 0) {
283
+ problems.push({
284
+ code: 'PAINTING_JOINTS_STATED',
285
+ object: `${keypoints} person "${person.id}"`,
286
+ detail: 'states 0 joints with a position; at least one observed or occluded joint with a position is required — propose --keypoints places bones only from joints that carry a position',
287
+ });
288
+ }
289
+ for (const j of reading.joints) {
290
+ if (j.onFigure) continue;
291
+ problems.push({
292
+ code: 'PAINTING_JOINT_ON_FIGURE',
293
+ object: `${keypoints} person "${person.id}" joint ${j.name}`,
294
+ detail: `is ${j.state === 'occluded' ? 'occluded ' : ''}at ${pt(j.at)}, and pixel ${pt(j.pixel)} is unpainted (alpha ${j.alpha} <= ${OPAQUE_ALPHA_ABOVE}); a joint on painted canvas (alpha above ${OPAQUE_ALPHA_ABOVE}) is required — a file measured on another painting, or a joint placed on an empty border`,
295
+ });
296
+ }
297
+ return problems;
298
+ }
217
299
  if (reading.borderNotGround > 0) {
218
300
  const f = reading.firstBorderNotGround as [number, number];
219
301
  problems.push({
@@ -253,8 +335,25 @@ export function paintingProblems(reading: PaintingReading, person: KeypointPerso
253
335
  return problems;
254
336
  }
255
337
 
256
- /** The lines a reading prints, before any refusal: the figure, every joint and every gap, each with its numbers. */
257
- export function paintingLines(reading: PaintingReading): string[] {
338
+ /** The lines a reading prints, before any refusal: the figure, every joint and every gap (or in `illustration`, the `SKIP` lines), each with its numbers. */
339
+ export function paintingLines(reading: PaintingReading, form: PaintingForm = DEFAULT_PAINTING_FORM): string[] {
340
+ if (form === 'illustration') {
341
+ const unpainted = reading.width * reading.height - reading.opaquePx;
342
+ const on = reading.joints.filter((j) => j.onFigure).length;
343
+ const observed = reading.joints.filter((j) => j.state === 'observed').length;
344
+ const occluded = reading.joints.filter((j) => j.state === 'occluded').map((j) => j.name);
345
+ const occ = occluded.length === 0 ? '' : `, ${occluded.length} occluded with a position (${occluded.join(', ')})`;
346
+ return [
347
+ ` painting ${reading.width}x${reading.height} (form: illustration): opaque ${reading.opaquePx} px, unpainted ${unpainted} px; border ${reading.border} px, ${reading.borderNotGround} not near-white or unpainted`,
348
+ ` joints: ${observed} observed${occ}, ${on} on painted canvas${on === reading.joints.length ? '' : ` (unpainted: ${reading.joints.filter((j) => !j.onFigure).map((j) => j.name).join(', ')})`}`,
349
+ ` SKIP PAINTING_BACKGROUND_PLAIN: framed illustration; a painted room or a figure cut by the frame has non-ground border pixels (${reading.borderNotGround} of ${reading.border} border pixels not near-white or unpainted)`,
350
+ ` SKIP PAINTING_JOINTS_STATED (standee gap joints): framed illustration; wrists, hips, knees and ankles may lie outside the frame or under clothing (${reading.joints.length} joint(s) stated with a position)`,
351
+ ` SKIP PAINTING_JOINT_ON_FIGURE (near-white border flood): framed illustration; a painted room is not near-white ground and white clothing at the crop edge floods into the figure (opaque pixel alpha > ${OPAQUE_ALPHA_ABOVE} is still checked)`,
352
+ ' SKIP PAINTING_ARM_CLEAR: framed illustration; arm-to-hip lines do not cross plain border-connected background over a painted room or a pose with arms against the body',
353
+ ' SKIP PAINTING_HANDS_APART: framed illustration; the wrist-to-wrist line crosses the torso or painted room rather than plain border-connected background',
354
+ ' SKIP PAINTING_LEGS_APART: framed illustration; knees and ankles may lie outside the frame or over a painted room without plain border-connected background',
355
+ ];
356
+ }
258
357
  const out = [
259
358
  ` painting ${reading.width}x${reading.height}: background ${reading.backgroundPx} px, figure ${reading.figurePx} px; border ${reading.border} px, ${reading.borderNotGround} not background-coloured`,
260
359
  ];
@@ -271,7 +370,13 @@ export function paintingLines(reading: PaintingReading): string[] {
271
370
  }
272
371
 
273
372
  /** Read and judge: the lines to print and every refusal, which the caller prints before it throws them at once. */
274
- export function checkPainting(r: Raster, person: KeypointPerson, painting: string, keypoints: string): { lines: string[]; problems: Problem[] } {
275
- const reading = readPainting(r, person);
276
- return { lines: paintingLines(reading), problems: paintingProblems(reading, person, painting, keypoints) };
373
+ export function checkPainting(
374
+ r: Raster,
375
+ person: KeypointPerson,
376
+ painting: string,
377
+ keypoints: string,
378
+ form: PaintingForm = DEFAULT_PAINTING_FORM,
379
+ ): { lines: string[]; problems: Problem[] } {
380
+ const reading = readPainting(r, person, form);
381
+ return { lines: paintingLines(reading, form), problems: paintingProblems(reading, person, painting, keypoints, form) };
277
382
  }