@rig-c/parts 1.17.0 → 1.19.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/docs/AUTHORING.md CHANGED
@@ -93,9 +93,9 @@ read by no CPU stage).
93
93
  | `key` | every stage's report | authored; the examples use the directory name |
94
94
  | `generation.*` | the optional `comfy` adapter only; no CPU stage | recorded: checkpoint, LoRAs, sampler, prompt parts, `latent`, `seed`, `control` |
95
95
  | `generation.control.skeleton` | `comfy paint` — the OpenPose control image, drawn at `latent` | **authored**: a built-in standing pose, `"stand_sides"` or `"stand_clasp"`, or (issue #198) any other string, read as a path relative to the config file's directory to a pose file `{"width": W, "height": H, "points": {"nose": [x, y], "neck": [x, y], …}}` — `width` x `height` is the canvas and must be `generation.latent` (the points are not scaled; `CONFIG_POSE_CANVAS`), and `points` holds the 18 body-18 keypoints of `src/skeleton.ts` by name (`nose`, `neck`, `r_shoulder`, `r_elbow`, `r_wrist`, `l_shoulder`, `l_elbow`, `l_wrist`, `r_hip`, `r_knee`, `r_ankle`, `l_hip`, `l_knee`, `l_ankle`, `r_eye`, `l_eye`, `r_ear`, `l_ear`; `r`/`l` the subject's sides), each `[x, y]` in canvas px, x right, y down, `0 <= x <= W`, `0 <= y <= H`. The image is drawn from the file exactly as from a built-in (same renderer, same size), written as `control_pose-<first 12 hex of its sha256>.png`. A pose file carries no words, so `generation.pose` is required beside it (`CONFIG_FIELD_PRESENT`). The file is read wherever the config is read from a path, every problem at once: no such file — a misspelt built-in lands here too — or one that cannot be read (`CONFIG_FILE_PRESENT`), not JSON (`CONFIG_IS_JSON`), not an object, a size that is not a whole positive px count, a point that is not two numbers (`CONFIG_FIELD_TYPE`), a key besides the three (`CONFIG_KEY_KNOWN`) or written twice (`CONFIG_KEY_UNIQUE`), `points` holding other than the 18 names (`CONFIG_POSE_KEYPOINTS`, with the count found and required), a point off the canvas (`CONFIG_POSE_IN_CANVAS`, naming the point and the canvas size) |
96
- | `seethrough.resolution` | assemble — each run's canvas must be `resolution` square (`ASSEMBLE_RUN_CANVAS`) | recorded from the See-through runs; 1024 in both examples |
96
+ | `seethrough.resolution` | assemble — each run's canvas must be `resolution` square (`ASSEMBLE_RUN_CANVAS`); with one run (no `--head`, issue #366) the file may be smaller on either side, and is padded to the `resolution` square, centred | recorded from the See-through runs; 1024 in both examples. With one run it is the square the file is read on: at least the file's longer side (`ASSEMBLE_RUN_CANVAS` otherwise), and nothing a part records depends on how much larger |
97
97
  | `seethrough.steps`, `.seed`, `.offload` | no CPU stage | recorded from the runs |
98
- | `seethrough.head_box` | inputs (cuts the head image there; optional at the first call); assemble — where the head run sits on the painting | **proposed** by `propose --head-box` from the full run, square, source pixels, shifted inside the painting when needed; the head run must then be fed exactly that crop |
98
+ | `seethrough.head_box` | inputs (cuts the head image there; optional at the first call); assemble — where the head run sits on the painting | **proposed** by `propose --head-box` from the full run, square, source pixels, shifted inside the painting when needed; the head run must then be fed exactly that crop. With one run (no `--head`, issue #359: an artist's own file, §4 *For artists*) it is left out — there is no crop to place — and a config that sets it is refused (`ASSEMBLE_SINGLE_RUN_HEAD_BOX`) |
99
99
  | `assemble.rig_scale` | assemble — the rig is `trunc(w × rig_scale)` x `trunc(h × rig_scale)`, and every rig-px value after it (`bones`, `meshes`, `motion`, `patches`, `cuts`) is in that rig's pixels; `check --source` (`CHECK_SOURCE_SIZE`) and `compose` (`SCENE_RIG_SCALE_AGREES`) read it back off `parts.json` | authored; rig pixels per source pixel. 0.5 in both examples (a 1664x2432 painting makes an 832x1216 rig). **Keep the figure about 1,100 to 1,200 rig px tall** — the `H` of `propose`'s `mesh spacing:` note, the height of the parts' boxes together — so for another painting, about 1,150 over the figure's height in painting px: that is the size the proposer's spacing divisor was calibrated at (H 1106–1185, the `mesh spacing` note says so) and the size of the two examples `check`'s bars are derived from, and the proposer and `check` also hold fixed rig-px values that do not scale with it (the fringe's 10 px step, the trunk segments' 18, 20, 30 and 100 px, the 15 px wrist tolerance, the 10-row pendant floor, the seam bar's 50 rig px over 40). What else it trades, measured on `demo` with the README loop from a fresh config at 0.25, 0.5 and 1 (issue #345, rig-c 2.47.1): the same plan and the same ten chains; every bar but the one the uncorrected `demo` always fails (`bottomwear` `TIP_OVER_ROOT`, the README loop's `RL01`) passing at all three; the atlas page 418x606, 832x1205 and 1825x2047; the figure's mesh spacing 16, 32 and 64, so 830, 868 and 932 triangles; the breath 2.5 % of a torso that scales with it; and the proposed bones, scaled to 0.5, off the 0.5 proposal's by up to 38 px at 0.25 and 22.5 px at 1 (the hairpin's origin; the skirt roots 20 and 10.5) — the fixed-px values above. Changing it after the rig sections are written is not refused: `demo`'s tracked config built at 0.25 gated green and `check` failed only `TIP_OVER_ROOT` on both sleeves; `propose --from-config` names it (54 `LINT` lines, links off the art) — propose again at the new scale. Absent: `CONFIG_FIELD_PRESENT`; 0, negative or not a number: `CONFIG_FIELD_TYPE`; a rig under 1x1: `ASSEMBLE_RIG_SIZE` |
100
100
  | `assemble.plan` | assemble; also the list every `meshes`/`regions` key must come from | **proposed** by `assemble --propose-plan`. `[part name, "full" \| "head", See-through tag]`, in draw order back to front. Names are free; roles come from the tag. A layer the author made and added to a run takes `author:<label>` in the tag's place, and has no role (§5, *A layer the author made*) |
101
101
  | `assemble.extend_below_crop` | assemble | **proposed** with the plan: `{part, run, tag}` — a head-run part that reaches the bottom of the head crop is continued from a full-run layer, whole connected components |
@@ -1724,6 +1724,136 @@ The selftest runs this order, with the README's flags, on each fetched example f
1724
1724
  a config holding only `key`, `seethrough` and `assemble.rig_scale`, pasting each
1725
1725
  proposal in as the steps above say (`RL01`); every step must exit 0.
1726
1726
 
1727
+ ### For artists: your own layered file (issue #359)
1728
+
1729
+ An artist's layered file is an input in its own right: you paint the figure in
1730
+ layers, the tool reads them, and an agent does the data work. Nothing below asks
1731
+ for See-through.
1732
+
1733
+ **What to paint.** One pixel layer per part, named by its tag. `rig-parts layers
1734
+ --template art.psd --canvas <side>x<side>` writes a PSD holding one empty layer per
1735
+ name the rig reads, bottom first: `back hair`, `neck`, `neckwear`, `topwear`,
1736
+ `handwear-r`, `handwear-l`, `bottomwear`, `legwear`, `footwear`, `tail`, `wings`,
1737
+ `objects`, `headwear`, `face`, `irides-r`, `irides-l`, `eyebrow-r`, `eyebrow-l`,
1738
+ `eyewhite-r`, `eyewhite-l`, `eyelash-r`, `eyelash-l`, `eyewear`, `ears-r`, `ears-l`,
1739
+ `earwear`, `nose`, `mouth`, `front hair` — 29 names: the 24 See-through tags
1740
+ (`src/tags.ts`) less `head`, which upstream never emits and no stage reads, with the
1741
+ six split tags as their halves, `-r` being the character's right (the smaller x).
1742
+ The order is a starting stack, not a rule. Paint the layers the figure has and
1743
+ delete the rest: an empty layer is refused (`PSD_LAYER_HAS_PIXELS`). A layer you
1744
+ need that no tag fits is `author:<label>` (§5, *A layer the author made*).
1745
+
1746
+ - **The face, separated.** `face`, `eyewhite-r` and `eyewhite-l`, each painted, are
1747
+ what lets one file stand for See-through's two runs: the face box comes from
1748
+ `face`, the eye bones and the blink from the eyewhites. Without them the run is
1749
+ refused (`ASSEMBLE_SINGLE_RUN_FACE`, one line per tag missing).
1750
+ - **The stacking order is the draw order**, bottom first. A PSD carries no depth,
1751
+ and none is invented: `layers` prints one `SKIP depth:` line naming the file,
1752
+ and the turn draft (`propose --full`) skips it (`turn: SKIP`).
1753
+ - **Flatten each layer.** Normal blending, 100 % opacity, no clipping layer, no
1754
+ layer mask, 8-bit RGB: each is refused by name (`PSD_LAYER_PLAIN` naming the
1755
+ layer and what it carries, `PSD_IS_RGB8`), because the composite it would make
1756
+ is not one the reader can see. Groups are read through (pass-through or normal
1757
+ blending only). A switched-off layer is refused too, except a hidden-art layer.
1758
+ - **Small pieces.** A layer's pieces under 40 px, and under 1 % of its largest
1759
+ piece, are removed as See-through's specks (`GHOST_MIN_AREA`, `assemble`'s
1760
+ `ghost px removed:` line counts them), and `--propose-plan` leaves out a layer
1761
+ under 150 px (a note names it); both are in run pixels, so paint at the size
1762
+ the rig needs.
1763
+ - **The canvas.** One run is one canvas, of any shape no side of which is above
1764
+ `seethrough.resolution` (`ASSEMBLE_RUN_CANVAS` otherwise: padding cannot shrink a
1765
+ file, so set `resolution` to at least the longer side). The painting — `--source`,
1766
+ the file flattened and saved as an opaque PNG — is the same size
1767
+ (`ASSEMBLE_SINGLE_RUN_SOURCE` otherwise). A file that is not the `resolution`
1768
+ square is padded to it before it is read, the way `inputs` pads a painting for
1769
+ See-through (issue #366): centred, each margin `floor((resolution - side) / 2)`,
1770
+ the odd pixel to the right or the bottom, the pad transparent. The rig holds no
1771
+ pad — it is the file times `rig_scale`, so a part's box over `rig_scale` is its
1772
+ box in the file — and `parts.json` records the pad in `single_run_pad` (§5); a
1773
+ `padded:` line under `one run:` prints the four margins. A 75 x 128 file at
1774
+ resolution 128 is padded left 26, right 27; a square file at the resolution is
1775
+ not padded, and its `parts.json` has no such block.
1776
+ - **Hidden art.** A layer named `<layer>:hidden` — `handwear-r:hidden`, the forearm
1777
+ under the sleeve; `topwear:hidden`, the shirt under a scarf's tail — is that
1778
+ layer's art where something covers it. It is never drawn and never a part, and
1779
+ it may be switched off in the file. `assemble` reads it as the `under` of a cut
1780
+ of that layer's part (§3, `assemble.cuts`) and as the cap of a limb segment that
1781
+ takes that part (§3, *Limbs*), wherever the config names no file for them; a
1782
+ config that names one as well is refused (`ASSEMBLE_HIDDEN_ONE_SOURCE`), since
1783
+ reading either would be a guess. A cut of a part with hidden art is a cut with an
1784
+ `under`, so its piece is drawn in front of the part (`ASSEMBLE_UNDER_DRAW`
1785
+ otherwise), and a cut the hidden art holds nothing under fills 0 and says so. One `hidden art:` line per such layer names
1786
+ what read it, or says nothing did. Its name is the tag plus a suffix, so it
1787
+ needs no config field to say whose it is; a `:hidden` layer whose layer the file
1788
+ does not hold is refused (`LAYERS_HIDDEN_OF_PRESENT`).
1789
+
1790
+ **The commands.** The config holds no `seethrough.head_box`. Leave `--head` out
1791
+ everywhere:
1792
+
1793
+ ```sh
1794
+ rig-parts layers art.psd
1795
+ rig-parts assemble --propose-plan --source painting.png --full art.psd --config config.json
1796
+ rig-parts assemble --source painting.png --full art.psd --config config.json --out .
1797
+ rig-parts propose --parts rig --source painting.png --out .
1798
+ rig-parts build --config config.json --source painting.png --full art.psd --out build
1799
+ ```
1800
+
1801
+ `--propose-plan` writes every row from the full run, and its first note says the
1802
+ file was read as both runs; the assemble stage refuses a row that takes `"head"`
1803
+ or an `extend_below_crop` entry (`ASSEMBLE_SINGLE_RUN_PLAN`), and prints `one run:`
1804
+ under its first line.
1805
+
1806
+ **What you get back.** `parts/<name>.png` and `parts.json` (where every part came
1807
+ from and what it measured), `rig.json` and `motion.json`, and from `build` the packed
1808
+ atlas — `check/build/skeleton.json`, `.atlas` and `.png` — that a Spine runtime
1809
+ loads, with `check/contact.png` and the idle frames to look at.
1810
+
1811
+ ### The correction loop (issue #359)
1812
+
1813
+ A parse can be wrong: a patch painted into the wrong layer, two layers that should
1814
+ be one, a split along a painted edge. An agent cannot edit a PSD, so the loop runs
1815
+ on the wrapper form instead, which any image library edits:
1816
+
1817
+ 1. **Export.** `rig-parts layers --export work art.psd` writes `work/layers.json`
1818
+ and one PNG per layer (`<index>_<name>.png`), drawn layers in draw order, then
1819
+ the hidden-art layers; `depth_median` is `null` throughout, which the reader
1820
+ takes as "the manifest's order is the draw order". The export is read back at
1821
+ once and must reproduce the file's reading — layers, boxes, pixels, order — or
1822
+ it is refused (`LAYERS_EXPORT_ROUND_TRIP`).
1823
+ 2. **Read.** `rig-parts layers work` (the table and its `WARN` lines),
1824
+ `assemble --full work …` (the part lines, `recomposite vs source`, the
1825
+ `uncovered hole` and `uncovered:` lines) and `render/recomposite_error_rig.png`
1826
+ (uncovered error red, covered error blue). A pixel in the wrong layer shows as
1827
+ error where that layer is drawn behind another part and the painting is
1828
+ near-white there (the seam override leaves near-white alone), or as `drift=`
1829
+ and `seam=` counts on the part drawn over it when it is not: projection and the
1830
+ seam override repaint that part to the painting's colour, so the error map
1831
+ stays clean while the part carries the wrong art.
1832
+ 3. **Fix** the PNGs and `layers.json`: move the pixels, and when a layer's art
1833
+ leaves its box, grow the PNG and the box together (`LAYERS_PNG_MATCHES_BBOX`
1834
+ otherwise).
1835
+ 4. **Re-run** from step 2 on `work`.
1836
+
1837
+ **A worked example** (the selftest's, `fixtures/artist.ts`): a 128-px file whose
1838
+ right eyewhite was painted half into `back hair` — the 8 x 10 patch at
1839
+ `[52, 22, 60, 32]`, near-white, behind the face. Assembled from the export:
1840
+
1841
+ ```
1842
+ recomposite vs source: mean |d|=0.40, within 8: 99.5%, error px > 40: 80, uncovered error px: 0
1843
+ ```
1844
+
1845
+ 80 covered error pixels, blue on the error map, in one 8 x 10 block; the face's
1846
+ part line says `drift=60`, the block's pixels inside the face's projection core
1847
+ whose colour is more than 90 off the painting's. The fix:
1848
+ in `work/00_back_hair.png` clear the block (it is `back hair`'s pixels
1849
+ `(20..27, 18..27)`, box `[32, 4, 96, 48]`); widen `work/03_eyewhite-r.png` from
1850
+ 8 x 10 to 16 x 10, painting the block into its right half; and set that layer's
1851
+ `right` from 52 to 60 in `layers.json`. Re-run: `error px > 40: 0`, the clean
1852
+ file's figure, and every part is the clean file's but `hair_back`, which keeps an
1853
+ 80-px hole behind the face where the hair under the eye was never painted — every
1854
+ one of them occluded, so nothing shows it at rest; paint it there if the face will
1855
+ move off it (`AS245`).
1856
+
1727
1857
  ## 5. After each stage: what to read
1728
1858
 
1729
1859
  | stage | read | green looks like | a common red |
@@ -2083,6 +2213,21 @@ note saying so — give it a mesh in `meshes` if it should bend. The one excepti
2083
2213
  sleeve rule, which names its chains by side and takes a cut sleeve as a blob of its own
2084
2214
  (the `one handwear part holds both wrists` note, §3).
2085
2215
 
2216
+ With one run whose file is not the `seethrough.resolution` square (issue #366, §4
2217
+ *For artists*), `parts.json` ends with a `single_run_pad` block: where the file sat on
2218
+ the square it was read on. Two runs and a file that is the square write none.
2219
+
2220
+ | field | holds |
2221
+ | --- | --- |
2222
+ | `file_size` | the file's `[w, h]` — the painting's too |
2223
+ | `side` | the square's side, `seethrough.resolution` |
2224
+ | `left`, `top` | the margins, `floor((side - w) / 2)` and `floor((side - h) / 2)`; the right and the bottom take the remainder |
2225
+
2226
+ Run pixel `(x, y)` is file pixel `(x - left, y - top)`, and a part's box over
2227
+ `scale_rig_per_source` is its box in the file. The reader refuses margins that are not
2228
+ that centring, a file larger than its square or equal to it, and a `rig_size` that is
2229
+ not `file_size` times the scale, truncated (`PARTS_SINGLE_RUN_PAD`).
2230
+
2086
2231
  `visible_px + occluded_px = opaque_px`, and every projected pixel is visible; the stage
2087
2232
  refuses (`ASSEMBLE_COUNTS_ADD_UP`) rather than write counts that break either, and the
2088
2233
  reader refuses a record that breaks them (`PARTS_COUNTS_ADD_UP`). The three visibility
@@ -2134,7 +2279,11 @@ row in this page's `rule` and `line` tables; the selftest holds that (`TY16`).
2134
2279
  | <a id="LAYERS_MANIFEST_PRESENT"></a><a id="LAYERS_MANIFEST_IS_JSON"></a><a id="LAYERS_FIELD_PRESENT"></a><a id="LAYERS_KEY_KNOWN"></a>`LAYERS_MANIFEST_PRESENT`, `LAYERS_MANIFEST_IS_JSON`, `LAYERS_FIELD_PRESENT`, `LAYERS_KEY_KNOWN` | the wrapper's `layers.json` is missing, unparsable, or not the wrapper's shape | the run's output directory; re-export it |
2135
2280
  | <a id="LAYERS_PNG_PRESENT"></a><a id="LAYERS_PNG_UNAMBIGUOUS"></a><a id="LAYERS_PNG_DECODES"></a><a id="LAYERS_PNG_MATCHES_BBOX"></a>`LAYERS_PNG_PRESENT`, `LAYERS_PNG_UNAMBIGUOUS`, `LAYERS_PNG_DECODES`, `LAYERS_PNG_MATCHES_BBOX` | a layer's PNG is missing, found in two places, not a PNG, or not its box's size | the run's files |
2136
2281
  | <a id="LAYERS_TAG_KNOWN"></a><a id="LAYERS_NAME_UNIQUE"></a><a id="LAYERS_BBOX_INSIDE_CANVAS"></a>`LAYERS_TAG_KNOWN`, `LAYERS_NAME_UNIQUE`, `LAYERS_BBOX_INSIDE_CANVAS` | a layer name that is neither a See-through v3 tag nor `author:<label>` (or is `author:` with a label that is empty, starts with `.` or whitespace, ends with whitespace, or holds a slash, backslash or colon — the detail says which), a repeated name, a box off the canvas | the run (these are See-through's own outputs); a layer you added by hand is named `author:<label>` (§5, *A layer the author made*) |
2137
- | <a id="PSD_FILE_PRESENT"></a><a id="PSD_PARSES"></a><a id="PSD_IS_RGB8"></a><a id="PSD_HAS_LAYERS"></a><a id="PSD_LAYER_PLAIN"></a><a id="PSD_LAYER_HAS_PIXELS"></a>`PSD_FILE_PRESENT`, `PSD_PARSES`, `PSD_IS_RGB8`, `PSD_HAS_LAYERS`, `PSD_LAYER_PLAIN`, `PSD_LAYER_HAS_PIXELS` | the `.psd` is missing, unreadable, not 8-bit RGB, has no layer, or has a layer that is hidden, not at full opacity, or without pixels | the `.psd` |
2282
+ | <a id="PSD_FILE_PRESENT"></a><a id="PSD_PARSES"></a><a id="PSD_IS_RGB8"></a><a id="PSD_HAS_LAYERS"></a><a id="PSD_LAYER_PLAIN"></a><a id="PSD_LAYER_HAS_PIXELS"></a>`PSD_FILE_PRESENT`, `PSD_PARSES`, `PSD_IS_RGB8`, `PSD_HAS_LAYERS`, `PSD_LAYER_PLAIN`, `PSD_LAYER_HAS_PIXELS` | the `.psd` is missing, unreadable, not 8-bit RGB, has no layer, or has a layer that is hidden (a hidden-art layer, `<layer>:hidden`, may be), not at full opacity, blended other than normal, clipped, masked, or without pixels | the `.psd`; an artist's file: §4, *For artists* |
2283
+ | <a id="LAYERS_DEPTH_ALL_OR_NONE"></a>`LAYERS_DEPTH_ALL_OR_NONE` | some drawn layers of a wrapper `layers.json` carry a `depth_median` and others carry `null` (issue #359): a mix has no one draw order | a number on every drawn layer (a See-through run), or `null` on every one (an export of a PSD: the manifest's order is then the draw order) |
2284
+ | <a id="LAYERS_HIDDEN_OF_PRESENT"></a><a id="LAYERS_HIDDEN_NO_DEPTH"></a>`LAYERS_HIDDEN_OF_PRESENT`, `LAYERS_HIDDEN_NO_DEPTH` | a hidden-art layer, `<layer>:hidden` (issue #359), belongs to a drawn layer the input does not hold; or, in a wrapper `layers.json`, carries a `depth_median` other than `null` (it is never drawn) | the layer's name, or the layer it names; `depth_median: null` |
2285
+ | <a id="LAYERS_EXPORT_DIR_EMPTY"></a>`LAYERS_EXPORT_DIR_EMPTY` | `layers --export <dir>` was given a file, or a directory that is not empty (issue #359): a file left there could be read in place of the one written | a new directory, or empty it |
2286
+ | <a id="LAYERS_EXPORT_ROUND_TRIP"></a>`LAYERS_EXPORT_ROUND_TRIP` | the export, read back, differs from the input's reading (the detail names the first difference) — an exporter bug, not an input problem | report it with the input named |
2138
2287
  | <a id="ASSEMBLE_SOURCE_PRESENT"></a><a id="ASSEMBLE_SOURCE_DECODES"></a>`ASSEMBLE_SOURCE_PRESENT`, `ASSEMBLE_SOURCE_DECODES` | the painting is missing or not a PNG | `--source` |
2139
2288
  | <a id="SHEET_SOURCE_PRESENT"></a><a id="SHEET_PNG_PRESENT"></a>`SHEET_SOURCE_PRESENT`, `SHEET_PNG_PRESENT` | `sheet`'s painting, or a part PNG a `parts.json` lists, is missing | `--source`, `--layers` |
2140
2289
 
@@ -2230,6 +2379,24 @@ See-through with another seed.
2230
2379
  | <a id="INPUTS_PAINTING_OPAQUE"></a>`INPUTS_PAINTING_OPAQUE` | the painting has a pixel below alpha 255 | the painting |
2231
2380
  | <a id="INPUTS_HEAD_BOX_INSIDE"></a>`INPUTS_HEAD_BOX_INSIDE` | `seethrough.head_box` leaves the painting (the crop is not padded) | `head_box` — take `propose --head-box`'s |
2232
2381
 
2382
+ ### painting
2383
+
2384
+ `rig-parts painting --keypoints <k.json> <painting.png>` (issue #350) reads the painting
2385
+ before See-through runs on it. *Background* is ground (alpha 8 or less, or every channel
2386
+ above 235) connected to the painting's edge; the *figure* is the rest; a *gap* is one
2387
+ background pixel on the straight line between two joints. [PAINTING.md](PAINTING.md)
2388
+ says why each requirement exists and what breaks without it.
2389
+
2390
+ | rule | means | change |
2391
+ | --- | --- | --- |
2392
+ | <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 |
2393
+ | <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*) |
2394
+ | <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*) |
2395
+ | <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 |
2396
+ | <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*) |
2397
+ | <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*) |
2398
+ | <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*) |
2399
+
2233
2400
  ### The optional ComfyUI adapter (`comfy paint`, `comfy seethrough`)
2234
2401
 
2235
2402
  | rule | means | change |
@@ -2249,7 +2416,11 @@ See-through with another seed.
2249
2416
  | <a id="ASSEMBLE_FIELD_PRESENT"></a>`ASSEMBLE_FIELD_PRESENT` | `seethrough.head_box` is missing | `propose --head-box` proposes it (§3) |
2250
2417
  | <a id="ASSEMBLE_RIG_SIZE"></a>`ASSEMBLE_RIG_SIZE` | `rig_scale` makes an empty rig | `assemble.rig_scale` |
2251
2418
  | <a id="ASSEMBLE_HEAD_BOX_INSIDE"></a>`ASSEMBLE_HEAD_BOX_INSIDE` | the head box is outside the painting | `head_box` — `propose --head-box` holds it inside |
2252
- | <a id="ASSEMBLE_RUN_CANVAS"></a>`ASSEMBLE_RUN_CANVAS` | a run's canvas is not `resolution` square | `seethrough.resolution`, or the run |
2419
+ | <a id="ASSEMBLE_RUN_CANVAS"></a>`ASSEMBLE_RUN_CANVAS` | a run's canvas is not `resolution` square; with one run (issue #366), a side of the file is above `resolution` — a smaller one is padded, a larger one would have to be shrunk | `seethrough.resolution`, or the run |
2420
+ | <a id="ASSEMBLE_SINGLE_RUN_SOURCE"></a>`ASSEMBLE_SINGLE_RUN_SOURCE` | with no `--head` (issue #366), the painting `--source` names is not the one run's file's size: the painting is the file flattened, and every part's box maps back to the file through it | the painting — the file flattened, at the file's size |
2421
+ | <a id="ASSEMBLE_SINGLE_RUN_FACE"></a>`ASSEMBLE_SINGLE_RUN_FACE` | with no `--head` (issue #359), the one run holds no painted layer for one of `face`, `eyewhite-r`, `eyewhite-l` — the parts the head run exists to separate (one problem per tag) | paint the layer, or give the head run with `--head` |
2422
+ | <a id="ASSEMBLE_SINGLE_RUN_PLAN"></a>`ASSEMBLE_SINGLE_RUN_PLAN` | with no `--head`, a plan row takes `"head"`, or an `extend_below_crop` entry continues a part below a crop there is not | `"full"` in the row; remove the entry |
2423
+ | <a id="ASSEMBLE_SINGLE_RUN_HEAD_BOX"></a>`ASSEMBLE_SINGLE_RUN_HEAD_BOX` | with no `--head`, the config sets `seethrough.head_box`, which places a head run there is not | remove `head_box`, or give the head run with `--head` |
2253
2424
  | <a id="ASSEMBLE_PLAN_TAG_IN_RUN"></a><a id="ASSEMBLE_EXTEND_TAG_IN_RUN"></a>`ASSEMBLE_PLAN_TAG_IN_RUN`, `ASSEMBLE_EXTEND_TAG_IN_RUN` | an entry takes a tag its run does not hold (the detail lists what it does hold) | that entry's run or tag |
2254
2425
  | <a id="ASSEMBLE_PART_OPAQUE"></a>`ASSEMBLE_PART_OPAQUE` | a part ended with no opaque pixel | drop the entry, or take the tag from the other run |
2255
2426
  | <a id="ASSEMBLE_PATCH_BOX_INSIDE"></a>`ASSEMBLE_PATCH_BOX_INSIDE` | a patch's box reaches past the rig (the painting times `rig_scale`; the detail gives the size) | that patch's `box` — it is in rig pixels, not painting pixels |
@@ -2264,6 +2435,7 @@ See-through with another seed.
2264
2435
  | <a id="ASSEMBLE_LIMB_TAKES"></a><a id="ASSEMBLE_LIMB_PIXELS"></a>`ASSEMBLE_LIMB_TAKES`, `ASSEMBLE_LIMB_PIXELS` | a part a limb takes has no opaque pixel, or a limb segment gets none of it — the joint lines through the bones' origins put none of the limb's pixels on it, or another limb's polyline is nearer every one (issue #349) | the bones: their origins on the limb's art, each joint where the limb bends |
2265
2436
  | <a id="ASSEMBLE_LIMB_JOINT"></a><a id="ASSEMBLE_LIMB_JOINT_ON_ART"></a>`ASSEMBLE_LIMB_JOINT`, `ASSEMBLE_LIMB_JOINT_ON_ART` | a joint has no line — the upper bone has no length, or the limb folds straight back on itself — or its bone's origin lies on none of the limb's art within 1 px along the line, so the limb has no width there to size the cap by (issue #349) | the joint's bone: on the limb's art, at the joint |
2266
2437
  | <a id="ASSEMBLE_LIMB_CAP_PRESENT"></a><a id="ASSEMBLE_LIMB_CAP_DECODES"></a><a id="ASSEMBLE_LIMB_CAP_SIZE"></a><a id="ASSEMBLE_LIMB_CAP_EMPTY"></a>`ASSEMBLE_LIMB_CAP_PRESENT`, `ASSEMBLE_LIMB_CAP_DECODES`, `ASSEMBLE_LIMB_CAP_SIZE`, `ASSEMBLE_LIMB_CAP_EMPTY` | a segment's `cap` names no file (relative to the config's directory) or one that is not a PNG; an image not the size of the cap's box (the detail gives both, and the box's corner); or one transparent over every cap pixel (issue #349) | the image: the box the `limb cap:` line prints, `w`x`h`, its pixel `(i, j)` rig pixel `(x + i, y + j)`; to keep the layer's cap, drop `cap` |
2438
+ | <a id="ASSEMBLE_HIDDEN_ONE_SOURCE"></a>`ASSEMBLE_HIDDEN_ONE_SOURCE` | a cut's `under`, or a limb segment's `cap`, names a file, and the part it reads also has a hidden-art layer (`<layer>:hidden`, issue #359): reading either would be a guess | remove the file from the config, or the hidden-art layer from the input |
2267
2439
  | <a id="ASSEMBLE_COUNTS_ADD_UP"></a>`ASSEMBLE_COUNTS_ADD_UP` | a part's visible and occluded counts do not add up to its opaque pixels, or a projected pixel is not visible, or a cut part's pieces do not add up to it — an assembler bug, not an input problem | report it with the part named; nothing was written |
2268
2440
 
2269
2441
  ### propose
@@ -2276,14 +2448,14 @@ See-through with another seed.
2276
2448
  | <a id="PROPOSE_CUT_FROM_RECORDED"></a>`PROPOSE_CUT_FROM_RECORDED` | two parts share one layer and none of them records `cut_from`: a `parts.json` an `assemble` before 1.6.0 wrote with `assemble.cuts`. The records are in draw order, so which is the plan part and which the piece cannot be read off the file; the message names the parts and the layer. Every other reader still takes the file | `--parts`: re-run assemble (1.6.0+) to record `cut_from` |
2277
2449
  | <a id="PROPOSE_LIMB_PARTS"></a>`PROPOSE_LIMB_PARTS` | `parts.json` holds parts `config.limbs` cut at their joints (issue #349); the limbs are drafted from the parts before the cut, which the rules read by layer | `--parts` a build assembled with the config's `limbs` left out; keep the block the config holds (its caps and ranges are the author's). `propose --from-config` reads the config's bones on either build |
2278
2450
  | <a id="PROPOSE_ACCESSORY_BODY"></a>`PROPOSE_ACCESSORY_BODY` | an accessory has nothing above its pendant rows to hang its bone on | that part's plan entry, or author its bones by hand |
2279
- | <a id="PROPOSE_TURN_RUN_GIVEN"></a><a id="PROPOSE_TURN_LAYER_PRESENT"></a><a id="PROPOSE_TURN_DEPTH_PRESENT"></a>`PROPOSE_TURN_RUN_GIVEN`, `PROPOSE_TURN_LAYER_PRESENT`, `PROPOSE_TURN_DEPTH_PRESENT` | with `--full`/`--head` (issue #251): a part came from a run whose layers were not given, the run given holds no layer of the part's tag, or its layer carries no `depth_median` (a PSD carries none) | give the run the parts were assembled from, as the wrapper's `layers.json` |
2451
+ | <a id="PROPOSE_TURN_RUN_GIVEN"></a><a id="PROPOSE_TURN_LAYER_PRESENT"></a><a id="PROPOSE_TURN_DEPTH_PRESENT"></a>`PROPOSE_TURN_RUN_GIVEN`, `PROPOSE_TURN_LAYER_PRESENT`, `PROPOSE_TURN_DEPTH_PRESENT` | with `--full`/`--head` (issue #251): a part came from a run whose layers were not given, the run given holds no layer of the part's tag, or its layer carries no `depth_median` while another part's layer carries one; a run with no depth on any part's layer (a PSD, or an export of one) is not refused — the turn is skipped, and the note says `turn: SKIP` and names the run (issue #359) | give the run the parts were assembled from, as the wrapper's `layers.json` |
2280
2452
  | <a id="BASIS_RECORDS_EVERY_BONE"></a><a id="BASIS_NAMES_RESOLVE"></a><a id="BASIS_KIND_KNOWN"></a><a id="BASIS_KIND_HOLDS"></a>`BASIS_RECORDS_EVERY_BONE`, `BASIS_NAMES_RESOLVE`, `BASIS_KIND_KNOWN`, `BASIS_KIND_HOLDS` | `propose --keypoints`'s record of how it placed each bone, `basis.json`, does not hold to the proposal it was written with: a proposed bone with no record, one recorded twice or out of the proposal's order; a part, bone or frame quantity it names that is not there; a placement kind outside the known ones; or a placement that does not hold what its kind says (a joint placement with no joint, a non-joint one with a joint, a derived one with no bone, a constant that is not finite). Nothing is written | a proposer bug, not an input problem: report it with the line |
2281
2453
  | <a id="KEYPOINTS_FILE_PRESENT"></a><a id="KEYPOINTS_IS_JSON"></a><a id="KEYPOINTS_KEY_KNOWN"></a><a id="KEYPOINTS_FIELD_PRESENT"></a><a id="KEYPOINTS_FIELD_TYPE"></a>`KEYPOINTS_FILE_PRESENT`, `KEYPOINTS_IS_JSON`, `KEYPOINTS_KEY_KNOWN`, `KEYPOINTS_FIELD_PRESENT`, `KEYPOINTS_FIELD_TYPE` | the `--keypoints` file is absent, not JSON, has a key this reader does not know, lacks one of `spec`, `space`, `width`, `height`, `source`, `people`, or holds a value of the wrong type (a size that is not a whole positive px count, an empty `source`, no people) | the keypoint file (§3, *A posed figure*) |
2282
2454
  | <a id="KEYPOINTS_SPEC_KNOWN"></a><a id="KEYPOINTS_SPACE_STATED"></a>`KEYPOINTS_SPEC_KNOWN`, `KEYPOINTS_SPACE_STATED` | the file is not `spine-parts-keypoints/1`, or states a space other than painting-px, origin top-left, y down | convert the file to that space; it is not read as if it were in it |
2283
2455
  | <a id="KEYPOINTS_JOINT_KNOWN"></a><a id="KEYPOINTS_JOINT_STATE"></a><a id="KEYPOINTS_POSITION_STATED"></a><a id="KEYPOINTS_POSITION_INSIDE"></a>`KEYPOINTS_JOINT_KNOWN`, `KEYPOINTS_JOINT_STATE`, `KEYPOINTS_POSITION_STATED`, `KEYPOINTS_POSITION_INSIDE` | a joint name outside body-18, a state outside observed / occluded / missing, an observed joint with no `at` or a missing one with one, or a position outside the `width`x`height` image | that joint |
2284
2456
  | <a id="KEYPOINTS_PERSON_UNIQUE"></a><a id="KEYPOINTS_PERSON_CHOSEN"></a>`KEYPOINTS_PERSON_UNIQUE`, `KEYPOINTS_PERSON_CHOSEN` | two people share an id; or the file holds more than one person and no `--person` names one, or `--person` names none of them (the message lists the ids) | `--person <id>` |
2285
2457
  | <a id="KEYPOINTS_IMAGE_SIZE"></a>`KEYPOINTS_IMAGE_SIZE` | the file's `width`x`height` is not the `--source` painting's | a file measured on this painting |
2286
- | <a id="PARTS_FILE_PRESENT"></a><a id="PARTS_IS_JSON"></a><a id="PARTS_KEY_KNOWN"></a><a id="PARTS_FIELD_PRESENT"></a><a id="PARTS_FIELD_TYPE"></a><a id="PARTS_NAME_UNIQUE"></a><a id="PARTS_FROM_KNOWN"></a><a id="PARTS_CUT_FROM_KNOWN"></a><a id="PARTS_BOX_INSIDE_RIG"></a><a id="PARTS_COUNTS_ADD_UP"></a><a id="PARTS_HOLES_LARGEST_FIRST"></a><a id="PARTS_HOLE_PART_KNOWN"></a>`PARTS_*` (`PARTS_FILE_PRESENT`, `PARTS_IS_JSON`, `PARTS_KEY_KNOWN`, `PARTS_FIELD_PRESENT`, `PARTS_FIELD_TYPE`, `PARTS_NAME_UNIQUE`, `PARTS_FROM_KNOWN`, `PARTS_CUT_FROM_KNOWN`, `PARTS_BOX_INSIDE_RIG`, `PARTS_COUNTS_ADD_UP`, `PARTS_HOLES_LARGEST_FIRST`, `PARTS_HOLE_PART_KNOWN`) | the `parts.json` read is not assemble's contract (`PARTS_COUNTS_ADD_UP`: `visible_px + occluded_px` is not `opaque_px`, `visible_not_projected_px` is above `visible_px`, or the `recomposite` block's figures disagree — more uncovered than error pixels, a hole larger than its box, the wrong number of holes listed, listed areas that do not sum to `uncovered_error_px`; the last two codes are the block's order and its border names, §5) | re-run assemble; do not edit `parts.json` |
2458
+ | <a id="PARTS_FILE_PRESENT"></a><a id="PARTS_IS_JSON"></a><a id="PARTS_KEY_KNOWN"></a><a id="PARTS_FIELD_PRESENT"></a><a id="PARTS_FIELD_TYPE"></a><a id="PARTS_NAME_UNIQUE"></a><a id="PARTS_FROM_KNOWN"></a><a id="PARTS_CUT_FROM_KNOWN"></a><a id="PARTS_BOX_INSIDE_RIG"></a><a id="PARTS_COUNTS_ADD_UP"></a><a id="PARTS_HOLES_LARGEST_FIRST"></a><a id="PARTS_HOLE_PART_KNOWN"></a><a id="PARTS_SINGLE_RUN_PAD"></a>`PARTS_*` (`PARTS_FILE_PRESENT`, `PARTS_IS_JSON`, `PARTS_KEY_KNOWN`, `PARTS_FIELD_PRESENT`, `PARTS_FIELD_TYPE`, `PARTS_NAME_UNIQUE`, `PARTS_FROM_KNOWN`, `PARTS_CUT_FROM_KNOWN`, `PARTS_BOX_INSIDE_RIG`, `PARTS_COUNTS_ADD_UP`, `PARTS_HOLES_LARGEST_FIRST`, `PARTS_HOLE_PART_KNOWN`, `PARTS_SINGLE_RUN_PAD`) | the `parts.json` read is not assemble's contract (`PARTS_COUNTS_ADD_UP`: `visible_px + occluded_px` is not `opaque_px`, `visible_not_projected_px` is above `visible_px`, or the `recomposite` block's figures disagree — more uncovered than error pixels, a hole larger than its box, the wrong number of holes listed, listed areas that do not sum to `uncovered_error_px`; the last two codes are the block's order and its border names; `PARTS_SINGLE_RUN_PAD`: the `single_run_pad` block's margins are not the centring, its file is not smaller than its square, or `rig_size` is not the file at the scale, issue #366 — §5) | re-run assemble; do not edit `parts.json` |
2287
2459
 
2288
2460
  ### compare
2289
2461
 
package/docs/MIGRATION.md CHANGED
@@ -210,3 +210,76 @@ no declared form changed.
210
210
  What to do: nothing for a config without `contour.bridges`. A part that declares it and
211
211
  holds a faint island no longer draws that island; if those pixels matter, keep the
212
212
  lattice for that part.
213
+
214
+ ## After 1.17.0: an artist's own file
215
+
216
+ Issue #359 makes an artist's layered PSD an input in its own right (AUTHORING §4,
217
+ *For artists* and *The correction loop*). Nothing a config already builds changes:
218
+ every new form was refused or a usage error before, so no byte any existing build
219
+ writes moves (the selftest's existing controls and the examples' `expected/` are
220
+ unchanged by it).
221
+
222
+ | change | before | after |
223
+ | --- | --- | --- |
224
+ | `assemble`, `--propose-plan` and `build` without `--head` | a usage error (exit 2) | one run: the full run must hold `face`, `eyewhite-r` and `eyewhite-l`, painted (`ASSEMBLE_SINGLE_RUN_FACE`); every row takes `"full"` (`ASSEMBLE_SINGLE_RUN_PLAN`); no `seethrough.head_box` (`ASSEMBLE_SINGLE_RUN_HEAD_BOX`) |
225
+ | `propose --full`/`--head` over runs with no depth on any part's layer (a PSD) | refused, `PROPOSE_TURN_DEPTH_PRESENT` on every part (exit 1) | no `turn`, one note `turn: SKIP — <runs> carry no depth_median …`, the proposal written (exit 0); a run where some parts' layers carry a depth and others do not is still refused part by part, and the detail now says so |
226
+ | a wrapper `layers.json` with `depth_median: null` | refused, `LAYERS_FIELD_PRESENT` | `null` on every drawn layer reads in the manifest's order; a mix is `LAYERS_DEPTH_ALL_OR_NONE` |
227
+ | a layer named `<layer>:hidden` | refused, `LAYERS_TAG_KNOWN` | a hidden-art layer, listed apart and never drawn; read as a cut's `under` and a limb segment's cap where the config names no file (`ASSEMBLE_HIDDEN_ONE_SOURCE` when it names one too) |
228
+ | `layers` on a PSD, or on a manifest with no depth | the table | the table and one `SKIP depth:` line naming the file |
229
+ | `parts.json`, a limb segment's `cap` | `image_px` with a cap file | `image_layer` after `image_px` when the image was a hidden-art layer |
230
+
231
+ What to do: nothing. A consumer that matched `PROPOSE_TURN_DEPTH_PRESENT` to detect a
232
+ PSD run reads the `turn: SKIP` note instead.
233
+
234
+ Issue #366 lets that one run be a file that is not square (AUTHORING §4, *For
235
+ artists*: the canvas). Two-run builds and a square file at `seethrough.resolution`
236
+ are read exactly as before — no `single_run_pad` block, no `padded:` line, the same
237
+ bytes (the selftest's existing single-run controls and the examples' `expected/` are
238
+ unchanged by it).
239
+
240
+ | change | before | after |
241
+ | --- | --- | --- |
242
+ | one run whose file is not the `resolution` square, no side above it | refused, `ASSEMBLE_RUN_CANVAS` | padded to the square, centred (the odd pixel right or bottom), and assembled in the file's pixels; `parts.json` ends with `single_run_pad` (`file_size`, `side`, `left`, `top`) and `assemble` prints a `padded:` line under `one run:` |
243
+ | one run whose file has a side above `resolution` | refused, `ASSEMBLE_RUN_CANVAS` | still refused by that code; the detail now says padding cannot shrink a file and names the resolution that would hold it |
244
+ | one run with a painting of another size than the file | read: the run mapped onto the painting at the painting's scale | refused, `ASSEMBLE_SINGLE_RUN_SOURCE` — §4 already required the same size |
245
+ | a `parts.json` with `single_run_pad` | refused, `PARTS_KEY_KNOWN` | read; a block that is not the centring, or disagrees with `rig_size`, is `PARTS_SINGLE_RUN_PAD` |
246
+
247
+ What to do: nothing, unless a single-run painting was not the file's own size — pass
248
+ the file flattened.
249
+
250
+ ### And a check before See-through (`painting`, issue #350)
251
+
252
+ A new command, `rig-parts painting --keypoints <k.json> [--person <id>]
253
+ <painting.png>`, reads a painting against its body-18 joints before See-through runs
254
+ and refuses what a rig cannot be cut from (arms against the body, hands together,
255
+ legs without a gap, a joint off the figure, a background that is not plain;
256
+ [PAINTING.md](PAINTING.md)). It reads the keypoint file `propose --keypoints` reads
257
+ and writes nothing; no other command changed, and no byte any build writes moves.
258
+
259
+ | change | before | after |
260
+ | --- | --- | --- |
261
+ | `rig-parts painting` | a usage error, `unknown command` (exit 2) | the check: exit 0 PASS, 1 with a `FAIL PAINTING_…` line per refusal |
262
+ | the README's input bullet for the painting | one sentence | the requirements, linked to `docs/PAINTING.md`, which ships in the package |
263
+
264
+ What to do: nothing. Run `painting` on a new painting before See-through.
265
+
266
+ ### And `painting` reads an occluded hip (issue #372)
267
+
268
+ A skirt hides the hips on most clothed figures, and the keypoint file says so with
269
+ `occluded`. `painting` refused that state, which taught a writer to call an
270
+ estimated hip `observed`. It now reads an occluded hip when the arm's gap from it is
271
+ well defined: the hip carries a position (inside the image, as the keypoint reader
272
+ holds every position) and that position's pixel is figure (PAINTING.md, *An occluded
273
+ hip*). No other command changed, and no byte any build writes moves.
274
+
275
+ | change | before | after |
276
+ | --- | --- | --- |
277
+ | a hip `occluded` with a position, on the figure | `PAINTING_JOINTS_STATED` (exit 1) | read: its arm's gap is measured from it, and the run passes or fails on the gaps |
278
+ | a hip `occluded` with a position, on the background | `PAINTING_JOINTS_STATED` | `PAINTING_JOINT_ON_FIGURE`, as an observed joint there is |
279
+ | a hip `occluded` with no position, or `missing` | `PAINTING_JOINTS_STATED` | the same rule; the detail now says a hip may be occluded with a position |
280
+ | a wrist, knee or ankle `occluded` | `PAINTING_JOINTS_STATED` | unchanged; the detail adds that only a hip may be occluded |
281
+ | the `joints:` line | `joints: <n> observed, <m> on the figure` | the same when no hip is occluded; otherwise `joints: <n> observed, <k> occluded with a position (<hips>), <m> on the figure` |
282
+ | a gap line read from an occluded hip | — | ends ` (<hip> occluded, read at its position)` |
283
+
284
+ What to do: write a hip a skirt hides as `occluded` with its estimate. A file that
285
+ wrote it `observed` reads the same gaps as before.