From 826755804bb642901d45a0a6a1e4413ae16cdee7 Mon Sep 17 00:00:00 2001 From: firejune Date: Wed, 21 Oct 2026 03:54:56 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20the=20settle=20is=20a=20stance=20ch?= =?UTF-8?q?ange=20=E2=80=94=20shoes=20split=20through=20the=20producer,=20?= =?UTF-8?q?the=20splayed=20leg=20comes=2040=20%=20of=20the=20way=20in=20(#?= =?UTF-8?q?13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 7 corrects the left ankle keypoint ([848, 2140]; the script's lands 11.8 px off the leg art), so the producer's limbs rule splits the shoes: propose prints 44 bones and 0 LINT, step 8's build prints `limb cut: "shoes" opaque 13735 = "foot_r" 8071 + "foot_l" 5664`. The hand-placed set in docs/KEYPOINTS.md is fixed at the source too. hello.ts replaces the hip-only settle with a stance change solved from rig.json: the splayed right leg's ankle slides 33.2 px inward along the floor (40 % of the way to parallel; tilt 15.96 -> 12.87 deg; leg_r0 +9.61, leg_r1 -13.63) by two-bone IK, the left leg follows the hip (-6 px) by IK, feet unkeyed and level, the skirt's first link follows the right thigh; 3.15-4.75 s, smoothstep. The hello is 4.75 s and loops. 50 % (the probe's figure) failed one bar on the cloth build (topwear|bottomwear ghost 54 > 50 over the hold), so it ships at 40 %; bars unchanged. At 40 % all pairs are inside on 9b and 9c, idle and hello (worst topwear|bottomwear 3/44), pass-through 0, no folds, FEET_STAY_LEVEL PASS, check PASS, build PASS. The right shoe stays drawn toed-out (a true "11" needs a second shoe painting). check-adapters asserts foot_r/foot_l, no shoes slot, 72 bones and the 4.75 s hello. README as written reproduces from a clean folder on parts 1.49.1, byte-identical to the measured build. Closes #13 Co-Authored-By: Roa <342598975+fj-roa[bot]@users.noreply.github.com> --- README.md | 78 +++++++++++------- docs/KEYPOINTS.md | 12 +-- docs/REFERENCE.md | 164 +++++++++++++++++++++++++++++++++++++- scripts/check-adapters.ts | 19 ++++- scripts/hello.ts | 127 +++++++++++++++++++---------- 5 files changed, 316 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 9747bbf..cc4401e 100644 --- a/README.md +++ b/README.md @@ -131,26 +131,33 @@ The first run downloads two pose models (about 300 MB) and some Python packages. should see `18 observed, 0 occluded, 0 missing -> keypoints.json`. No Python? [docs/KEYPOINTS.md](docs/KEYPOINTS.md) has a hand-placed set for this very -painting; save it as `keypoints.json`. You get the same 42 bones without the `LINT` line -step 7 prints for the script's left ankle, and it builds green all the same. +painting; save it as `keypoints.json`. Its left ankle is already where step 7's first line +puts it, and you get the same 44 bones. ### 7. Draft the bones ```sh +bun -e 'const f = "keypoints.json", k = await Bun.file(f).json(); k.people[0].joints.l_ankle.at = [848, 2140]; await Bun.write(f, JSON.stringify(k, null, 1) + "\n")' bunx rig-parts propose --parts work/rig --source inputs/painting.png --out work --keypoints keypoints.json bun ../getting-started/scripts/take.ts work/proposal.json config.json bones meshes regions motion limbs ``` `propose` drafts the bones, the meshes and an idle motion; `take.ts` copies them into the -config. This is where a person corrects the draft. The example uses it as is. - -You should see a note per joint, then a coverage line ending `42 bone(s) — 29 checked and -clean, 1 checked with a LINT line, 12 not checked`. The one `LINT` line begins -`LINT foot_l at [428, 1077], the l_ankle joint and leg_l1's tip, lies on none of leg_l's art -within 1 px along its joint line`: the script put the left ankle just off the leg, so the -left leg is drafted without its ankle cut and `shoes` stays one part. A `LINT` is something -to look at, not a refusal. Open `work/render/landmarks.png` to see the bones over the -painting. +config. This is where a person corrects the draft, and the first line is the example's one +correction. The script puts the left ankle at `[855.7, 2154.1]`, 12 px off the leg's art, +and without the line `propose` says so: `LINT foot_l at [428, 1077], the l_ankle joint and +leg_l1's tip, lies on none of leg_l's art within 1 px along its joint line`, and the left +leg is drafted without its ankle cut, so the shoes stay one part that no leg bone moves. +Moved onto the shin at `[848, 2140]` (painting px), both ankles lie on the leg art and on +the shoes, and `propose` cuts the shoes between the feet, one part per foot under each +shin. A `LINT` is something to look at, not a refusal; this one is worth fixing because +step 9b's hello moves a foot. + +You should see a note per joint, among them `and foot_r, shoes's share below the ankle, on +bone foot_r at the r_ankle joint under leg_r1` and the same for `foot_l`, then +`0 LINT line(s) over 6 mesh(es) and the hip` and a coverage line ending `44 bone(s) — 29 +checked and clean, 0 checked with a LINT line, 15 not checked`. Open +`work/render/landmarks.png` to see the bones over the painting. ### 8. Build the rig @@ -158,8 +165,10 @@ painting. bunx rig-parts build --config config.json --source inputs/painting.png --full inputs/layers/full --head inputs/layers/head --out out --loop ``` -About a minute. You should see `[check] check: PASS; 11 of 12 bar(s) measured, 1 skipped -(STRESS_OFF_CHAIN)`, then `build: PASS`. Open `out/idle.gif`: the character breathes and blinks in a 3.3-second loop. +About a minute. You should see `limb cut: "shoes" opaque 13735 = "foot_r" 8071 + "foot_l" +5664`, the shoes cut in two, then `[check] check: PASS; 11 of 13 bar(s) measured, 2 skipped +(FEET_STAY_LEVEL, STRESS_OFF_CHAIN)`, then `build: PASS`. `FEET_STAY_LEVEL` is skipped +because the idle turns no leg; step 9b's hello does. Open `out/idle.gif`: the character breathes and blinks in a 3.3-second loop. If any line says `FAIL`, nothing was written, and that line says what to change. ### 9. Give it a wave @@ -196,7 +205,7 @@ failure: where a mesh does fold, it names the part and the frames to look at. Step 9 turned joint positions into a motion. A motion can also be written as keys on the bones and handed to the build, which checks it with the rig. -[`scripts/hello.ts`](scripts/hello.ts) writes a four-second hello that way, reading the bones +[`scripts/hello.ts`](scripts/hello.ts) writes a hello of under five seconds that way, reading the bones from the rig step 8 built; the config names the file in `motion.animations_from`, and the build runs again. @@ -207,19 +216,24 @@ bun ../getting-started/scripts/take.ts work/hello-from.json config.json motion.a bunx rig-parts build --config config.json --source inputs/painting.png --full inputs/layers/full --head inputs/layers/head --out out --loop ``` -About a minute and a half. You should see `hello.ts: 4 beats (hm?, heh, wave, settle), 4 s at -24 fps, 16 tracks of 97 keys; sleeve_r0 at most 4.0 deg; hip shift 6.0 px`, then from the -build `animations beside the idle, from …/hello.anim.json as written: hello`, +About a minute and a half. You should see `hello.ts: 4 beats (hm?, heh, wave, settle), 4.75 s +at 24 fps, 16 tracks of 115 keys; sleeve_r0 at most 4.0 deg; settle: right ankle 33.2 px in +(40 % of the way to parallel, 15.96 -> 12.87 deg), hip 6.0 px`, then from the build +`animations beside the idle, from …/hello.anim.json as written: hello`, `ANIMATIONS_DO_NOT_FOLD: PASS — 2 animation(s) read by rigc render --folds at 12 fps (idle, -hello)`, `check: PASS; 11 of 12 bar(s) measured, 1 skipped (STRESS_OFF_CHAIN); 1 warning(s)`, -and `build: PASS`. The warning is `SEAM_OVER_MOTION`, never a bar: where the forearm turns +hello)`, `FEET_STAY_LEVEL: PASS`, `check: PASS; 12 of 13 bar(s) measured, 1 skipped +(STRESS_OFF_CHAIN); 1 warning(s)`, and `build: PASS`. The warning is `SEAM_OVER_MOTION`, never a bar: where the forearm turns at the elbow, a sliver of it shows past the sleeve's cuff. `out/check/build/` now holds `idle` and `hello`. The wave here is the forearm's, with the upper arm kept within 4 degrees of rest. On this painting the upper arm and its puff sleeve are one image under a sleeveless top, so -swinging it out, as step 9's wave does, opens a gap at the armhole. The comment at the top -of `hello.ts` says why each limit is where it is. +swinging it out, as step 9's wave does, opens a gap at the armhole. Then she re-plants her +weight as the arm comes down: her splayed right leg (the picture's left) slides its foot +40 % of the way in toward parallel with the other, the knee bending the way it is painted, her hip +follows 6 px, and both go back by the end of the loop. The feet are the two parts step 7's +correction made; each keeps its shoe level while the leg turns, and the hello never keys +them. The comment at the top of `hello.ts` says why each limit is where it is. ### 9c. Let the hair and the skirt answer @@ -227,7 +241,7 @@ So far the hair and the skirt move only where a key moves them. The parts packag recipe that hands them to physics instead: a `cloth` block in the config names a recipe per See-through layer tag, `propose --cloth` drafts chains with a physics constraint on each link, and the build carries them. Then what moves under them, her breath, her head and the -hello's weight shift, moves them, and no key does. +hello's hip, moves them, and no key does. ```sh bun ../getting-started/scripts/hello.ts out/rig/rig.json --no-skirt-keys @@ -249,9 +263,11 @@ bunx rig-parts build --config config.json --source inputs/painting.png --full in ``` About two and a half minutes. The first line writes the hello again without its three skirt -keys (`hello.ts` then prints `13 tracks of 97 keys` and `skirt left to physics`): step 9b's -settle turns the skirt's top links by hand to follow her hip, and from here physics does -that. `bottomwear` is the skirt's layer; `front hair` is the hair around the face, the +keys (`hello.ts` then prints `14 tracks of 115 keys` and `skirt0_0 follows the thigh, the rest +left to physics`): step 9b's settle turns the skirt's top links by hand to follow her hip and +her right thigh, and from here physics does the hip's part. The thigh's stays a key, on +`skirt0_0`, the recipe's first link over the right thigh, which carries no physics of its +own. `bottomwear` is the skirt's layer; `front hair` is the hair around the face, the fringe and the two locks that hang beside her cheeks, all one layer. `propose` prints what the recipe drafted: `3 tree(s) on one shared swing, skirt_swing` for the skirt, one tree of three links on each side lock (`lockt0_` and `lock1t0_`), and the fringe's @@ -274,12 +290,12 @@ swing at least as far as the one above. The draft gives the bun 0.8 degrees unde From the build you should see `loop physics: 27 physics constraint(s) left out of the loop's frames, reported, not judged`, four `WARN chain` lines from `CLOTH_SETTLES` for the skirt, -`check: PASS; 11 of 12 bar(s) measured, 1 skipped (STRESS_OFF_CHAIN); 5 warning(s) +`check: PASS; 12 of 13 bar(s) measured, 1 skipped (STRESS_OFF_CHAIN); 5 warning(s) (SEAM_OVER_MOTION, CLOTH_SETTLES, not a bar)`, a `lead-in:` line ending `max |d| 0, 0 px over 40 — the jump at the wrap`, and `build: PASS`. Physics starts at rest when an animation starts, and the skirt hangs into place within 2 seconds. Then it rests, because the idle never moves her hip; `CLOTH_SETTLES` says so, and it is a warning, not a failure. The hello's -weight shift moves the skirt again. `--lead-in 1` plays the idle once before +settle moves the skirt again. `--lead-in 1` plays the idle once before `out/idle.gif` is cut from it, so the loop starts after the skirt has settled. ### 10. Play it in your browser @@ -291,9 +307,9 @@ bun ../getting-started/scripts/serve.ts Open . You should see her notice you — "hm?", her head straightening — then smile with narrowed -eyes and two little shrugs, wave with her forearm beside her shoulder, and settle her weight -before it starts again. The locks beside her face trail her head, and the skirt swings after -her hip when she settles. `&under=idle` keeps the idle playing beneath, so she breathes +eyes and two little shrugs, wave with her forearm beside her shoulder, and re-plant her +weight, one foot sliding in toward the other and back, before it starts again. The locks beside her +face trail her head, and the skirt swings after her hip when she settles. `&under=idle` keeps the idle playing beneath, so she breathes through it. Step 9's wave is at ; the menu switches animations. Ctrl+C stops the server. That's the whole chain. The page is @@ -309,7 +325,7 @@ the original three are measured in [docs/REFERENCE.md](docs/REFERENCE.md#use-wha For your own site, with no Spine runtime. The page from step 10 is already this: it loads `skeleton.model.json`, the document a build writes next to the Spine files, and the part -images beside it. Copy `wave-build/` (that document plus 22 part PNGs, 1.6 MB; it holds +images beside it. Copy `wave-build/` (that document plus 28 part PNGs, 1.3 MB; it holds both `idle` and `wave`) next to a page, and start from the play README's [minimal page](https://github.com/rig-c/play#a-minimal-page): about twenty lines that load the player from a CDN, fit the rig to a box and play it. Point its `modelUrl` at diff --git a/docs/KEYPOINTS.md b/docs/KEYPOINTS.md index c875409..da29da0 100644 --- a/docs/KEYPOINTS.md +++ b/docs/KEYPOINTS.md @@ -55,7 +55,7 @@ This is a hand-placed set for the example painting (1664 x 2432). Save it as "r_ankle": { "state": "observed", "at": [455, 2110] }, "l_hip": { "state": "occluded", "at": [970, 1030] }, "l_knee": { "state": "observed", "at": [940, 1655] }, - "l_ankle": { "state": "observed", "at": [850, 2110] }, + "l_ankle": { "state": "observed", "at": [848, 2140] }, "r_eye": { "state": "observed", "at": [825, 275] }, "l_eye": { "state": "observed", "at": [944, 294] }, "r_ear": { "state": "missing" }, @@ -64,10 +64,12 @@ This is a hand-placed set for the example painting (1664 x 2432). Save it as } ``` -With this set, the example rig comes out with 42 bones — a chain per leg — where the -script's joints give 40 and one chain down the middle of both legs. Both build green, and -the README's wave runs on both. [REFERENCE.md](REFERENCE.md#the-leg-chain-moves-with-the-joints) -says why. +With this set, the example rig comes out with 44 bones: a chain per leg, and the shoes cut +into one part per foot under each shin, the same as the script's joints give once the +README's step 7 has moved their left ankle. The feet are cut only when each ankle lies on +its leg's art and on the shoes; `l_ankle` sits at `[848, 2140]` for that, and the README's +step 7 line puts it there whichever set you use. Earlier versions of the parts package gave +other counts; [REFERENCE.md](REFERENCE.md#the-leg-chain-moves-with-the-joints) keeps them. ## What the script writes diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 1625a12..5bc2128 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -10,7 +10,7 @@ quoted from. Three words recur: ## Tested with -Recorded 2026-10-13; *Try it first* re-run twice on 2026-10-20, the second time with step 9b, and once more on 2026-10-21 with step 9c. Everything marked *measured* on this +Recorded 2026-10-13; *Try it first* re-run twice on 2026-10-20, the second time with step 9b, and twice on 2026-10-21, with step 9c and then with the stance settle. Everything marked *measured* on this page was run on this machine with these versions. | | | @@ -64,6 +64,19 @@ readings under *Step 9c* were taken on. One line's wording differs between the t `wrote config.cloth.json:` names each chain and track it removed and inserted, where 1.48.0 named the sections. Step 10 was checked by fetching the page and both models (HTTP 200). +Later on 2026-10-21, with step 7's ankle correction and the stance settle, every `sh` block +was run verbatim in order from an empty folder once more (run 7), `git clone` again replaced +by a copy of the checkout; `bun add` installed parts 1.49.1, rig-c 2.64.1, pose 0.37.0 and +play 1.12.0. Every expected line on the README held, the ones changed by this step included +(`44 bone(s)`, `limb cut: "shoes" …`, step 8's `11 of 13`, the two `hello.ts` lines, steps +9b's and 9c's `12 of 13`). Its outputs were byte-identical to the scratch run the readings +under [*The settle: a stance change*](#the-settle-a-stance-change) were taken on +(`keypoints.json`, `config.json`, `wave.motion.json`, `wave-build/`'s `skeleton.json` and +`skeleton.model.json`, step 9b's and step 9c's `skeleton.model.json`, step 9c's +`skeleton.json`, `skeleton.atlas`, `skeleton.png` and `out/idle.gif`), except +`hello.anim.json`'s `note`, whose wording was changed between the two runs; its tracks are +the same. Step 10 was checked by fetching the page and both models (HTTP 200). + *Try it first* (the CPU-only half) was run end to end twice on the machine above: once command by command while drafting, and once as a smoke, every `sh` block of the finished page extracted and run verbatim in order from an empty folder with an empty Bun cache. The @@ -113,6 +126,10 @@ Run 6 (clean, 2026-10-21, caches warm, the machine's load average 13–16 from o an upper bound): **495.5 s** in all — steps 1 to 10 took 1.3, 5.3, 4.3, 8.5, 63.7, 4.3, 8.6, 101.5, 16.6, 122.7 (9b), 153.5 (9c) and 2.7 s. +Run 7 (clean, 2026-10-21, caches warm, other jobs running, so an upper bound): **416.6 s** +in all — steps 1 to 10 took 1.0, 3.8, 3.7, 6.8, 42.1, 2.1, 5.5, 75.5, 6.4, 119.5 (9b), 146.1 +(9c) and 2.4 s. + Disk after run 2: `my-rig` 79 MB (`node_modules` 37, `inputs` 19, `work` 4.3, `out` 16, `wave-build` 1.6) and Bun's download cache 38 MB — about 120 MB in all. Bun reported `7 packages installed`. @@ -157,6 +174,11 @@ published package checks every build with rigc's own validator, and the ### Step 9b, the hello +The settle this section measures (hip 6 px, 0.7 s, the ankles held) was replaced on +2026-10-21 by a stance change, after step 7 began cutting the shoes into one part per foot; +its readings are under [*The settle: a stance change*](#the-settle-a-stance-change). The rest +of this section is as measured then. + `scripts/hello.ts` writes keys, not joint positions: the face is outside the 18 body joints rig-pose reads, and the arm's limit is an angle, which a key states exactly. It reads the bones from `out/rig/rig.json` — which bones exist, and the legs' lengths and rest angles, @@ -227,7 +249,8 @@ idle key. **What the recipe drafted** (parts 1.48.0, measured): the keyed chains `lock`, `lock1_` and `skirt_r`, `skirt_c`, `skirt_l` and their idle tracks are replaced; the fringe's chains keep -their names and places. The rig goes from 70 bones to 46 (the keyed chains' `_ctl` bones go). +their names and places. The rig goes from 70 bones to 46 (the keyed chains' `_ctl` bones go); +with step 7's ankle correction, from 72 to 48 (the two feet are bones of their own). | piece | chains | constraints | | --- | --- | --- | @@ -254,7 +277,8 @@ none, so the runtime's default world applies: wind (1, 0), gravity (0, 1) (state **The hello's skirt keys.** `hello.ts` keys `skirt_r0`, `skirt_c0` and `skirt_l0` to follow the settle's hip shift; under the recipe those bones do not exist and the swing carries the skirt, so step 9c writes the hello with `--no-skirt-keys` (13 tracks; the other 13 are the -same keys, byte for byte). Without physics the keys are needed: step 9b's build with them left +same keys, byte for byte; since the stance change, 14 tracks, the 14th keying `skirt0_0`, see +below). Without physics the keys are needed: step 9b's build with them left out reads `thigh_l|bottomwear` gap 48 px on the hello, past the bar of 25. **The build's lines** (run on 2026-10-21): `CLOTH_SETTLES: WARN — 9 physics chain(s) …: 4 of @@ -329,6 +353,136 @@ step-10 page sets no world of its own; the player's `environment()` is where a p `&under=idle` plays it (`rigc render` reads one animation at a time); the player's physics against rigc's render frame by frame. +### The settle: a stance change + +Added 2026-10-21. The owner asked for the settle to bring the feet toward a parallel stance +and back, which would also drive the skirt. A read-only probe on this build (parts 1.48.0) +designed it; the readings below are the guide's own, on parts 1.49.1 and rig-c 2.64.1. + +**The feet, through the producer.** The parts package's limbs rule drafts `foot_r` and +`foot_l` under the shins "when every leg's ankle lies on one footwear part — `foot_` on a +bone of its own at the ankle under the shin, `inherit` `"noRotationOrReflection"`, so the +shoe keeps its setup angle while the leg turns" (stated by the parts [AUTHORING][authoring], +*Limbs: cut at their joints*). The script's `l_ankle`, at painting px `[855.7, 2154.1]`, lies +11.761 px from the leg's art along its joint line, so `propose` prints `LINT foot_l … +(ASSEMBLE_LIMB_JOINT_ON_ART)` and leaves `shoes` whole. The hand-placed set's `[850, 2110]` +lies on the leg but 22 px from the shoes, so it leaves them whole too, with a note and no +`LINT`. At `[848, 2140]` both sets give `propose` 44 bones and 0 `LINT` lines, and `build` +prints `limb cut: "shoes" opaque 13735 = "foot_r" 8071 + "foot_l" 5664` (measured). Step 7's +first line moves the ankle there in whichever `keypoints.json` the reader has, and +[KEYPOINTS.md](KEYPOINTS.md)'s set now holds it too. The build's rig has 72 bones (70 +before) and 28 parts; step 8's `check` adds `FEET_STAY_LEVEL`, skipped on the idle, which +turns no leg. The ankle caps are reported missing (`limb cap: … "ankle" … 593 px missing`, +`… 620 px missing`), and `RECOMPOSITE_HOLES` reads the same 402 px hole at the left ankle as +before the split (`error_px 6275`): neither shows while the foot stays level and the shin +draws over it. + +**The painted stance** (rig px, from the rig's leg bones): the right leg (the picture's left) +runs 15.96 degrees from straight down, hip to ankle; the left 8.09 degrees, the same way. The +right shoe is painted turned out 44.4 degrees more than the left (the probe's reading of the +shoes' long axes). + +**The keys** (`scripts/hello.ts`, from the rig's leg lengths and rest angles): + +| | value | +| --- | --- | +| right ankle | 33.2 px in along the floor: 40 % of the 83.0 px to where hip-to-ankle is parallel to the left leg (15.96 → 12.87 degrees) | +| right leg | two-bone IK, the knee bending the way it is painted: `leg_r0` +9.61, `leg_r1` −13.63 degrees at the hold | +| hip | x −6 px, y 0 | +| left leg | ankle held, IK under the hip's move: `leg_l0` +1.70, `leg_l1` −2.65 degrees | +| feet | not keyed: each keeps its shoe level by its inherit and goes with its ankle | +| skirt, step 9b | `skirt_r0` equal to `leg_r0`; `skirt_c0`, `skirt_l0` 1.66 degrees after the hip, as before | +| skirt, step 9c | `skirt0_0` equal to `leg_r0`: the first link of the tree over the right thigh, which takes the swing's turn through a transform constraint and has no physics of its own, so a key adds to it; the rest of the skirt answers the hip by physics | +| timing | out over 3.15–3.75 s as the arm comes down, held to 4.15 s, back over 4.15–4.75 s, smoothstep; the hello is 4.75 s (115 keys at 24 fps), every track at rest at both ends | + +**Bars, written down before measuring** (from rig-c/getting-started#13): hem +`thigh_?|bottomwear` gap ≤ 25 px; knee `shin_?|thigh_?` ghost ≤ 50 px, and `shin_?|foot_?` +within the same; 0 frames of pass-through; the ankles (`leg_?1`'s tips) and the soles (each +foot's lowest vertex) within 2 px of rest (`--geometry`); no folds; `sleeve_r0` within 4 +degrees; every seam pair within gap ≤ 25 and ghost ≤ 50 on `idle` and `hello`, physics on; +`FEET_STAY_LEVEL` PASS; `check: PASS`; `build: PASS`. + +**50 % failed one, and the beat came down to 40 %.** The probe proposed 50 % (41.5 px, +`leg_r0` +11.11, `leg_r1` −15.28 degrees). On step 9c's build, physics on, it read +`topwear|bottomwear` ghost **54** px (frames 86–101, the hold), over the bar of 50: the +skirt's top, turned with `skirt0_0`, pulls off the shirt. Every other pair passed (hem +`thigh_r` 20 / 3, knee 1 / 40), and step 9b's plain build passed everything (`topwear` 1 / +45). At 40 % every bar holds on both builds. The bars were not changed. + +**Readings at 40 %**, `rigc render --candidate out/check/build --animation --fps 24 +--geometry --folds --seam … --pass-through bottomwear:thigh_r,bottomwear:thigh_l +--angle-range …`, physics on where the rig has it (gap / ghost, px, worst frame; `rigc +render` starts physics at rest, so the frames include the skirt hanging into place): + +| pair | 9b `idle` | 9b `hello` | 9c `idle` | 9c `hello` | +| --- | --- | --- | --- | --- | +| upperarm_r \| topwear | 0 / 2 | 8 / 8 | 0 / 2 | 8 / 8 | +| forearm_r \| upperarm_r | 0 / 2 | 1 / 26 | 0 / 2 | 1 / 26 | +| upperarm_l \| topwear | 0 / 0 | 0 / 1 | 0 / 0 | 0 / 1 | +| forearm_l \| upperarm_l | 0 / 1 | 0 / 0 | 0 / 1 | 0 / 0 | +| neck \| topwear | 0 / 0 | 0 / 5 | 0 / 0 | 0 / 5 | +| face \| hair_front | 2 / 0 | 0 / 0 | 0 / 0 | 7 / 47 | +| face \| neck | 0 / 0 | 0 / 1 | 0 / 0 | 0 / 1 | +| hair_back \| face | 0 / 1 | 0 / 0 | 0 / 1 | 0 / 0 | +| topwear \| bottomwear | 0 / 3 | 1 / 38 | 1 / 7 | 3 / 44 | +| thigh_r \| bottomwear | 1 / 0 | 11 / 0 | 6 / 1 | 10 / 2 | +| thigh_l \| bottomwear | 0 / 1 | 11 / 3 | 1 / 2 | 7 / 6 | +| shin_r \| thigh_r | 0 / 0 | 1 / 38 | 0 / 0 | 1 / 38 | +| shin_l \| thigh_l | 0 / 0 | 0 / 5 | 0 / 0 | 0 / 5 | +| shin_r \| foot_r | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 | +| shin_l \| foot_l | SKIP | SKIP | SKIP | SKIP | + +The two `shoes` pairs of the earlier tables are gone with the part; the two foot pairs take +their place, and `shin_l|foot_l` is rigc's `SKIP` as `shoes|shin_l` was. Pass-through: 0 of +81 idle and 0 of 115 hello frames, both thighs, both builds. Folds: none. Feet: the ankles' +y and the soles' lowest y move 0.001 px at most on every frame (the right ankle 142, the +left 146, the soles 26 and 22, world y up); the right ankle's x runs −192.00 to −158.80, +33.2 px. `sleeve_r0` against `chest`: 4.00 degrees out at most. The build: `FEET_STAY_LEVEL: +PASS — 2 animation(s) …: each foot's drawing within ±10 degrees of its setup pose while a bone +above it is turned`, `ANIMATIONS_DO_NOT_FOLD: PASS`, `check: PASS; 12 of 13 bar(s) measured, +1 skipped (STRESS_OFF_CHAIN)`, `build: PASS`, on both 9b and 9c. Every reading is inside its +bar; the worst is `face|hair_front` ghost 47 (the head turn at 0.333 s, as before) and +`topwear|bottomwear` 44. + +**Angles under the hello** (`--angle-range`, span over the frames read, degrees): + +| bone against | 9b | 9c | before, the hip-only settle (9c) | +| --- | --- | --- | --- | +| `leg_r0` against `hip` | 9.61 | 9.61 | — | +| `leg_r1` against `leg_r0` | 13.63 | 13.63 | — | +| `leg_l0`, `leg_l1` | 1.70, 2.65 | 1.70, 2.65 | — | +| `foot_r`, `foot_l` against `hip` | 0, 0 | 0, 0 | — | +| `skirt_r0` / `skirt0_0` against `hip` (keyed) | 9.61 | 10.28 | 3.48 | +| `skirt_swing` against `hip` | — | 1.60 | 2.90 | +| `skirt1_0`, `skirt2_0` against `hip` | 1.66, 1.66 (keyed) | 1.60, 1.92 | 2.90, 3.48 | +| `skirt0_2` against `skirt0_1` | — | 10.44 | 4.74 | +| `skirt2_2` against `skirt2_1` | — | 8.33 | 8.84 | + +The feet's bones do not turn, so the right shoe stays drawn at its painted angle, turned out: +the legs come together, the feet do not become parallel. The skirt answers in two ways: the +tree over the right thigh follows the thigh by its key, and its tip link swings 10.44 degrees +against the link above (4.74 under the hip-only settle); the shared swing and the other +trees answer the hip, and less than before (1.60 against 2.90), because the hip now takes +0.6 s to move where it took 0.35 s. + +**Needs a part** (from the probe, not changed by this step): + +- **A turned-in right shoe.** The toe-out is painted. Turning the foot's image in the + picture's plane reads as a shoe tipped on its side, pushes the sole 13–22 px through the + floor, and past 10 degrees fails `FEET_STAY_LEVEL`. A true parallel stance needs a second + painting of the right shoe, switched in by an attachment key. +- **Thigh art under the hem**, to go further than about 50–75 % of the way: the thigh turns + about the hip 180 px above the hem and its art starts at the hem, so a turn opens a sliver + that only the skirt's link covers, and past about 75 % the skirt mesh itself splits. +- **A lower hip or a painting with the legs closer**, for straight parallel legs: with the + hip where it is the right leg must shorten 18 px, which only the knee gives, and from 75 % + the knees nearly touch. +- **Ankle caps**, the 593 and 620 px the `limb cap:` lines size, and the 402 px hole at the + left ankle: unseen while the feet stay level, seen as soon as a foot turns against its + shin. + +**Not measured:** the hello over the idle on two tracks, as step 10 plays it. + ### The joint script, `scripts/keypoints.py` - **Downloads, first run**: two model zips, 94,223,081 and 212,739,469 bytes (sizes from the @@ -358,7 +512,9 @@ a different, smaller pose model; it was not tried and would need its own downloa On the example painting both legs are one See-through layer. From the script's joints the proposer gave one leg chain down the middle of both legs (40 bones); from the hand-placed -set it gave a chain per leg on that same layer (42 bones). Which one you get depends on +set it gave a chain per leg on that same layer (42 bones). On parts 1.49.1 both sets give a +chain per leg, and with step 7's ankle correction both give 44 bones, the feet cut from the +shoes. Which one you get depends on where the knees and ankles fall on the leg art, and a note on the leg part says which and why. Both build green. The README's map leaves the legs out for this reason — the wave does not move them, and a map written for one bone list misses on the other. To give each leg its diff --git a/scripts/check-adapters.ts b/scripts/check-adapters.ts index 7749187..78dbb91 100644 --- a/scripts/check-adapters.ts +++ b/scripts/check-adapters.ts @@ -156,6 +156,15 @@ try { writeFileSync(join(consumer, 'keypoints.json'), keypoints); const beforeDraft = readFileSync(join(consumer, 'config.json'), 'utf8'); for (const n of [7, 8]) run(['bash', '-eu', '-o', 'pipefail', '-c', blocks(step(n), 'sh')[0]], consumer); + // Step 7's ankle correction lets the producer cut the shoes into one part per foot. + type Built = { animations: { name: string; duration?: number }[]; bones: { name: string }[]; slots: { name: string }[]; constraints: { kind: string; name: string }[] }; + const feetBuild = JSON.parse(readFileSync(join(consumer, 'out/check/build/skeleton.model.json'), 'utf8')) as Built; + const slotNames = feetBuild.slots.map(slot => slot.name); + for (const name of ['foot_r', 'foot_l']) assert(slotNames.includes(name), `GUIDE_FEET: step 8's build has no ${name} slot (step 7's l_ankle correction)`); + assert(!slotNames.includes('shoes'), 'GUIDE_FEET: step 8\'s build still draws the shoes as one part'); + assert.equal(feetBuild.bones.length, 72, `GUIDE_FEET: expected 72 bones in step 8's build, found ${feetBuild.bones.length}`); + reports.feet = { slots: ['foot_r', 'foot_l'], bones: feetBuild.bones.length }; + console.log('[bar:guide-feet] step 8 build: foot_r and foot_l slots, no shoes slot, 72 bones PASS'); // Revert the real field-copy repair in a private README copy. Its command, // over the same proposal and installed packages, must reproduce the refusal. const take = blocks(step(7), 'sh')[0].split('\n').find(line => line.includes('/scripts/take.ts ')); @@ -193,13 +202,17 @@ try { reports.modelSha256 = createHash('sha256').update(readFileSync(join(consumer, 'wave-build/skeleton.model.json'))).digest('hex'); // Step 9b: the hello, named in the config and gated by the build. run(['bash', '-eu', '-o', 'pipefail', '-c', blocks(step('9b'), 'sh')[0]], consumer); - const built = JSON.parse(readFileSync(join(consumer, 'out/check/build/skeleton.model.json'), 'utf8')) as { animations: { name: string }[] }; + const built = JSON.parse(readFileSync(join(consumer, 'out/check/build/skeleton.model.json'), 'utf8')) as Built; assert.deepEqual(built.animations.map(a => a.name), ['idle', 'hello'], 'GUIDE_HELLO: the build must hold idle and hello'); - console.log('[bar:guide-hello] step 9b build exit 0; out/check/build holds idle, hello PASS'); + const helloLength = (b: Built) => b.animations.find(a => a.name === 'hello')?.duration; + assert.equal(helloLength(built), 4.75, `GUIDE_HELLO: expected a 4.75 s hello, found ${helloLength(built)}`); + console.log('[bar:guide-hello] step 9b build exit 0; out/check/build holds idle, hello (4.75 s) PASS'); // Step 9c: the cloth recipe puts the side locks, the fringe and the skirt on physics. run(['bash', '-eu', '-o', 'pipefail', '-c', blocks(step('9c'), 'sh')[0]], consumer); - const physical = JSON.parse(readFileSync(join(consumer, 'out/check/build/skeleton.model.json'), 'utf8')) as { animations: { name: string }[]; constraints: { kind: string; name: string }[] }; + const physical = JSON.parse(readFileSync(join(consumer, 'out/check/build/skeleton.model.json'), 'utf8')) as Built; assert.deepEqual(physical.animations.map(a => a.name), ['idle', 'hello'], 'GUIDE_CLOTH: the build must still hold idle and hello'); + assert.equal(helloLength(physical), 4.75, `GUIDE_CLOTH: expected a 4.75 s hello, found ${helloLength(physical)}`); + for (const name of ['foot_r', 'foot_l']) assert(physical.slots.some(slot => slot.name === name), `GUIDE_CLOTH: the cloth build lost the ${name} slot`); const ofKind = (kind: string) => physical.constraints.filter(c => c.kind === kind).map(c => c.name); assert.equal(ofKind('physics').length, 27, `GUIDE_CLOTH: expected 27 physics constraints, found ${ofKind('physics').length}`); assert.equal(ofKind('transform').length, 3, `GUIDE_CLOTH: expected 3 transform constraints, found ${ofKind('transform').length}`); diff --git a/scripts/hello.ts b/scripts/hello.ts index 84d3329..1dc9fb7 100644 --- a/scripts/hello.ts +++ b/scripts/hello.ts @@ -1,34 +1,48 @@ -// Make a four-second "hello" as one rig-c motion spec, for the config's +// Make a hello of under five seconds as one rig-c motion spec, for the config's // motion.animations_from. // -// bun hello.ts [--seconds 4] [--fps 24] [--out hello.anim.json] [--no-skirt-keys] +// bun hello.ts [--seconds 4.75] [--fps 24] [--out hello.anim.json] [--no-skirt-keys] // // is the rig a build wrote, out/rig/rig.json: the bones the hello -// keys are read from it, and the legs' lengths and rest angles for the weight -// shift. The output is {"spec": "rigc-motion/1", "animations": {"hello": …}}, -// the shape rig-parts reads beside the idle; `rig-parts build` then gates it -// with the rig and reads it for folds. +// keys are read from it, and the legs' lengths and rest angles for the settle. +// The output is {"spec": "rigc-motion/1", "animations": {"hello": …}}, the +// shape rig-parts reads beside the idle; `rig-parts build` then gates it with +// the rig and reads it for folds. // // Four beats, every track back at rest by the end, so it loops: // hm? the head straightens toward the viewer, a small lean, brows up; // heh the eyes narrow and the mouth widens, two shoulder bobs; // wave the forearm comes up beside the shoulder and waves twice; -// settle she shifts her weight onto her right foot and back, to rest. +// settle she re-plants her weight: as the arm comes down, her splayed right +// leg (the picture's left) slides its foot 40 % of the way in toward +// parallel with the other, her hip follows it, and both go back. // Nothing here comes from a video or a dataset: it is an authored test motion, // keyed on the bones, sampled once per frame. // -// Two limits come from the example painting, measured with `rigc render --seam`: +// The limits come from the example painting, measured with `rigc render --seam`: // - The upper arm stays within 4 degrees of rest. Its image is the puff sleeve // and the arm in one piece, under a sleeveless top, so swinging it out shows // background at the armhole (50 px at 6 degrees). The forearm tucks under // the cuff and is free at the elbow, so the wave is the forearm's. -// - The weight shift is 6 px (0.5 % of the figure). The thigh art starts at the -// skirt's hem, so a larger hip move opens a gap there; the skirt's first -// links follow the hip, and both ankles stay where they were painted (the -// shoes are one image that no leg bone moves). -// --no-skirt-keys leaves the skirt out: once step 9c's cloth recipe hangs it on -// physics, the hip shift moves it and no key does. Without physics the keys -// are needed: with them left out, the hem opens 48 px over the left thigh. +// - The right foot comes 40 % of the way to parallel, 33.2 px. Each foot is a +// part of its own on a bone that keeps the shoe level (step 7's keypoint +// correction gives the rig foot_r and foot_l), so the ankle slides along the +// floor and a two-bone solve bends the knee the way it is painted. The thigh +// art starts at the skirt's hem, so the skirt's link over the right thigh +// turns with it. At 50 % the skirt's top pulls off the shirt under physics +// (topwear|bottomwear ghost 54 px, over the guide's 50); past about 75 % +// the hem opens and the knees meet. +// - The hip moves 6 px (0.5 % of the figure); a larger move opens the hem +// over the left thigh. The left foot stays where it was painted. +// - The feet are never keyed: the right shoe is painted turned out, and +// turning its image reads as a shoe tipped on its side. +// --no-skirt-keys is for step 9c's cloth recipe, which hangs the skirt on +// physics: the hip's move then swings it and no key does, except on skirt0_0, +// the first link of the tree over the right thigh, which carries no physics of +// its own and takes the thigh's turn. That bone is the recipe's, so it is not +// in the rig this reads; the build that follows checks it. Without physics the +// keys are needed: skirt_r0 takes the thigh's turn and skirt_c0, skirt_l0 +// follow the hip, or the hem opens over the thighs. import { readFileSync, writeFileSync } from 'node:fs'; type Bone = { name: string; parent?: string; x?: number; y?: number; rotation?: number; length?: number }; @@ -48,35 +62,42 @@ const opt = (name: string, dflt: number): number => { args.splice(i, 2); return v; }; -const seconds = opt('--seconds', 4); +const SHEET = 4.75; // seconds: the beat sheet's own length +const seconds = opt('--seconds', SHEET); const fps = opt('--fps', 24); const oi = args.indexOf('--out'); const outPath = oi >= 0 ? args.splice(oi, 2)[1] : 'hello.anim.json'; const [rigPath] = args; if (!rigPath || !outPath) { - console.error('usage: bun hello.ts [--seconds 4] [--fps 24] [--out hello.anim.json] [--no-skirt-keys]'); + console.error('usage: bun hello.ts [--seconds 4.75] [--fps 24] [--out hello.anim.json] [--no-skirt-keys]'); process.exit(2); } const rig = JSON.parse(readFileSync(rigPath, 'utf8')); const bones: Record = {}; for (const b of rig.bones ?? []) bones[b.name] = b; -const skirt = skirtKeys ? ['skirt_r0', 'skirt_c0', 'skirt_l0'] : []; -const need = ['hip', 'chest', 'head', 'mouth', 'sleeve_r0', 'sleeve_r1', 'leg_r0', 'leg_r1', 'leg_l0', 'leg_l1', ...skirt]; +const need = ['hip', 'chest', 'head', 'mouth', 'sleeve_r0', 'sleeve_r1', 'leg_r0', 'leg_r1', 'leg_l0', 'leg_l1', 'foot_r', 'foot_l', + ...(skirtKeys ? ['skirt_r0', 'skirt_c0', 'skirt_l0'] : [])]; const lacking = need.filter((n) => !bones[n]); if (lacking.length > 0) { - console.error(`hello.ts: ${rigPath} has no bone ${lacking.join(', ')}; the hello keys all of ${need.join(', ')}`); + const feet = lacking.includes('foot_r') || lacking.includes('foot_l') + ? ' (no foot_r/foot_l: the shoes are one part, which no leg bone moves; see step 7)' : ''; + console.error(`hello.ts: ${rigPath} has no bone ${lacking.join(', ')}; the hello keys all of ${need.join(', ')}${feet}`); process.exit(1); } for (const leg of ['leg_r', 'leg_l']) { if (bones[`${leg}0`].parent !== 'hip' || bones[`${leg}1`].parent !== `${leg}0`) { - console.error(`hello.ts: ${leg}0 must hang from hip and ${leg}1 from ${leg}0 for the weight shift`); + console.error(`hello.ts: ${leg}0 must hang from hip and ${leg}1 from ${leg}0 for the settle`); process.exit(1); } } +if (bones.hip.rotation) { + console.error('hello.ts: the hip is turned at rest; the settle slides the foot along the hip\'s x axis, which is then not the floor'); + process.exit(1); +} -// The beat sheet is written for 4 s; another --seconds stretches it evenly. -const k = seconds / 4; +// The beat sheet is written for 4.75 s; another --seconds stretches it evenly. +const k = seconds / SHEET; const smooth = (a: number, b: number, t: number) => { const x = Math.min(1, Math.max(0, (t - a) / (b - a))); return x * x * (3 - 2 * x); @@ -98,16 +119,17 @@ const twoLifts = (t: number, a: number, b: number) => const UPPER_ARM = -4; // degrees: sleeve_r0, out from her side; the limit above const FOREARM = -125; // degrees: sleeve_r1, the forearm up beside the shoulder const WAVE = 15; // degrees each way, twice a second -const SHIFT = -6; // px: hip x, toward the image's left, onto her right foot; the limit above -const SKIRT = 1.66; // degrees: skirt_*0 following the hip +const STANCE = 0.4; // of the way from the painted stance to parallel: the right ankle's slide; the limit above +const SHIFT = -6; // px: hip x, toward the picture's left, over the feet's new middle; the limit above +const SKIRT = 1.66; // degrees: skirt_c0, skirt_l0 following the hip (no physics) -// Settle: out over 3.30–3.65 s, back over 3.65–4.00 s. -const settle = (t: number) => smooth(3.3, 3.65, t) - smooth(3.65, 4.0, t); +// Settle: out over 3.15–3.75 s, held to 4.15 s, back over 4.15–4.75 s. +const settle = (t: number) => smooth(3.15, 3.75, t) - smooth(4.15, 4.75, t); -// Two-bone leg IK in the hip's frame (the hip has no rotation): the hip moves -// by dx, the ankle stays put, the knee keeps the side it was painted on. +// Legs, in the hip's frame (the hip has no rotation, so its x axis is the floor). const rad = Math.PI / 180; -const legIK = (leg: string, dx: number): [number, number] => { +type Leg = { R: [number, number]; A: [number, number]; L0: number; L1: number; r0: number; r1: number }; +const legOf = (leg: string): Leg => { const b0 = bones[`${leg}0`]; const b1 = bones[`${leg}1`]; const L0 = b0.length ?? 0; @@ -117,8 +139,21 @@ const legIK = (leg: string, dx: number): [number, number] => { const R: [number, number] = [b0.x ?? 0, b0.y ?? 0]; const K: [number, number] = [R[0] + L0 * Math.cos(r0), R[1] + L0 * Math.sin(r0)]; const A: [number, number] = [K[0] + L1 * Math.cos(r0 + r1), K[1] + L1 * Math.sin(r0 + r1)]; + return { R, A, L0, L1, r0, r1 }; +}; +const legs = { leg_r: legOf('leg_r'), leg_l: legOf('leg_l') }; +// Hip-to-ankle angle from straight down, degrees; + leans the ankle toward the picture's left. +const tilt = (l: Leg, ax = l.A[0]) => Math.atan2(l.R[0] - ax, l.R[1] - l.A[1]) / rad; +// The right ankle's x where its hip-to-ankle line is parallel to the left leg's, on the same floor. +const parallelX = legs.leg_r.R[0] - (legs.leg_r.R[1] - legs.leg_r.A[1]) * Math.tan(tilt(legs.leg_l) * rad); +const inward = STANCE * (parallelX - legs.leg_r.A[0]); + +// Two-bone IK: the hip moves by dx, the ankle by ax along the floor; the knee +// keeps the side it was painted on. Returns the keys for 0 and 1. +const legIK = (leg: 'leg_r' | 'leg_l', dx: number, ax: number): [number, number] => { + const { R, A, L0, L1, r0, r1 } = legs[leg]; // The ankle, seen from the moved hip. - const Ax = A[0] - dx; + const Ax = A[0] + ax - dx; const Ay = A[1]; const d = Math.hypot(Ax - R[0], Ay - R[1]); if (d > L0 + L1) { @@ -134,6 +169,8 @@ const legIK = (leg: string, dx: number): [number, number] => { const wrap = (a: number) => Math.atan2(Math.sin(a), Math.cos(a)) / rad; return [wrap(n0 - r0), wrap(n1 - r1)]; }; +const right = (t: number) => legIK('leg_r', SHIFT * settle(t), inward * settle(t)); +const left = (t: number) => legIK('leg_l', SHIFT * settle(t), 0); type Curve = (t: number) => number | number[]; const curves: [string, string, Curve, boolean?][] = [ @@ -151,11 +188,17 @@ const curves: [string, string, Curve, boolean?][] = [ ramp(t, [[0, 0], [1.9, 0], [2.2, FOREARM], [3.1, FOREARM], [3.4, 0]]) + (t >= 2.2 && t <= 3.1 ? WAVE * Math.sin(2 * Math.PI * 2 * (t - 2.2)) * smooth(2.2, 2.3, t) * (1 - smooth(3.0, 3.1, t)) : 0)], ['hip', 'translate', (t) => [SHIFT * settle(t), 0]], - ['leg_r0', 'rotate', (t) => legIK('leg_r', SHIFT * settle(t))[0]], - ['leg_r1', 'rotate', (t) => legIK('leg_r', SHIFT * settle(t))[1]], - ['leg_l0', 'rotate', (t) => legIK('leg_l', SHIFT * settle(t))[0]], - ['leg_l1', 'rotate', (t) => legIK('leg_l', SHIFT * settle(t))[1]], - ...skirt.map((b): [string, string, Curve] => [b, 'rotate', (t) => SKIRT * settle(t)]), + ['leg_r0', 'rotate', (t) => right(t)[0]], + ['leg_r1', 'rotate', (t) => right(t)[1]], + ['leg_l0', 'rotate', (t) => left(t)[0]], + ['leg_l1', 'rotate', (t) => left(t)[1]], + ...(skirtKeys + ? [ + ['skirt_r0', 'rotate', (t: number) => right(t)[0]], + ['skirt_c0', 'rotate', (t: number) => SKIRT * settle(t)], + ['skirt_l0', 'rotate', (t: number) => SKIRT * settle(t)], + ] as [string, string, Curve][] + : [['skirt0_0', 'rotate', (t: number) => right(t)[0]]] as [string, string, Curve][]), ]; const n = Math.round(seconds * fps); @@ -181,17 +224,19 @@ const doc = { hello: { duration: seconds, loop: true, - note: `scripts/hello.ts over ${rigPath}: hm? (head straightens, brows up), heh (eyes narrowed, mouth wide, two shoulder bobs), an elbow-down wave, a weight-shift settle; ${seconds} s at ${fps} fps`, + note: `scripts/hello.ts over ${rigPath}: hm? (head straightens, brows up), heh (eyes narrowed, mouth wide, two shoulder bobs), an elbow-down wave, a settle (the right foot 40 % of the way to parallel, the hip after it); ${seconds} s at ${fps} fps`, tracks, }, }, }; writeFileSync(outPath, JSON.stringify(doc) + '\n'); -const [r0, r1] = legIK('leg_r', SHIFT); -const [l0, l1] = legIK('leg_l', SHIFT); +const [r0, r1] = legIK('leg_r', SHIFT, inward); +const [l0, l1] = legIK('leg_l', SHIFT, 0); const f2 = (x: number) => (x >= 0 ? '+' : '') + x.toFixed(2); console.log( `hello.ts: 4 beats (hm?, heh, wave, settle), ${seconds} s at ${fps} fps, ${tracks.length} tracks of ${n + 1} keys; ` + - `sleeve_r0 at most ${maxUpper.toFixed(1)} deg; hip shift ${maxShift.toFixed(1)} px${skirtKeys ? '' : ', skirt left to physics'} ` + - `(legs r ${f2(r0)}/${f2(r1)}, l ${f2(l0)}/${f2(l1)} deg, ankles held) -> ${outPath}`, + `sleeve_r0 at most ${maxUpper.toFixed(1)} deg; settle: right ankle ${inward.toFixed(1)} px in ` + + `(${Math.round(STANCE * 100)} % of the way to parallel, ${tilt(legs.leg_r).toFixed(2)} -> ${tilt(legs.leg_r, legs.leg_r.A[0] + inward).toFixed(2)} deg), ` + + `hip ${maxShift.toFixed(1)} px (legs r ${f2(r0)}/${f2(r1)}, l ${f2(l0)}/${f2(l1)} deg, feet on the floor); ` + + `${skirtKeys ? 'skirt_r0 follows the thigh' : 'skirt0_0 follows the thigh, the rest left to physics'} -> ${outPath}`, ); From ca40ed9e5089777838374c6c427b38f29a772580 Mon Sep 17 00:00:00 2001 From: firejune Date: Wed, 21 Oct 2026 10:18:23 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20the=20hand-placed=20keypoints=20rep?= =?UTF-8?q?roduce=20the=20hello=20=E2=80=94=20tuned=20to=20the=20script's?= =?UTF-8?q?=20joints=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The no-Python route's set put the right leg's painted stance at 16.70 deg (script 15.96), so hello.ts brought the ankle 41.1 px in and the hello failed the guide's own seam bars (topwear|bottomwear ghost 79, hem 32/30, plus a thigh_r|shin_r seam warning). The hips, knees and right ankle were 14-50 px off; the set now sits within a pixel of keypoints.py's output and the hello reads 33.6 px, 16.05 -> 12.92 deg, inside every bar. Co-Authored-By: Roa <342598975+fj-roa[bot]@users.noreply.github.com> --- README.md | 5 +++-- docs/KEYPOINTS.md | 38 +++++++++++++++++++++++--------------- docs/REFERENCE.md | 6 ++++-- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index cc4401e..d88eee9 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,8 @@ should see `18 observed, 0 occluded, 0 missing -> keypoints.json`. No Python? [docs/KEYPOINTS.md](docs/KEYPOINTS.md) has a hand-placed set for this very painting; save it as `keypoints.json`. Its left ankle is already where step 7's first line -puts it, and you get the same 44 bones. +puts it, and you get the same 44 bones. It was tuned to the script's joints, so the steps +print what they quote, except that step 9b's hello reads `33.6 px in` and `16.05 -> 12.92 deg`. ### 7. Draft the bones @@ -591,7 +592,7 @@ guesses the torso from the clothes and draws no legs. **On the CPU**: [`scripts/keypoints.py`](scripts/keypoints.py) finds the 18 joints with a pose model (RTMPose), as in [step 6](#6-find-the-joints): about 300 MB of models the first time, then a few seconds per painting. On the example painting its joints were within 4–52 -pixels of a hand-placed set, and the rig built green on them unchanged. +pixels of a first hand-placed set, and the rig built green on them unchanged. **By hand**: read the 18 positions off the painting in an image editor and write them into a small JSON file. The format, a diagram of where each joint goes, and a complete example diff --git a/docs/KEYPOINTS.md b/docs/KEYPOINTS.md index da29da0..10fae4c 100644 --- a/docs/KEYPOINTS.md +++ b/docs/KEYPOINTS.md @@ -42,22 +42,22 @@ This is a hand-placed set for the example painting (1664 x 2432). Save it as "source": "placed by hand", "people": [ { "id": "a", "joints": { - "nose": { "state": "observed", "at": [894, 344] }, - "neck": { "state": "observed", "at": [880, 490] }, - "r_shoulder": { "state": "occluded", "at": [690, 560] }, - "r_elbow": { "state": "observed", "at": [664, 847] }, - "r_wrist": { "state": "observed", "at": [573, 1120] }, - "l_shoulder": { "state": "occluded", "at": [1060, 550] }, - "l_elbow": { "state": "observed", "at": [1100, 831] }, - "l_wrist": { "state": "observed", "at": [1223, 1097] }, - "r_hip": { "state": "occluded", "at": [780, 1030] }, - "r_knee": { "state": "observed", "at": [650, 1650] }, - "r_ankle": { "state": "observed", "at": [455, 2110] }, - "l_hip": { "state": "occluded", "at": [970, 1030] }, - "l_knee": { "state": "observed", "at": [940, 1655] }, + "nose": { "state": "observed", "at": [891, 332] }, + "neck": { "state": "observed", "at": [877, 541] }, + "r_shoulder": { "state": "occluded", "at": [720, 532] }, + "r_elbow": { "state": "observed", "at": [672, 835] }, + "r_wrist": { "state": "observed", "at": [560, 1136] }, + "l_shoulder": { "state": "occluded", "at": [1034, 550] }, + "l_elbow": { "state": "observed", "at": [1098, 835] }, + "l_wrist": { "state": "observed", "at": [1228, 1107] }, + "r_hip": { "state": "occluded", "at": [779, 994] }, + "r_knee": { "state": "observed", "at": [637, 1657] }, + "r_ankle": { "state": "observed", "at": [447, 2148] }, + "l_hip": { "state": "occluded", "at": [1010, 1000] }, + "l_knee": { "state": "observed", "at": [944, 1669] }, "l_ankle": { "state": "observed", "at": [848, 2140] }, - "r_eye": { "state": "observed", "at": [825, 275] }, - "l_eye": { "state": "observed", "at": [944, 294] }, + "r_eye": { "state": "observed", "at": [832, 272] }, + "l_eye": { "state": "observed", "at": [950, 284] }, "r_ear": { "state": "missing" }, "l_ear": { "state": "missing" } } } ] @@ -71,6 +71,14 @@ its leg's art and on the shoes; `l_ankle` sits at `[848, 2140]` for that, and th step 7 line puts it there whichever set you use. Earlier versions of the parts package gave other counts; [REFERENCE.md](REFERENCE.md#the-leg-chain-moves-with-the-joints) keeps them. +The set was tuned to land where the script's joints do: each joint here is within a pixel +of what `scripts/keypoints.py` writes for this painting, and the ears, which it places, +are left `missing`. The hips, the knees and the right ankle are the ones that matter. An +earlier set had them 14–50 px off, and the right leg's stance came out 0.7 degrees wider: +the README's step 9b hello then slid the right foot 41.1 px instead of 33.2 and opened a +seam at the right knee. With this set step 9b prints `right ankle 33.6 px in (40 % of the +way to parallel, 16.05 -> 12.92 deg)`, and every other line the README quotes as written. + ## What the script writes `scripts/keypoints.py` runs RTMPose (whole-body, "balanced") through diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 5bc2128..780b021 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -496,8 +496,10 @@ trees answer the hip, and less than before (1.60 against 2.90), because the hip both times with every package and model already cached — so that 40 s is environment set-up and first imports, not downloads. - **Agreement with hand-placed joints**: on the example painting, within 4–52 painting - pixels of the set in [KEYPOINTS.md](KEYPOINTS.md) (neck 52, l_hip 49, l_ankle 44, - r_shoulder 40; elbows, wrists, knees 4–21). + pixels of the set first placed in [KEYPOINTS.md](KEYPOINTS.md) (neck 52, l_hip 49, l_ankle 44, + r_shoulder 40; elbows, wrists, knees 4–21). The set there now was moved to within a pixel + of the script's joints, so that the no-Python route gives the same hello + ([KEYPOINTS.md](KEYPOINTS.md#the-file)). - **The estimator's joints built green unedited**: `propose --keypoints` (2 LINT lines, both the leg chain's top under the skirt) → `take.ts` → `build` PASS 10/10. - **The hand-placed set** through the same steps: 42 bones, a chain per leg on the one