rig-parts 2.0.5 → 2.1.1
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 +34 -19
- package/docs/AUTHORING.md +177 -10
- package/docs/PAINTING.md +62 -3
- package/docs/STABILITY.md +1 -1
- package/package.json +1 -1
- package/src/paintcheck.ts +126 -21
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.
|
|
954
|
-
(
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
occluded
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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>` (
|
|
2157
|
-
* See-through runs on it, read off its pixels and the body-18 joints
|
|
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(
|
|
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
|
@@ -3077,6 +3077,168 @@ max and absolute diff at native scale, then
|
|
|
3077
3077
|
inspect neck boundaries at native and 4x nearest-neighbour scale for tears or
|
|
3078
3078
|
ghosts. A green build alone does not certify the source art.
|
|
3079
3079
|
|
|
3080
|
+
### Recipe: a Form 1 idle (living illustration, issue #932)
|
|
3081
|
+
|
|
3082
|
+
A Form 1 living illustration (an anime character CG with no joint-cut `limbs` block)
|
|
3083
|
+
drives **the body with keyframes** and **every cloth and hair chain with `physics`
|
|
3084
|
+
constraints**. Plain `propose` (without `--cloth`) drafts keyframed harmonic `chain`
|
|
3085
|
+
tracks on every hair, fringe, sleeve, skirt and strap chain and writes no
|
|
3086
|
+
`constraints`, while `propose --cloth` replaces only the `cloth`-declared tags
|
|
3087
|
+
(`bottomwear`, `back hair`, `front hair`) and leaves `handwear` sleeves and
|
|
3088
|
+
`topwear` straps on keyframed chains. To author a Form 1 idle where cloth and hair
|
|
3089
|
+
all move by physics instead of keyframed sine chains:
|
|
3090
|
+
|
|
3091
|
+
1. **Key only the body carrier in `motion.tracks`.** Keep the torso breath on `chest`
|
|
3092
|
+
(`translatey` and `scalex`), the upper-body sway chain `chest` → `neck` → `head`
|
|
3093
|
+
on `rotate`, and `motion.blink`. Remove every `{ "chain": "…" }` track on hair
|
|
3094
|
+
(`hairback_*`, `bang_*`, `lock*`), sleeves (`sleeve_*`), skirts (`skirt_*`), and
|
|
3095
|
+
straps (`robe_*`). Whether `hip` is also keyed at the root of the sway chain
|
|
3096
|
+
(`hip` → `chest` → `neck` → `head`) depends on what sits below the chest:
|
|
3097
|
+
- Under keyframed `propose` (§3, *A standing cloth is the consumer's to drive*,
|
|
3098
|
+
issue #502), `skirt_*` chains carry keyframed `rotate` tracks at disparate
|
|
3099
|
+
phases (`0.08..0.38`), so keying `hip` in `rotate` puts `hip_ctl` above four
|
|
3100
|
+
keyframed children (`chest_ctl`, `skirt_l0`, `skirt_c0`, `skirt_r0`) whose
|
|
3101
|
+
phases disagree and fails `CHAIN_LAG`. Once `skirt_*` moves to `physics`
|
|
3102
|
+
constraints, `hip`'s only keyframed child is `chest`: a 4-link body chain
|
|
3103
|
+
`hip` → `chest` → `neck` → `head` (`phase: 0.20, 0.25, 0.30, 0.35`, with
|
|
3104
|
+
`amp(hip) <= amp(chest)`) passes `CHAIN_LAG` and drives the skirt from a moving
|
|
3105
|
+
parent (clearing `CLOTH_SETTLES` on the default world with `0` warnings).
|
|
3106
|
+
- Two geometric limits then decide whether `hip` is left still or keyed, and by
|
|
3107
|
+
how much:
|
|
3108
|
+
1. **Standing feet (`shoes` and `legwear`):** On a full-body standing rig where
|
|
3109
|
+
`shoes` sit on `root` (`BREATH_VISIBLE` enforces `feet_heat_max: 0`) and
|
|
3110
|
+
`legwear` rides `hip` as a rigid region (as on `examples/demo`), large `hip`
|
|
3111
|
+
sway slides the shins across the stationary shoes unless the legs use the
|
|
3112
|
+
planted-feet two-bone IK recipe (*Recipe: a two-bone IK chain with a planted
|
|
3113
|
+
target (issue #570)*) or the crop ends above the ankles. Without leg IK on
|
|
3114
|
+
`demo`, either leave `hip` unkeyed or keep `hip` `rotate` `amp <= 0.4°` so
|
|
3115
|
+
ankle shear stays sub-pixel.
|
|
3116
|
+
2. **Waist and upper-skirt binding (`CHECK_TIP_OVER_ROOT` on `bottomwear`):**
|
|
3117
|
+
`TIP_OVER_ROOT` splits `bottomwear`'s box (`y = 400..647` on `demo`, midpoint
|
|
3118
|
+
`y = 523.5`) into its upper half (`root_rig_px`) and lower half
|
|
3119
|
+
(`tip_rig_px`) and requires `tip / root >= 1.4725`. When
|
|
3120
|
+
`meshes.bottomwear.segments` binds the waist to `["hip", [x0, y0], [x1, y0]]`
|
|
3121
|
+
and the root link `skirt_*0` (`y = 432..499` on `demo`) also lies inside the
|
|
3122
|
+
upper half, moving `hip` while applying `"wind"` to `skirt_*0` inflates
|
|
3123
|
+
`root_rig_px` past `tip_rig_px / 1.4725`. To key `hip` when `bottomwear` is
|
|
3124
|
+
bound to it, set `"wind": 0` on the upper-half link `skirt_*0` and concentrate
|
|
3125
|
+
`"wind"` on the lower links (`skirt_*1..2`) so the hem travels at least
|
|
3126
|
+
`1.4725x` further than the waist.
|
|
3127
|
+
2. **Drive every cloth and hair link in `constraints`.** Give each link `<chain>0 ..
|
|
3128
|
+
<chain>(n-1)` a `"type": "physics"` entry with `"rotate": 1` and `"mix": 1` on the
|
|
3129
|
+
link bone itself (`sleeve_r0`, never `sleeve_r0_ctl`). A single ornament bone with
|
|
3130
|
+
no `length` (a bow, hairpin or earring) cannot take `"rotate"` (rig-c's `A23`) and
|
|
3131
|
+
takes `"x"` and `"y"` translation springs instead (e.g. `"x": 0.5, "y": 1,
|
|
3132
|
+
"inertia": 0.3, "strength": 160, "damping": 0.88, "mass": 1, "mix": 1`).
|
|
3133
|
+
|
|
3134
|
+
**How a physics rig passes `check`'s loop reading, `CHAIN_LAG`, and `STRESS_OFF_CHAIN`:**
|
|
3135
|
+
|
|
3136
|
+
- **`CHECK_LOOP_CLOSES` (`loop:` and `loop physics:`):** `check` strips every
|
|
3137
|
+
`physics` constraint (`withoutPhysics` in `src/check.ts`) when rendering the loop
|
|
3138
|
+
comparison frames (`f0000` vs the final frame at `motion.duration` at 12 fps), and
|
|
3139
|
+
lists the stripped constraints on `loop physics: <N> physics constraint(s) left out
|
|
3140
|
+
of the loop's frames, reported, not judged`. Because `physics` is left out of that
|
|
3141
|
+
comparison, the loop bar judges only the keyframed body: when `motion.duration` is
|
|
3142
|
+
a whole number of `1/12` s ticks (`CHECK_LOOP_LAST_FRAME_AT_DURATION`, within
|
|
3143
|
+
`1e-9` s) and every body track's `period` divides `motion.duration`
|
|
3144
|
+
(`CONFIG_PERIOD_DIVIDES_DURATION`), the loop reads `max |d| 0 (0 required)`
|
|
3145
|
+
regardless of how many physics springs are active. When baking a rendered loop file
|
|
3146
|
+
with physics **on** (`rig-parts build --loop` or `rig-parts loop`), frame 0 starts
|
|
3147
|
+
from `rigc render`'s physics reset while the end of cycle 1 carries the springs'
|
|
3148
|
+
motion; pass `--lead-in <n>` (for example `build --loop --lead-in 3`) so `<n>`
|
|
3149
|
+
warm-up cycles run before the final cycle is cut.
|
|
3150
|
+
- **`CHAIN_LAG`:** Reads only parent-child pairs of bones that both carry keyframed
|
|
3151
|
+
`rotate` tracks of the same period. Unkeyed physics chains are not read by
|
|
3152
|
+
`CHAIN_LAG`; only the keyframed body rotation chain (`chest_ctl > neck > head_ctl`,
|
|
3153
|
+
or `hip_ctl > chest_ctl > neck > head_ctl` when `hip` is keyed) is checked,
|
|
3154
|
+
requiring a positive lag step in `0.001 .. 0.25` of a cycle and non-decreasing
|
|
3155
|
+
amplitudes (`amp(hip) <= amp(chest) <= amp(neck) <= amp(head)`).
|
|
3156
|
+
- **`STRESS_OFF_CHAIN`:** For each `handwear` mesh, walks up the bone hierarchy from
|
|
3157
|
+
its bound sleeve links to find the nearest ancestor on a 2+-link keyframed rotation
|
|
3158
|
+
chain. Because the torso and head form a keyframed rotation chain above the sleeves,
|
|
3159
|
+
`STRESS_OFF_CHAIN` turns that chain's root (`chest_ctl` or `hip_ctl`) by `+90°`,
|
|
3160
|
+
`-90°`, and `180°`; the sleeve links hang wholly under `chest`, so each sleeve
|
|
3161
|
+
rotates rigidly (`ratio 1`, `reading 1 <= bar 1`, `PASS`) without needing keyframed
|
|
3162
|
+
tracks on `sleeve_*`.
|
|
3163
|
+
|
|
3164
|
+
**Starting floor (not the target) — getting a first green build from rig measurements:**
|
|
3165
|
+
|
|
3166
|
+
The formulas and values in the table below are **starting points that pass the bars
|
|
3167
|
+
(the floor), not the target**. A build at the floor clears every check with
|
|
3168
|
+
sub-degree sway (`0.7°..1.0°`) and `2.4` rig px of breath (`0.21 %` of figure height
|
|
3169
|
+
`H`), which is barely perceptible at full stage scale. Use this table only to reach a
|
|
3170
|
+
first green build; size the finished motion from the rig's own ceiling using the
|
|
3171
|
+
step-up method below.
|
|
3172
|
+
|
|
3173
|
+
| quantity | what to measure on the rig | floor starting value (passes the bars; step up from here) |
|
|
3174
|
+
| --- | --- | --- |
|
|
3175
|
+
| `motion.duration` and `track.period` | `check`'s 12 fps grid (`1/12` s per frame) | Choose `N` frames (e.g. `N = 40`, `duration = 40 / 12 = 3.3333333333333335` s, or `N = 48`, `duration = 4.0` s) and set every body track's `period = duration`. `CHECK_LOOP_LAST_FRAME_AT_DURATION` requires `duration` to land on a `1/12` s tick, and `CONFIG_PERIOD_DIVIDES_DURATION` requires `duration / period` to be an integer. |
|
|
3176
|
+
| Torso breath (`chest` `translatey`, `scalex`) | Figure height `H` (`parts.json` union height, kept near `1,100..1,200` rig px via `assemble.rig_scale`), torso span `L_torso = hypot(hip.x - neck.x, hip.y - neck.y)` from `bones`, and `BREATH_VISIBLE`'s `torso_heat_mean` vs `torso_floor` (pictured in `motion_heat.png`) | Floor: peak-to-peak vertical travel at `1.25 %` of `L_torso` (`amp = round2(0.00625 * L_torso)`, `base = amp`, `phase = 0.0`, i.e. `amp = 1.2` rig px on `demo`), paired with `chest` `scalex` `amp = 0.008`, `base = 1.008`, `phase = 0.0`. `check` scales the reference floor (`2.13` at `render_scale = 0.4211`) by `render_scale / 0.4211` and isolates the breath by posing only `chest`/`hip` `translate` and `scale` tracks. |
|
|
3177
|
+
| Upper-body sway (`chest` → `neck` → `head` `rotate`) | Shoulder lever arm `R_shoulder = \|sleeve_s0.x - chest.x\|` in `bones`, and `CHAIN_LAG` / `TIP_OVER_ROOT` on `handwear` | Floor: `amp(chest) = 0.7°`, `amp(neck) = 0.85°`, `amp(head) = 1.0°` with lag `0.05` (`phase: 0.25, 0.30, 0.35`). **Set `chest` `rotate` `phase` to `0.25` (quadrature, 90° from `chest` `translatey`'s `phase: 0.0`), never near `0.0`:** at `phase ≈ 0`, vertical breath `y(t)` and shoulder lift `Δx · θ(t)` reinforce on one shoulder and cancel on the other, inflating `root_rig_px` on the reinforced sleeve and dropping `tip_rig_px / root_rig_px` below `1.4725`. At `phase = 0.25`, vertical breath and rotational sway are orthogonal on both shoulders alike. |
|
|
3178
|
+
| Sleeve physics (`sleeve_<s>0..2` under `chest`) | `CHECK_TIP_OVER_ROOT` (`ratio >= 1.4725` on each `handwear` part) and `CHECK_ANIMATIONS_DO_NOT_FOLD` | Set `"rotate": 1, "mix": 1, "inertia": 1.0, "damping": 0.75, "mass": 1` and taper `strength` down the chain: `[45, 30, 20]` for a 3-link sleeve (`1 : 0.67 : 0.44`). Stiff follow-through springs (`strength: 200`, `inertia: 0.2`) barely bend under a `3.333` s idle and move the sleeve as a rigid unit (`ratio ≈ 1.19..1.22`, failing `TIP_OVER_ROOT`); tapering `45 → 30 → 20` lets cumulative link rotation amplify cuff travel past `2.0x` the shoulder travel while keeping `0` folded triangles in `wave` and `hop`. |
|
|
3179
|
+
| Hair physics (`hairback_*`, `bang_*` under `head`) | `CLOTH_SETTLES` (`>= 0.01694` rig px/s to the last frame), `CHECK_TEXTURE_STRETCH` (`<= 1.926544`), and `CHECK_ANIMATIONS_DO_NOT_FOLD` | Taper `strength` down each `n`-link chain by `strength_0 * (1 - 0.35 * i / (n - 1))`: on long back-hair chains, `strength_0 = 90` (`[90, 74.25, 58.5]` for 3 links), `inertia = 0.8`, `damping = 0.85`; on shorter fringe chains (`bang_*`), `strength = [130, 105]`, `inertia = 0.2`, `damping = 0.9`. |
|
|
3180
|
+
| Skirt / `bottomwear` physics (`skirt_*` under `hip`) | `CHECK_TIP_OVER_ROOT` on `bottomwear` (`ratio >= 1.4725`), `CHECK_ANIMATIONS_DO_NOT_FOLD` on `hop`, and `CLOTH_SETTLES` | Keep high restoration `strength` (`[200, 165, 130]` for 3 links, `inertia = 0.5`, `damping = 0.82`) so `bottomwear` does not fold during `hop`. With `hip` unkeyed, floor `"wind": [0.4, 0.6, 0.8]` passes `TIP_OVER_ROOT` (`ratio 1.83`); with `hip` keyed in `rotate` (`amp <= 0.4°`, `phase: 0.20`), set `"wind": 0` on the upper-half link `skirt_*0` and put `"wind"` on the lower links (`[0, 15, 40]`) so `tip / root` stays above `1.4725` and `CLOTH_SETTLES` clears on the default world. |
|
|
3181
|
+
|
|
3182
|
+
**Sizing the motion from the rig's own ceiling (step up until a limit refuses, then back off):**
|
|
3183
|
+
|
|
3184
|
+
You do not need an external reference rig to decide how large the breath, sway, and
|
|
3185
|
+
cloth motion should be. Once the floor build is green, **raise each motion family in
|
|
3186
|
+
steps (`1.3x..1.5x` per step) until the rig's own limits refuse a step, then back off
|
|
3187
|
+
one or two steps below that ceiling**:
|
|
3188
|
+
|
|
3189
|
+
1. **Gate ceiling (`check`'s upper-bound bars):** Step up the amplitude or physics
|
|
3190
|
+
drive until one of `check`'s upper limits fires — `CHECK_ANIMATIONS_DO_NOT_FOLD`
|
|
3191
|
+
(triangles flipping on `idle`, `wave`, or `hop`), `CHECK_TEXTURE_STRETCH` (an edge
|
|
3192
|
+
stretching or compressing past `1.926544`), `CHECK_TIP_OVER_ROOT` (`ratio < 1.4725`
|
|
3193
|
+
when vertical breath or `hip` sway carries the upper half of a garment faster than
|
|
3194
|
+
its tip swings), or `CHECK_STILL_REGIONS_DARK` (face drift relative to `head`).
|
|
3195
|
+
2. **Visual ceiling (`motion_heat.png` and `extremes/`):** Inspect `motion_heat.png`
|
|
3196
|
+
and the extreme-pose renders (`extremes/torso_y_min.png`, `torso_y_max.png`,
|
|
3197
|
+
`head_rot_min.png`, `head_rot_max.png`) at each passing step. Where a rigid region
|
|
3198
|
+
or mesh boundary shears across a neighbour (collar against neck, waist against
|
|
3199
|
+
torso, shin against shoe), the eye stops the sweep before the mesh bars trip.
|
|
3200
|
+
3. **Back off to the working size:** Pick the step at roughly `40 %..60 %` of the
|
|
3201
|
+
gate ceiling (one or two steps below the first refusal), where `extremes/` shows no
|
|
3202
|
+
seam shear and `TEXTURE_STRETCH` stays near `1.10..1.30`.
|
|
3203
|
+
|
|
3204
|
+
Stepping each motion family on `examples/demo` (`rig 832x1216`, figure height
|
|
3205
|
+
`H = 1138` rig px, `L_torso = 192` rig px, `R_shoulder = 95..96` rig px;
|
|
3206
|
+
`rig-c 3.1.0`, `@esotericsoftware/spine-core 4.3.13`) measures the floor, the
|
|
3207
|
+
backed-off working size, the gate ceiling (last passing step), and the first failing
|
|
3208
|
+
step as fractions of `H`:
|
|
3209
|
+
|
|
3210
|
+
| motion family on `examples/demo` (`H = 1138` rig px) | floor (first green build) | backed-off working size | gate ceiling (last green step) | first failing step and the limit / frame that stopped it |
|
|
3211
|
+
| --- | --- | --- | --- | --- |
|
|
3212
|
+
| **Torso breath** (`chest` `translatey` `amp = base`, p2p `2 * amp`, with `scalex` scaled in proportion; sway at `2.0° / 2.5° / 3.0°`) | `amp = 1.2` (`2.4` rig px p2p = **`0.21 %` of `H`**; `torso_heat_mean 18.045` vs `floor 3.809`) | `amp = 2.4..3.6` (`4.8..7.2` rig px p2p = **`0.42..0.63 %` of `H`**; `torso_heat_mean 40.933..50.409`) | `amp = 6.0` (`12.0` rig px p2p = **`1.05 %` of `H`**; `torso_heat_mean 64.77`, `handwear_r` `ratio 1.656`, `TEXTURE_STRETCH 1.192` on `topwear` tri 208, idle frame 10) | `amp = 8.0` (`16.0` rig px p2p = **`1.41 %` of `H`**) — stopped by **`CHECK_TIP_OVER_ROOT` on `handwear_r`** (`root 8.11`, `tip 11.854` rig px, `ratio 1.462 < 1.4725`, `carried_by sleeve_r0, breath_rig_px 8`): vertical breath carries the upper sleeve faster than cuff swing grows. |
|
|
3213
|
+
| **Shoulder & head sway** (`chest` / `neck` / `head` `rotate` at `phase 0.25 / 0.30 / 0.35`, `breath amp = 2.4`; shoulder travel = `handwear` `root_rig_px`) | `0.7° / 0.85° / 1.0°` — shoulder `1.856..2.079` rig px (**`0.16..0.18 %` of `H`**), cuff `4.21..4.994` rig px (**`0.37..0.44 %` of `H`**) | `2.5° / 3.0° / 3.6°` — shoulder `6.001..6.479` rig px (**`0.53..0.57 %` of `H`**), cuff `14.577..18.144` rig px (**`1.28..1.59 %` of `H`**), `TEXTURE_STRETCH 1.099` (`hair_back` tri 185, idle frame 16); collar seam visible in `extremes/` above `3.5°` (`0.72..0.77 %` of `H`) | `8.0° / 9.6° / 11.2°` — shoulder `18.648..19.031` rig px (**`1.64..1.67 %` of `H`**), cuff `46.699..58.15` rig px (**`4.10..5.11 %` of `H`**), `TEXTURE_STRETCH 1.643` (`hair_back` tri 225, idle frame 37, `min_ratio 0.609`) | `10.0° / 12.0° / 14.0°` — shoulder `23.322..23.734` rig px (**`2.05..2.09 %` of `H`**), cuff `58.462..72.018` rig px (**`5.14..6.33 %` of `H`**) — stopped by **`CHECK_TEXTURE_STRETCH` on `hair_back` triangle 225 (edge 29–28, idle frame 37)** (`ratio 0.439`, `severity 2.279 > 1.926544`); at `15.0°`, **`CHECK_ANIMATIONS_DO_NOT_FOLD`** also fails on `idle` mesh `hair_back` frame 36 (`1/226` tris). |
|
|
3214
|
+
| **Sleeve cuff tip** (`handwear` `tip_rig_px` via `sleeve_*_phys` `inertia` at `2.5° / 3.0° / 3.6°` sway, `strength: [45, 30, 20]`) | Stiff `strength: [200, 165, 130], inertia: 0.2` at `0.7°` sway fails `TIP_OVER_ROOT` (`tip 2.307..2.366` rig px = **`0.20..0.21 %` of `H`**, `ratio 1.19..1.216`) | `inertia = [1.0, 1.0, 1.0]` — cuff `14.577..18.144` rig px (**`1.28..1.59 %` of `H`**, `ratio 2.25..3.023`, `0` folds across all 8 clips) | `inertia = [1.0, 1.0, 1.0]` (cuff **`1.28..1.59 %` of `H`** at `2.5°` sway; reaches **`4.10..5.11 %` of `H`** at the `8.0°` sway ceiling with `0` folds) | `inertia = [1.2, 1.4, 1.6]` — stopped by **`CHECK_ANIMATIONS_DO_NOT_FOLD` on `wave` frame 14**, mesh `handwear_l` (`2/154` triangles on 5 frames; at `[1.5, 1.8, 2.0]`, cuff reaches `36.16..41.449` rig px = **`3.18..3.64 %` of `H`** and folds on `wave` frame 13 and `hop` frame 8). |
|
|
3215
|
+
| **Skirt hem tip (`hip` unkeyed)** (`bottomwear` `tip_rig_px` via `skirt_*_phys` `wind = [0.4, 0.6, 0.8] * w`, `strength: [200, 165, 130]`) | `w = 1` (`wind: [0.4, 0.6, 0.8]`) — hem `0.271` rig px (**`0.024 %` of `H`**, `root 0.148`, `ratio 1.83`) | `w = 15..22` (`wind: [6, 9, 12]..[8.8, 13.2, 17.6]`) — hem `3.942..5.64` rig px (**`0.35..0.50 %` of `H`**, `root 2.205..3.261`, `ratio 1.788..1.73`, `TEXTURE_STRETCH 1.126..1.195` on `bottomwear` tri 216, idle frame 6) | `w = 60` (`wind: [24, 36, 48]`) — hem `14.301` rig px (**`1.26 %` of `H`**, `root 9.364` = `0.82 %` of `H`, `ratio 1.527`, `TEXTURE_STRETCH 1.715` on `bottomwear` tri 216, idle frame 6) | `w = 80` (`wind: [32, 48, 64]`) — hem `18.476` rig px (**`1.62 %` of `H`**, `root 12.613`) — stopped by **`CHECK_TIP_OVER_ROOT` on `bottomwear`** (`ratio 1.465 < 1.4725`), **`CHECK_TEXTURE_STRETCH` on `bottomwear` tri 216 (edge 159–144, idle frame 6)** (`ratio 0.474`, `severity 2.108`), and **`CHECK_ANIMATIONS_DO_NOT_FOLD` on `hop` frame 8** (`1/292` tris). |
|
|
3216
|
+
| **Lower-body sway (`hip` keyed)** (`hip` `rotate` at `phase: 0.20` leading `chest 0.25 → neck 0.30 → head 0.35`, `skirt_*_phys` `wind` on links `1..2`) | `hip` `amp = 0.4°` with `wind: [0, 15, 40]` — waist `2.742` rig px (**`0.24 %` of `H`**), hem `4.239` rig px (**`0.37 %` of `H`**, `ratio 1.546`), `0` `CLOTH_SETTLES` warnings | `hip` `amp = 0.4°`, `wind: [0, 15, 40]` (waist **`0.24 %` of `H`**, hem **`0.37 %` of `H`**, `TEXTURE_STRETCH 1.259` on `bottomwear` tri 180, idle frame 5) | `hip` `amp = 0.4°`, `wind: [0, 15, 40]` | `hip` `amp = 0.6°` (`wind: [0, 18, 48]`) — stopped by **`CHECK_TIP_OVER_ROOT` on `bottomwear`** (`root 4.128` = `0.36 %` of `H`, `tip 5.263` = `0.46 %` of `H`, `ratio 1.275 < 1.4725`; also fails at `amp = 0.4°` if link `0` carries `wind: [4, 6, 8]`, `ratio 0.786`). |
|
|
3217
|
+
|
|
3218
|
+
**Reproducing the backed-off working build on `examples/demo`:**
|
|
3219
|
+
|
|
3220
|
+
Write `work/form1_config.json` from `examples/demo/config.json` by replacing
|
|
3221
|
+
`motion.tracks` with the six body tracks (`chest` `translatey` `amp: 2.4, phase: 0,
|
|
3222
|
+
base: 2.4`; `chest` `scalex` `amp: 0.008, phase: 0, base: 1.008`; `hip` `rotate`
|
|
3223
|
+
`amp: 0.4, phase: 0.20`; `chest` `rotate` `amp: 2.5, phase: 0.25`; `neck` `rotate`
|
|
3224
|
+
`amp: 3.0, phase: 0.30`; `head` `rotate` `amp: 3.6, phase: 0.35`, all at
|
|
3225
|
+
`period: 3.3333333333333335`), setting `sleeve_*_phys` to `inertia: 1.0, damping: 0.75,
|
|
3226
|
+
strength: [45, 30, 20]`, and setting `skirt_*_phys` to `inertia: 0.5, damping: 0.82,
|
|
3227
|
+
strength: [200, 165, 130], wind: [0, 15, 40]`, and pointing `motion.animations_from`
|
|
3228
|
+
at `../examples/demo/animations.json` and `generation.control.skeleton` at
|
|
3229
|
+
`../examples/demo/pose.json`, because both resolve against the config's own folder. Running `rig` and `check` produces:
|
|
3230
|
+
|
|
3231
|
+
```
|
|
3232
|
+
loop: idle 41 frame(s) at 12 fps, f0000 vs f0040 (t = 3.333333333333333s) with the rig's 29 physics constraint(s) left out (named on the `loop physics:` line; rendered with them, the ends need not meet — `build --loop --lead-in <n>`): max |d| 0 (0 required)
|
|
3233
|
+
BREATH_VISIBLE: PASS — torso_parts topwear; torso_heat_mean 48.993; torso_floor 3.809; feet_parts shoes; feet_heat_max 0; feet_ceiling 0
|
|
3234
|
+
CHAIN_LAG: PASS — chains hip_ctl > chest_ctl > neck > head_ctl: period 3.333s, phase 0.200 0.250 0.300 0.350, amp 0.400 2.500 3.000 3.600; lags hip_ctl > chest_ctl +0.050 | chest_ctl > neck +0.050 | neck > head_ctl +0.050; min_step 0.05; step_floor 0.001; other_period none; unread none; first_violation null
|
|
3235
|
+
TIP_OVER_ROOT: PASS — instrument geometry; parts part handwear_l, root_rig_px 5.905, tip_rig_px 20.07, ratio 3.399 | part handwear_r, root_rig_px 6.404, tip_rig_px 15.951, ratio 2.491 | part bottomwear, root_rig_px 2.742, tip_rig_px 4.239, ratio 1.546; ratio_floor 1.4725
|
|
3236
|
+
TEXTURE_STRETCH: PASS — meshes 9; triangles 1816; frames 41; severity 1.259; severity_ceiling 1.926544
|
|
3237
|
+
ANIMATIONS_DO_NOT_FOLD: PASS — 8 animation(s) read by rigc render --folds at 12 fps, with its 29 physics constraint(s) on and off (idle, tilt, look_l, look_r, smile, startled, wave, hop): 0 folding mesh(es), 0 not declared in may_fold (<= 0)
|
|
3238
|
+
CLOTH_SETTLES: CLEAR — 12 physics chain(s) read by their tip on the idle as rendered (41 frame(s) at 12 fps over 3.333 s, physics on): 0 of them settle before the loop's length — 0 warning(s), never a bar
|
|
3239
|
+
check: PASS; 11 of 11 bar(s) measured, 0 skipped; 0 warning(s) (CLOTH_SETTLES, not a bar)
|
|
3240
|
+
```
|
|
3241
|
+
|
|
3080
3242
|
## 4. The command order
|
|
3081
3243
|
|
|
3082
3244
|
Two steps are external: the See-through runs (by any route; the optional `comfy seethrough` adapter is only a client for a ComfyUI box). Everything else is this tool.
|
|
@@ -3657,7 +3819,7 @@ existing hardlinks are refused before artifact output; the ordinary mode names
|
|
|
3657
3819
|
| `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
3820
|
| `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
3821
|
| `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
|
|
3822
|
+
| `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
3823
|
| `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
3824
|
| `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
3825
|
| `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 +4771,26 @@ See-through with another seed.
|
|
|
4609
4771
|
|
|
4610
4772
|
### painting
|
|
4611
4773
|
|
|
4612
|
-
`rig-parts painting --keypoints <k.json> <painting.png>` (
|
|
4613
|
-
before See-through runs on it. *
|
|
4774
|
+
`rig-parts painting --keypoints <k.json> [--person <id>] [--form standee|illustration] <painting.png>` (issues #350, #931) reads the painting
|
|
4775
|
+
before See-through runs on it. Under `--form standee` (the default), *background* is ground (alpha 8 or less, or every channel
|
|
4614
4776
|
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.
|
|
4777
|
+
background pixel on the straight line between two joints. Under `--form illustration`
|
|
4778
|
+
(a framed illustration with a painted room or a figure cut by the crop), the source
|
|
4779
|
+
and keypoint checks still run, `PAINTING_JOINTS_STATED` requires at least one joint
|
|
4780
|
+
with a position, `PAINTING_JOINT_ON_FIGURE` checks that every stated joint's pixel is
|
|
4781
|
+
painted (alpha above 8), and the standee border, 8-joint and gap rules report `SKIP`
|
|
4782
|
+
with their reason, never `PASS`. [PAINTING.md](PAINTING.md)
|
|
4616
4783
|
says why each requirement exists and what breaks without it.
|
|
4617
4784
|
|
|
4618
4785
|
| rule | means | change |
|
|
4619
4786
|
| --- | --- | --- |
|
|
4620
4787
|
| <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
|
|
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*) |
|
|
4788
|
+
| <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*) |
|
|
4789
|
+
| <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*) |
|
|
4790
|
+
| <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 |
|
|
4791
|
+
| <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*) |
|
|
4792
|
+
| <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*) |
|
|
4793
|
+
| <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
4794
|
|
|
4628
4795
|
### The optional ComfyUI adapter (`comfy paint`, `comfy seethrough`)
|
|
4629
4796
|
|
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`).
|
|
57
|
-
|
|
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.
|
|
3
|
+
"version": "2.1.1",
|
|
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
|
-
/**
|
|
116
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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(
|
|
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(
|
|
275
|
-
|
|
276
|
-
|
|
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
|
}
|