rig-parts 1.50.0 → 1.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.ts +22 -7
- package/docs/AUTHORING.md +56 -13
- package/docs/STABILITY.md +4 -4
- package/package.json +1 -1
- package/src/assemble.ts +68 -6
- package/src/build.ts +7 -3
- package/src/check.ts +130 -10
- package/src/clothconfig.ts +67 -13
- package/src/parts.ts +22 -3
package/cli.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { checkImageSize, choosePerson, KEYPOINTS_SPEC, loadKeypoints, type RigJo
|
|
|
30
30
|
import { basisLines, branchSqueezeLine, checkProposal, compare, compareLines, DEFAULT_GRID_RULE, draftTurn, drawLandmarks, FIGURE_GRID_DIVISOR, FRINGE_STEP_PX, GRID_RULES, type GridRule, HIP_MIN_FRACTION, type DraftLintFinding, lint, lintLine, type PartSet, proposeWithBasis, readPartSet, serializeProposal, withTurn, WRIST_ON_PART_PX } from './src/propose.ts';
|
|
31
31
|
import { BASIS_FILE, BASIS_SPEC, basisFile, basisLine, coverageLines, serializeBasis } from './src/diagnostics.ts';
|
|
32
32
|
import { BEND_DEGREES, BEND_WORST_SHARE, DUST_MAX_PX, DUST_MAX_SHARE, IDLE_BOUND_PX, MESH_DRAFT_REPORT, MESH_DRAFT_SPEC, meshDraftStage, MORE_TRIANGLES_FLAG, relativeConfigPaths, TOLERANCE_FIGURE_DIVISOR } from './src/meshdraft.ts';
|
|
33
|
-
import { clothConfigEdits, editPaths, writtenClothConfig } from './src/clothconfig.ts';
|
|
33
|
+
import { clothConfigEdits, editPaths, previousCount, writtenClothConfig } from './src/clothconfig.ts';
|
|
34
34
|
import { PROVENANCE_FILE, REMESH_SPEC, remeshStage } from './src/remesh.ts';
|
|
35
35
|
import { RETUNE_FILE, RETUNE_SPEC, retuneStage } from './src/retune.ts';
|
|
36
36
|
import { checkPainting } from './src/paintcheck.ts';
|
|
@@ -226,7 +226,7 @@ usage:
|
|
|
226
226
|
a LINT line names each drafted radius and both figures. Without --cloth
|
|
227
227
|
nothing changes.
|
|
228
228
|
|
|
229
|
-
rig-parts propose … --cloth <config.json> [--write-config <out.json>]
|
|
229
|
+
rig-parts propose … --cloth <config.json> [--write-config <out.json> [--from <previous.json>]]
|
|
230
230
|
Also write the --cloth config's own text with what the recipe drafts
|
|
231
231
|
spliced in (issue #537), the way mesh-draft writes its config, so that
|
|
232
232
|
rig --config <out.json> builds it. What is spliced is measured: the
|
|
@@ -246,6 +246,14 @@ usage:
|
|
|
246
246
|
(PROPOSE_WRITE_CONFIG_CONFLICT), or when the spliced text does not
|
|
247
247
|
parse to the config with the sections set, naming the first key
|
|
248
248
|
that moved (PROPOSE_WRITE_CONFIG_SPLICE).
|
|
249
|
+
--from <previous.json> (issue #571, only with --write-config) names
|
|
250
|
+
the config the recipe is changed from: its cloth block is read as
|
|
251
|
+
--cloth's is, the same parts are drafted a third time with it, and an
|
|
252
|
+
entry equal to that previous recipe's draft is the recipe's, replaced
|
|
253
|
+
(or removed) as one equal to the draft without the recipe is; an entry
|
|
254
|
+
equal to none of the three drafts is still refused by name, its field
|
|
255
|
+
read against the previous recipe's draft. The write line counts the
|
|
256
|
+
entries the previous draft vouched for. Without --from nothing changes.
|
|
249
257
|
|
|
250
258
|
rig-parts propose --parts <dir> --source <painting.png> --out <dir> --from-config <config.json>
|
|
251
259
|
Draw the config's CURRENT bones instead (<out>/render/landmarks_config.png
|
|
@@ -915,7 +923,7 @@ const TAKES = {
|
|
|
915
923
|
layers: { valued: ['--export', '--template', '--canvas'], switches: [] },
|
|
916
924
|
sheet: { valued: ['--source', '--layers', '--out', '--cell', '--cols'], switches: [] },
|
|
917
925
|
rig: { valued: ['--config', '--parts', '--out', '--idle-keys', '--page-edges', '--pack-shape'], switches: [] },
|
|
918
|
-
propose: { valued: ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--head', '--canvas', '--keypoints', '--person', '--wrist-snap', '--grid-rule', '--cloth', '--write-config'], switches: ['--head-box', '--no-back-hair'] },
|
|
926
|
+
propose: { valued: ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--head', '--canvas', '--keypoints', '--person', '--wrist-snap', '--grid-rule', '--cloth', '--write-config', '--from'], switches: ['--head-box', '--no-back-hair'] },
|
|
919
927
|
compare: { valued: ['--left', '--right', '--map'], switches: [] },
|
|
920
928
|
check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements', '--set', '--environment'], switches: ['--seam-pairs', '--keep-stills', '--all-sets'] },
|
|
921
929
|
loop: { valued: ['--frames', '--out'], switches: ['--palette'] },
|
|
@@ -1316,6 +1324,9 @@ function cmdPropose(args: string[]): number {
|
|
|
1316
1324
|
const writeArg = flags.get('--write-config');
|
|
1317
1325
|
if (writeArg !== undefined && flags.has('--from-config')) return usage('--write-config writes what the recipe drafts into its config; --from-config proposes nothing');
|
|
1318
1326
|
if (writeArg !== undefined && clothArg === undefined) return usage('--write-config writes the --cloth config with what its recipe drafts spliced in; give --cloth <config.json>');
|
|
1327
|
+
// Issue #571: the config the recipe is changed from; an entry equal to its recipe's draft is the recipe's, not a hand edit.
|
|
1328
|
+
const fromArg = flags.get('--from');
|
|
1329
|
+
if (fromArg !== undefined && writeArg === undefined) return usage('--from names the config the recipe is changed from, for --write-config; give --write-config <out.json>');
|
|
1319
1330
|
if (writeArg !== undefined) {
|
|
1320
1331
|
const ours = ['proposal.json', BASIS_FILE].map((f) => resolve(out, f));
|
|
1321
1332
|
if (existsSync(writeArg) || ours.includes(resolve(writeArg))) {
|
|
@@ -1323,6 +1334,7 @@ function cmdPropose(args: string[]): number {
|
|
|
1323
1334
|
}
|
|
1324
1335
|
}
|
|
1325
1336
|
const cloth = clothArg === undefined ? null : loadClothConfig(clothArg).cloth;
|
|
1337
|
+
const previousCloth = fromArg === undefined ? null : loadClothConfig(fromArg).cloth;
|
|
1326
1338
|
const partsDir = existsSync(partsArg) && statSync(partsArg).isFile() ? dirname(partsArg) : partsArg;
|
|
1327
1339
|
if (!existsSync(source)) {
|
|
1328
1340
|
throw new PartsError([{ code: 'PROPOSE_SOURCE_PRESENT', object: source, detail: 'no such file; the painting is drawn under the overlay' }]);
|
|
@@ -1366,8 +1378,10 @@ function cmdPropose(args: string[]): number {
|
|
|
1366
1378
|
: (() => {
|
|
1367
1379
|
const text = readFileSync(clothArg, 'utf8');
|
|
1368
1380
|
// Issue #552: bones, constraints and motion.tracks entry by entry; a hand edit the recipe would drop is refused by name.
|
|
1369
|
-
|
|
1370
|
-
|
|
1381
|
+
// Issue #571: with --from, the same parts drafted a third time with the previous recipe.
|
|
1382
|
+
const previous = previousCloth === null ? null : proposeWithBasis(P, joints, wristSnap, gridRule, previousCloth).proposal;
|
|
1383
|
+
const edits = clothConfigEdits(drafted, proposeWithBasis(P, joints, wristSnap, gridRule, null).proposal, JSON.parse(text) as unknown, false, previous);
|
|
1384
|
+
return { at: writeArg, text: writtenClothConfig(clothArg, text, edits), paths: editPaths(edits), vouched: previousCount(edits), relative: relativeConfigPaths(dirname(resolve(clothArg)), JSON.parse(text) as Record<string, unknown>) };
|
|
1371
1385
|
})();
|
|
1372
1386
|
const bfile = basisFile(P, prop, basis, cfg === null || cmpPath === undefined ? null : { where: cmpPath, config: cfg });
|
|
1373
1387
|
const img = drawLandmarks(P, painting, prop.bones, 'PROPOSAL - CORRECT ME');
|
|
@@ -1385,10 +1399,11 @@ function cmdPropose(args: string[]): number {
|
|
|
1385
1399
|
printLint(P, prop, joints, draftLint);
|
|
1386
1400
|
console.log(basisLine(bfile, out));
|
|
1387
1401
|
if (written !== null && clothArg !== undefined) {
|
|
1402
|
+
const from = fromArg === undefined ? '' : `; --from ${fromArg}: ${written.vouched} ${written.vouched === 1 ? 'entry' : 'entries'} taken as the previous recipe's draft`;
|
|
1388
1403
|
console.log(
|
|
1389
1404
|
written.paths.length > 0
|
|
1390
|
-
? `wrote ${written.at}: ${clothArg}'s text with ${written.paths.join(', ')} spliced in from the draft; every other byte as read`
|
|
1391
|
-
: `wrote ${written.at}: ${clothArg}'s text byte for byte; every entry the recipe drafts is already as drafted`,
|
|
1405
|
+
? `wrote ${written.at}: ${clothArg}'s text with ${written.paths.join(', ')} spliced in from the draft; every other byte as read${from}`
|
|
1406
|
+
: `wrote ${written.at}: ${clothArg}'s text byte for byte; every entry the recipe drafts is already as drafted${from}`,
|
|
1392
1407
|
);
|
|
1393
1408
|
if (written.relative.length > 0 && dirname(resolve(written.at)) !== dirname(resolve(clothArg))) {
|
|
1394
1409
|
console.log(`note: ${written.relative.map((r) => `${r.at} "${r.value}"`).join(', ')} written as stated; the loader resolves each against the directory the config sits in, ${dirname(written.at)} for ${written.at}, not ${dirname(clothArg)}; put the config beside ${clothArg} or rewrite them`);
|
package/docs/AUTHORING.md
CHANGED
|
@@ -128,7 +128,7 @@ read by no CPU stage).
|
|
|
128
128
|
| `stops` | rig (handed to rig-c as `rig.json`'s `stops`, and moves slots, mesh weights and child bones on stopped links onto `<link>_d`); `propose --from-config` (`LINT stop on <chain> …`) | **authored**, optional (issues #404, #447): `[{chain, links?, against, at, inward, outward, lower?: {inward, outward}}]` — rig-c's leg stops (`RigStop`, rig-c 2.56.0, `rig-c/src/stop.ts`). `chain` is a chain `bones` declares (`CONFIG_NAME_RESOLVES`, `RIG_NAME_RESOLVES`); `links`, optional, is a non-empty list of links of that chain (`CONFIG_NAME_RESOLVES`, `CONFIG_FIELD_TYPE`), or absent for every link of `chain` in chain order; `against` is a bone `bones` declares, a chain link as `<chain><i>` (`CONFIG_NAME_RESOLVES`, `RIG_NAME_RESOLVES`); `at` is `[x, y]` in `against`'s local space, as rig-c's stop takes it (`CONFIG_FIELD_TYPE`): since each chain link (`leg_r0`, `leg_r1`) is turned to point along its segment (`+x` from origin to next link or tip, `+y` 90° counter-clockwise in Spine world), `at: [d, n]` sits `d` px from `against`'s origin toward its end and `n` px perpendicular to it; `inward`, `outward` and `lower.inward`, `lower.outward` are finite numbers (`CONFIG_FIELD_TYPE`). Every other stop rule — non-negative bounds, each stopped link written by a constraint, `against` not under the stopped chain, the aim not degenerate, `\|rest\| + outward <= 180°` (`RIG_STOP_BOUNDS`) — is rigc's to accept or refuse at the rig stage's gate, in rigc's words (`RIG_RIGC_GREEN`). **Rising-limb / prone geometry (issue #447):** rig-c's `expandStops` aims `<chain>_rest` from `links[0]`'s setup origin at `<chain>_touch` (`at` in `against`'s local frame) and clamps `<link>_d` inside `[rest − inward, rest + outward]`; Spine wraps rotation at `±180°`, so `\|rest\| + outward <= 180°` is required (`RIG_STOP_BOUNDS`). Whenever `<chain>_touch` lies **above** `links[0]`'s root in rig pixels (`ty < y0`, i.e. higher Spine `y` — e.g. stopping a hanging cloth chain against a rising shin `leg_<s1>` in a prone or kneeling pose where the knee sits below `links[0]` and the ankle above it, so positive `at[0]` walks upward along the shin past `links[0]`'s root), `rest` lands in the upper half-plane (`\|rest\| > 90°`) and `\|rest\| + outward` exceeds `180°`. Stop against the descending thigh (`leg_<s0>`), set `links` to start at a link whose root sits above `<chain>_touch`, or omit the stop when the rising shin sits outside the hanging chain's sweep; `propose --from-config` prints `LINT stop on <chain> …` with the exact coordinates and angles when `<chain>_touch` lies above `links[0]`'s root or `\|rest\| + outward > 180°`. For every stopped link `<link>`, the rig stage moves any slot on `<link>`, any mesh weight on `<link>`, and any bone whose parent is `<link>` (a branched cloth tree's branches hanging from `<trunk_last>`, before `--idle-keys ctl` adds `<bone>_ctl` and before `turn` adds `turn_<part>_<bone>`) onto `<link>_d`, the drawn link bone rig-c's `expandStops` creates at `<link>`'s setup frame to carry the clamped rotation. Absent, `rig.json` carries no `stops` and no byte of any output moves |
|
|
129
129
|
| `turn` | rig (adds the turn control) | **proposed** only with `propose --full`/`--head` (issue #251), then corrected: `{pivot, depths, deform?, band?, radius?, surface?: {radius?}, head?: {pivot?, radius?}}`. `depths` holds, per part, the See-through layer's `depth_median` the part came from (larger = further back), read from the run the part was assembled from; a `painting:` patch has none and is not carried. A rider (issue #271) — a part from an `eyewhite`, `irides`, `eyelash`, `eyebrow`, `nose` or `mouth` layer — is drafted at the `face` part's depth, 0.001 nearer when parts.json draws it after the face and 0.001 further when before: See-through makes no depth of its own for the eye group (it cuts them from one depth map of the combined eyes) and clamps nose and mouth to the face's median − 0.001, and the two runs' own medians for these layers disagreed by up to 0.47 and swapped their order against the face. Every other layer — ears, headwear and earwear included — keeps its own median, unless it rides another part's bone. With no `face` part, or more than one, the riders keep their own and a note says so. A rider by bone (issue #535) — a part every *ownable* bone of which (a chain link, or a bone no other bone hangs from; `head`, `chest` and any bone with a child are shared and never count on their own) is *hosted* by another carried part, the host of a bone being the carried part that binds it (its region's bone; a mesh's `segments`, `contour` and `auto` regions' bones) with the most `opaque_px`, the earlier in parts.json on a tie, and which hosts no bone itself — is drafted at its host's depth, 0.001 nearer when drawn after the host and 0.001 further when before, and a note names the host, the bones and its own median: a pin, clip or ornament on the back hair's links travels with the back hair, not with its own layer's depth. `propose` reads the proposal's own `bones`, `meshes` and `regions`; the face riders are read by their tag first and never by their bones. A part bound to two hosts' bones rides neither and a note says so. In the rig stage, an authored depth on a rider by bone whose fraction differs from its host's by more than 0.1 of the dial (`TURN_RIDER_APART`, a share of `pivot − nearest`) is refused (`CONFIG_TURN_RIDER_DEPTH`); the host is read off `parts.json`'s `opaque_px`, so the config loader cannot see it. One note lists each part's depth and where it came from; a carrier the other run places more than 10 % of `pivot − nearest` away gets a note naming both values and the run read — the two runs are not reconciled. `pivot` is the depth that stays put; `propose` drafts the mean of the parts' depths weighted by `opaque_px`, the one depth at which the painted pixels' centroid does not slide under the dial. The rig stage adds a bone `turn` under `root` at its origin that nothing keys — its local x is the dial, in rig px — and carries each part by `(pivot − depth) / (pivot − nearest depth)` of it: the nearest part all of it, a part behind the pivot the other way, a part at the pivot not at all. Each part rides leaf twins (`turn_<part>_<bone>`, an identity child of every bone its slot and weights bind) moved by one `transform` constraint (`turn_<part>`, after the config's own); the idle's keys, the `<bone>_ctl` controls and a physics constraint stay on the bones, and the dial moves none of them. Optional `deform` (`Partial<FaceDeformFractions>`, issue #436) overrides the Set B ×2.5 face surface deformation table (`FACE_DEFORM_FRACTIONS`, checked against `FACE_DEFORM_RANGES` by `CONFIG_TURN_DEFORM_RANGE`); when `applyFaceSurfaceDeform` runs on a rig whose `motion` carries `tilt` or `turn` slider `derive` keys, it requires `config.turn` (`CONFIG_TURN_REQUIRED`, issue #478; `propose --full`/`--head` drafts it), promotes `face` on `head` (or `turn_face_head` when `turn` is declared) to the uneven 5×5 grid mesh, keys `face`, `hair_front` and `hair_back` deform timelines and ear/topknot tracks from `evalPitchFaceSurface` and `evalYawFaceSurface`, and samples the same deform field across each `features` member's rest span (`featRx = (24 / 106) · W`, `featRy = (12 / 128) · H`) with its depth fraction `z / zRef` as the field's angle input and chord foreshortening (`scalex` narrowing on the leading side and widening on the trailing side, `scaley` on pitch) so every feature's bounding box stays inside the deformed face plate across the rated band (issue #468). On yaw (`issue #479`), `evalYawFaceSurface` evaluates a **cylinder projection** (`cylinderYawDx`: `Δx(x, θ, R) = R · (sin(clamp(arcsin(clamp(x / R, −1, 1)) − θ, −π/2, π/2)) − clamp(x / R, −1, 1))`) of radius `R` (`turnSurfaceRadius`: `turn.surface.radius ?? turn.radius ?? turn.head.radius`, positive rig px checked by `CONFIG_FIELD_TYPE`, defaulting to half the face plate's opaque width `W / 2`) tapered by a cubic Hermite outline weight (`wOutline = 1` for `\|ux\| <= 0.7`, reaching `0` at `\|ux\| = 1.0`) so features slide across a fixed outline (`iris` shift within `±2%` of `cylinderYawDx`), the trailing cheek strip widens (`>= 1.54×` at `±15°`), and the leading jaw edge holds fixed (`0 px` shift) unless explicit horizontal `turn.deform` overrides (`turn_mid_x`, `turn_lead_compress_x`, `turn_trail_shift_x`) request the flat yaw table. On yaw (`issue #478`), `applyFaceSurfaceDeform` also derives a **head-only pivot** (`headTurnPivot`: `turn.head.pivot` when authored, otherwise the `opaque_px`-weighted mean of `depths[part]` across the head parts `DEFAULT_HEAD_TURN_PARTS` — `face`, `hair_front`, `hair_back`, `ear_r`, `ear_l`, `hairpin` — from `parts.json`, falling back to `turn.pivot`) so a forward torso or arm in `turn.depths` does not pull the head's rotation axis in front of `face`: `hair_back` (`depth_median > headPivot`, negative `headTurnFraction`) and `hairpin` behind `headPivot` translate opposite to the face's turn (`dx = s · turn_face_center_x · W · (headPivot − depth) / (headPivot − nearestHeadDepth)`), emerging on the trailing side to compensate for the leading-cheek foreshortening and keeping the combined head silhouette width near-constant across `±15°` without any same-sign `turn_hair_back_cheek_x` term. Optional `band` (`number` or `{yaw?, pitch?}`, positive degrees, issue #472) declares the rig's rated face turn band (default `FACE_RATED_BAND`: `yaw <= 15°`, `pitch <= 12°`); declaring a `turn.band` or a `motion.animations_from` `turn`/`tilt` `derive.degrees` above `±15°` yaw or `±12°` pitch without an `assemble.alternates` entry that replaces `face` is refused (`CONFIG_FACE_BAND_EXCEEDED`), and on a single-plate rig `applyFaceSurfaceDeform` adds self-targeting transform constraints `yaw_clamp` (`[-15, 15]`) and `pitch_clamp` (`[-12, 12]`) ahead of the slider constraints so driving `yaw` or `pitch` beyond the rated band clamps at the boundary. At rest: with `turn` declared, no pixel differs by more than 1 level at dial 0 (not byte-identical; the cause is float noise in the identity child bones, and the constraints add nothing); without `turn`, byte-identical. Neither `pivot` nor `depths` has a default (`CONFIG_FIELD_PRESENT`); `pivot` and `head.pivot` must lie behind the nearest part (`CONFIG_TURN_PIVOT_BEHIND_NEAREST`). Absent, no control is added and no byte of any output moves |
|
|
130
130
|
| `scene` | assemble (the recomposite, its holes and the figure's mask); check, through `parts.json`'s `scene` block (`RECOMPOSITE_HOLES`, the seam) | **authored**, optional; `propose` writes none (issue #242). `{}` declares the painting a scene: the figure and its surroundings in one image, See-through's full run holding only the figure. With it, `assemble` reads the recomposite figures, the hole list and the `uncovered:` lines inside the figure — the rig pixels where any full-run layer's alpha is above 8 (`OPAQUE_ALPHA_ABOVE`, the cut every layer's painted pixels are read by) — each line starting `inside the figure (config.scene: …, <n> of <m> rig px)`; writes that figure as `figure_rig.png` (white on black) beside the recomposite, and a `scene` block in `parts.json`; flags error pixels on the error map inside it only (outside, the painting grey); and `check` leaves out of the seam every frame pixel whose composite sample reaches the painting's outermost row or column, saying how many (`seam_border` in `check.json`, and on the seam line). It takes no field: the figure and its threshold are derived. A key in it other than a record or an annotation is `CONFIG_KEY_KNOWN`. Absent, no byte of any output moves |
|
|
131
|
-
| `cloth` | `propose --cloth` only; no other stage reads it, so it moves no byte of any build | **authored**, optional (issue #364): one recipe per tag, `{"bottomwear": {…}, "back hair": {…}, "front hair": {…}}`, each `{chains, links, wind, gravity, split?, length_ratio?, strength?, damping?, alternate_damping?, translate?, swing?, part?, influence?}`. `influence` (issue #518) — `{across, max, shape, radius?}`: the parts this recipe meshes are drafted in the influence mode (`meshes.<part>.influence` above) at their lattice's cell, with `across`, `max` and `shape` as declared (each required — the reading behind the radius did not vary them) and `radius` as declared or, absent, drafted as the farthest vertex's distance plus 3 cells (the `influence.radius` row); without it the parts keep their lattice. `chains` — trees across the piece (for `back hair`, per side of the eye axis), a whole number, 1 or more; `links` — links from root to tip on every path, 1 or more; `split` — `{at, into}`: the first `at` links are the trunk, which forks into `into` (2 or more) branches; `at` below `links` (`CONFIG_CLOTH_SPLIT_BEFORE_TIP`); `length_ratio` — a path's last link over its first, above 0, default 0.5; `strength` — `{trunk, branch, edge}`, each above 0 and below the spring's stability bound (`CONFIG_CLOTH_SPRING_STABLE`), default 100 / 100 / 50; `damping` — in [0, 1], absent: rig-c's default; `alternate_damping` — the odd trees' damping, in [0, 1], beside `damping` and different from it (`CONFIG_CLOTH_ALTERNATE_DIFFERS`); `wind`, `gravity` — `[root, tip]`, each 0 or more, tip at or above root (`CONFIG_CLOTH_RESPONSE_GROWS_TO_TIP`); `translate` — the x and y mix of a dangling end's flutter, above 0 and at most 1; `swing` — `bottomwear` only, `{scales}`, one positive scale per tree (`CONFIG_CLOTH_SWING_SCALE_PER_TREE`); `part` — `bottomwear` only (`CONFIG_CLOTH_PART_ON_BOTTOMWEAR`), a non-empty part or cut-piece name: the `topwear` part that holds a skirt See-through returned inside it (a dress), or its skirt piece cut by `assemble.cuts`, which takes the skirt's trees (issue #402, *A skirt inside topwear* below; the part is checked by `propose`, `PROPOSE_CLOTH_PART`). The two defaults are the recipe's own, measured on its synthetic fixture (the recipe below). The recipe below says what each field drafts. The config written: `propose --cloth <config.json> --write-config <out.json>` (issue #537) writes this config's own text back with the sections the recipe drafts spliced in — `
|
|
131
|
+
| `cloth` | `propose --cloth` only; no other stage reads it, so it moves no byte of any build | **authored**, optional (issue #364): one recipe per tag, `{"bottomwear": {…}, "back hair": {…}, "front hair": {…}}`, each `{chains, links, wind, gravity, split?, length_ratio?, strength?, damping?, alternate_damping?, translate?, swing?, part?, influence?}`. `influence` (issue #518) — `{across, max, shape, radius?}`: the parts this recipe meshes are drafted in the influence mode (`meshes.<part>.influence` above) at their lattice's cell, with `across`, `max` and `shape` as declared (each required — the reading behind the radius did not vary them) and `radius` as declared or, absent, drafted as the farthest vertex's distance plus 3 cells (the `influence.radius` row); without it the parts keep their lattice. `chains` — trees across the piece (for `back hair`, per side of the eye axis), a whole number, 1 or more; `links` — links from root to tip on every path, 1 or more; `split` — `{at, into}`: the first `at` links are the trunk, which forks into `into` (2 or more) branches; `at` below `links` (`CONFIG_CLOTH_SPLIT_BEFORE_TIP`); `length_ratio` — a path's last link over its first, above 0, default 0.5; `strength` — `{trunk, branch, edge}`, each above 0 and below the spring's stability bound (`CONFIG_CLOTH_SPRING_STABLE`), default 100 / 100 / 50; `damping` — in [0, 1], absent: rig-c's default; `alternate_damping` — the odd trees' damping, in [0, 1], beside `damping` and different from it (`CONFIG_CLOTH_ALTERNATE_DIFFERS`); `wind`, `gravity` — `[root, tip]`, each 0 or more, tip at or above root (`CONFIG_CLOTH_RESPONSE_GROWS_TO_TIP`); `translate` — the x and y mix of a dangling end's flutter, above 0 and at most 1; `swing` — `bottomwear` only, `{scales}`, one positive scale per tree (`CONFIG_CLOTH_SWING_SCALE_PER_TREE`); `part` — `bottomwear` only (`CONFIG_CLOTH_PART_ON_BOTTOMWEAR`), a non-empty part or cut-piece name: the `topwear` part that holds a skirt See-through returned inside it (a dress), or its skirt piece cut by `assemble.cuts`, which takes the skirt's trees (issue #402, *A skirt inside topwear* below; the part is checked by `propose`, `PROPOSE_CLOTH_PART`). The two defaults are the recipe's own, measured on its synthetic fixture (the recipe below). The recipe below says what each field drafts. The config written: `propose --cloth <config.json> --write-config <out.json>` (issue #537) writes this config's own text back with the sections the recipe drafts spliced in — `meshes.<part>` and `regions.<part>`, and `bones`, `constraints` and `motion.tracks` entry by entry by name (issue #552) — and every other byte as read; `--from <previous config>` (issue #571) takes an entry equal to the previous recipe's draft as the recipe's (*The config written*, under the recipe below) |
|
|
132
132
|
| `proposed_rig_scale` | assemble, rig, build, `propose --from-config`/`--compare` | **proposed** by `propose` (issue #354): `parts.json`'s `scale_rig_per_source`, the `assemble.rig_scale` the rig sections (`bones`, `meshes`, `limbs`, `motion`) were drafted at. Held equal to `assemble.rig_scale` when present (`CONFIG_PROPOSED_RIG_SCALE`; 0, negative or not a number: `CONFIG_FIELD_TYPE`). Absent on a hand-authored config, where no byte of any output moves |
|
|
133
133
|
| `requirements` | build (read in place of `--requirements` when the flag is not given), and so its check | **authored**, optional, never proposed (issue #556): a path, relative to the config file's directory, to the `spine-parts-requirements/1` file the build's check reads (§7, *Declared requirements*) — so a rig that needs one (a `not_swinging` part, a `may_fold`) rebuilds from its config alone. `build` reads it before assemble, as it reads the flag's file, and `check.json`'s `requirements.file` records it with `path` as declared. Given beside `--requirements`, the two must name the same file once resolved, or a file of the same bytes, and the declaration is read and recorded (a build given both writes what the config alone writes); otherwise `BUILD_REQUIREMENTS_CONFLICT`, naming both. Not a non-empty string: `CONFIG_FIELD_TYPE`; no such file, where the config is read from a path: `CONFIG_FILE_PRESENT`. `check`, `compose`, `mesh-draft`, `remesh` and `retune` do not read it: their `--requirements` is the only door. Absent, nothing is read and no byte of any output moves |
|
|
134
134
|
|
|
@@ -523,13 +523,30 @@ layers, as follows; every step is arithmetic on the declared bones and the layer
|
|
|
523
523
|
both segments' parts) within half a pixel of the line, the run through the joint (its nearest
|
|
524
524
|
pixel within 1 px of it, neighbours at most 1.5 px apart), first to last plus one pixel, to 3
|
|
525
525
|
places. The tool invents no pixel of it: each is the `cap` image's where that image is not
|
|
526
|
-
transparent, else the layer's where the limb has art there, else **
|
|
527
|
-
|
|
528
|
-
|
|
526
|
+
transparent, else the layer's where the limb has art there, else the **upper segment's** art
|
|
527
|
+
there (issue #557 — the shin's pixels where the boot's layer does not reach over the knee, the
|
|
528
|
+
shin's again for a shoe that starts below the ankle line; until #557 only `cap_shape: "disc"`
|
|
529
|
+
read it), else **missing**. The line `assemble` prints for each joint counts them and gives the
|
|
530
|
+
cap's box, `[x, y, w, h]` in rig px (`N px from "<upper>"` only where the upper segment gave a
|
|
531
|
+
pixel, so a full cap prints what it printed):
|
|
529
532
|
|
|
530
533
|
limb cap: "arm_r" joint "elbow" at [332, 424], on "forearm_r": a disc of radius 19.9165 px (the limb 39.833 px across the joint line), box [312, 404, 40, 40] — on the upper side of the line 621 px from the layer, 7 px missing — …
|
|
531
534
|
|
|
532
|
-
The missing pixels are where
|
|
535
|
+
The missing pixels are where no layer of the limb has a pixel: where the painting hides the limb,
|
|
536
|
+
or where the disc passes its outline — the half-disc is half the limb's width about the bone's
|
|
537
|
+
origin, so a bone off the limb's middle puts the far side of the disc past the outline (every
|
|
538
|
+
missing pixel on the public `wind` example is that: 9 to 72 per joint, each on one side, none
|
|
539
|
+
within the half-width about the joint). **A cap with missing pixels is short** (issue #557): its
|
|
540
|
+
record carries `short_px: {left, right}` — the missing pixels by side of the limb's axis at the
|
|
541
|
+
joint line, `left` where a pixel centre's offset across the axis is 0 or above (the side toward
|
|
542
|
+
smaller x when the limb hangs down the rig), `right` the other, the two adding up to
|
|
543
|
+
`missing_px` — and `assemble` prints one `LINT` line after its `limb cap:` line:
|
|
544
|
+
|
|
545
|
+
LINT "shin_r" cap at [34, 30] (limb "leg_r", joint "knee", radius 4 px): short_px left 0, right 3: no layer gives them — 3 px of the disc on the upper side of the cut, where neither the layer of "boots" nor that of "legs" has a pixel of the limb; the rest of the disc is filled, 17 px from "shin_r"'s layer, 6 px from "thigh_r"; under cap_shape "disc" the disc is the half-width about the joint, 4 px, and 3 of them lie within it
|
|
546
|
+
|
|
547
|
+
The last clause, on a cap without `cap_shape`, counts how many of them the disc of the
|
|
548
|
+
half-width about the joint would still hold: none means `cap_shape: "disc"` closes the
|
|
549
|
+
shortfall (it does on every `wind` joint). A full cap writes no `short_px` and prints no `LINT`.
|
|
533
550
|
Paint them — and, if the layer's own pixels carry the limb's outline across the cap, the whole
|
|
534
551
|
cap — into a `w`x`h` image whose pixel `(i, j)` is rig pixel `(x + i, y + j)`, and name it in
|
|
535
552
|
the segment's `cap`; its transparent pixels keep the layer's. The record is `parts.json`'s
|
|
@@ -542,8 +559,9 @@ layers, as follows; every step is arithmetic on the declared bones and the layer
|
|
|
542
559
|
the limb's art across the line either way. It is measured, never typed; a joint on the very edge of
|
|
543
560
|
the run has none and is `ASSEMBLE_LIMB_JOINT_ON_ART`. The cap's pixels come from the image, then
|
|
544
561
|
the layer, then **the upper segment's art** where the lower's layer has none (the shin painted down
|
|
545
|
-
to the ankle is the shoe's cap)
|
|
546
|
-
cap:` line adds `N px from "<upper>"
|
|
562
|
+
to the ankle is the shoe's cap) — the plain cap's order too since issue #557 — and only where no
|
|
563
|
+
segment has art are they missing. The `limb cap:` line adds `N px from "<upper>"` always, and the
|
|
564
|
+
record `shape` and `upper_px` (without the shape, `upper_px` only when above 0). The upper piece's
|
|
547
565
|
**corner** at the cut is kept where it is: on a joint off the limb's middle, a corner trimmed to the
|
|
548
566
|
disc leaves a notch at rest when no piece takes it, or a stub out of the bent lower piece's outline
|
|
549
567
|
when the lower takes it (pictured in the pull request that added these two fields).
|
|
@@ -588,7 +606,9 @@ layers, as follows; every step is arithmetic on the declared bones and the layer
|
|
|
588
606
|
bar at each, against the same pose with the joint's own art drawn under the limb — so a cap
|
|
589
607
|
that does not cover the turn is a FAIL naming the joint and the angle, where the rest pose
|
|
590
608
|
showed nothing. A joint whose cap is `cap_shape: "disc"` is held to more: no pixel of its disc
|
|
591
|
-
over 40 at any pose, the disc being the cap's whole claim.
|
|
609
|
+
over 40 at any pose, the disc being the cap's whole claim. Each pose's row also prints the
|
|
610
|
+
**contour step** (issue #557), the step the eye sees in the outline where the cut meets it — a
|
|
611
|
+
reading, never a bar (§7). `TIP_OVER_ROOT` does not read a limb part (it turns at its joint; it does not
|
|
592
612
|
hang), and its row says so.
|
|
593
613
|
|
|
594
614
|
The first character built this way (the exam-01 test rig, issue #349) read, per joint, 5 to 10
|
|
@@ -1667,7 +1687,29 @@ recipe re-drafts or removes that the author changed by hand (it equals neither d
|
|
|
1667
1687
|
not dropped: the run is refused, `PROPOSE_WRITE_CONFIG_CONFLICT`, naming the entry
|
|
1668
1688
|
(`config.constraints[40] "skirt0a_0_physics"` in CL207), the first field the hand edit holds and
|
|
1669
1689
|
what the draft writes there, every conflict at once and nothing written; take the draft
|
|
1670
|
-
by removing the hand edit, or leave the recipe out for that part. A
|
|
1690
|
+
by removing the hand edit, or leave the recipe out for that part. **A changed recipe
|
|
1691
|
+
(issue #571).** The draft of the recipe as it stood before a change is neither of the
|
|
1692
|
+
two drafts, so a config holding that draft untouched would read as hand-edited at every
|
|
1693
|
+
entry the change moves (29 refusals on the public `wind` example with
|
|
1694
|
+
`cloth.bottomwear.links` 3 → 4). `--from <previous config>`, given beside
|
|
1695
|
+
`--write-config`, names the config the recipe is changed from: its `cloth` block is read
|
|
1696
|
+
as `--cloth`'s is (a missing file or a missing `cloth` block is refused by the same
|
|
1697
|
+
codes), and the same parts are drafted a third time with it. An entry equal to that
|
|
1698
|
+
previous recipe's draft is then the recipe's, as one equal to the draft without the
|
|
1699
|
+
recipe is: replaced by the draft, or removed where the draft no longer holds the name
|
|
1700
|
+
(a link the change took away). An entry equal to none of the three drafts is still
|
|
1701
|
+
refused, `PROPOSE_WRITE_CONFIG_CONFLICT`, the detail reading "it equals neither the draft,
|
|
1702
|
+
the draft without the recipe, nor the previous recipe's draft" and naming the first field
|
|
1703
|
+
that differs from the previous recipe's draft (the entry a person most likely edited
|
|
1704
|
+
from). The write line ends `--from <path>: n entries taken as the previous recipe's
|
|
1705
|
+
draft`. `meshes.<part>` and `regions.<part>` are unchanged by `--from`: a mesh the
|
|
1706
|
+
recipe drafts is replaced whole already, and one the two drafts agree at is not touched.
|
|
1707
|
+
Without `--from` every rule and every byte is the writer's before #571; with a `--from`
|
|
1708
|
+
whose recipe equals `--cloth`'s the previous draft is the draft, and the written config
|
|
1709
|
+
is byte for byte the one written without it (CL209–CL212 hold each on the cloth fixture,
|
|
1710
|
+
CL213–CL215 on `wind`). The written config is the recipe's draft, not a verdict on it: on
|
|
1711
|
+
`wind`, `links` 3 → 4 writes the skirt entries of its proposal value for value, and that
|
|
1712
|
+
draft fails `CHECK_TIP_OVER_ROOT` on the skirt in `build`; 3 → 2 builds green (CL215). A `meshes.<part>` the
|
|
1671
1713
|
recipe drafts is still the draft's whole, as a hand-edited lattice does not survive
|
|
1672
1714
|
`mesh-draft`, and a mesh it does not draft keeps its bytes. Every other byte is the
|
|
1673
1715
|
input's; a section the input lacks (a config holding only `key` and `cloth`) is
|
|
@@ -1681,7 +1723,8 @@ wrote back byte for byte (measured once, when the flag landed). The text is writ
|
|
|
1681
1723
|
(`motion.animations_from`, `generation.control.skeleton`) resolves against the directory
|
|
1682
1724
|
the written config sits in, so the out path belongs beside the input, and when it is not
|
|
1683
1725
|
a note names each such path. Refused before anything is written: `--write-config`
|
|
1684
|
-
without `--cloth` or beside `--from-config
|
|
1726
|
+
without `--cloth` or beside `--from-config`, and `--from` without `--write-config`
|
|
1727
|
+
(`USAGE`), an out path that exists or that
|
|
1685
1728
|
the run writes itself (`PROPOSE_WRITE_CONFIG_FRESH`), a hand edit to an entry the
|
|
1686
1729
|
recipe owns (`PROPOSE_WRITE_CONFIG_CONFLICT`), and a text whose splice does not
|
|
1687
1730
|
parse to the config with the sections set — a key stated twice
|
|
@@ -2280,7 +2323,7 @@ move off it (`AS245`).
|
|
|
2280
2323
|
| --- | --- | --- | --- |
|
|
2281
2324
|
| `layers` | the table, then one `author-made: layer "author:<label>" — …` line per layer the author made (group `author` in the table; §5, *A layer the author made*), then the `WARN` lines | every tag the plan will need has opaque pixels; one `face` in the head run; `0 WARN line(s)` | a run whose layer PNG is not its box's size (`LAYERS_PNG_MATCHES_BBOX`); a `WARN PLAN_LAYER_…` line (§6, *Plausibility*) |
|
|
2282
2325
|
| `sheet` of both runs | the tile list (and the sheet, if you can see) | eyes, irises, lashes and brows as left/right pairs in the head run | a head box that cut off an ornament: move `head_box`, re-run the head crop |
|
|
2283
|
-
| `assemble` | one line per part — its counts (`op=` opaque, `vis=` visible, `src=` taken / candidates, `unproj=` visible but not projected, `drift=<n> (<p>% of core)`, `merged=`, `seam=`; *parts.json*, below). `drift=` is `refused_drift_px` in rig px and its share of the part's candidates, `projected_core_px`, which it is a subset of (issue #314); no bar reads either. On the four public examples the share runs to 94.5 % (`scarf` `hair_back`, 479 of 507) and the count to 26,472 (`scarf` `topwear`, 22.3 %), and the seam override repaints refused top-most pixels afterwards, so a large count says where See-through's colour was not taken, not that the part is unlike the painting: the `drift:` line below is the one that names a body of them another part holds. Then the `pixels:` totals (opaque = visible + occluded; taken; visible but not projected), then `recomposite vs source: mean \|d\|, within 8, error px > 40, uncovered error px`, then `uncovered holes (8-connected): N` and the largest five as `uncovered hole K: <px> px at x,y wxh (between "<part>" <px> px, …)`, then one `uncovered: <n> of the <N> uncovered error px lie on the full run's "<tag>", in a body of <px> px at x,y wxh; <the rule that left it out>` line per full-run layer the plan does not take from the full run whose uncovered error holds a body (5x5-opened, 8-connected, at least 150 full-run px in rig px), then one `drift: "<part>" refuses <n> px of <run>:<tag> (its colour more than 90 off the painting, max channel), in a body of <px> px at x,y wxh (…); by the plan's draw order "<holder>" holds <px> px of it (…), "<holder>" drawn in front of "<part>"` line per part whose drift refusals hold a body (the same opening and bar) that another part holds in the draw order — that layer is not what the painting shows there (none on the five public paintings); and look at `render/recomposite_error_rig.png`. Between the part lines and the totals, one `author-made: part "<part>" (<run>:author:<label>) — …` line per part from a layer the author made (below, *A layer the author made*), then one `cut: "<part>" opaque <n> = "<part>" <kept> + "<piece>" <taken> …` line per part an `assemble.cuts` entry cut (below; it ends `; under <n> px filled, <m> px left empty` when a cut of that part has `under`), then — with `config.limbs` (issue #349) — one `limb cut: "<part>" opaque <n> = "<segment part>" <px> + …` line per part a limb takes and one `limb cap:` line per joint, its cap's pixels from the cap image, from the layer and missing, and its box
|
|
2326
|
+
| `assemble` | one line per part — its counts (`op=` opaque, `vis=` visible, `src=` taken / candidates, `unproj=` visible but not projected, `drift=<n> (<p>% of core)`, `merged=`, `seam=`; *parts.json*, below). `drift=` is `refused_drift_px` in rig px and its share of the part's candidates, `projected_core_px`, which it is a subset of (issue #314); no bar reads either. On the four public examples the share runs to 94.5 % (`scarf` `hair_back`, 479 of 507) and the count to 26,472 (`scarf` `topwear`, 22.3 %), and the seam override repaints refused top-most pixels afterwards, so a large count says where See-through's colour was not taken, not that the part is unlike the painting: the `drift:` line below is the one that names a body of them another part holds. Then the `pixels:` totals (opaque = visible + occluded; taken; visible but not projected), then `recomposite vs source: mean \|d\|, within 8, error px > 40, uncovered error px`, then `uncovered holes (8-connected): N` and the largest five as `uncovered hole K: <px> px at x,y wxh (between "<part>" <px> px, …)`, then one `uncovered: <n> of the <N> uncovered error px lie on the full run's "<tag>", in a body of <px> px at x,y wxh; <the rule that left it out>` line per full-run layer the plan does not take from the full run whose uncovered error holds a body (5x5-opened, 8-connected, at least 150 full-run px in rig px), then one `drift: "<part>" refuses <n> px of <run>:<tag> (its colour more than 90 off the painting, max channel), in a body of <px> px at x,y wxh (…); by the plan's draw order "<holder>" holds <px> px of it (…), "<holder>" drawn in front of "<part>"` line per part whose drift refusals hold a body (the same opening and bar) that another part holds in the draw order — that layer is not what the painting shows there (none on the five public paintings); and look at `render/recomposite_error_rig.png`. Between the part lines and the totals, one `author-made: part "<part>" (<run>:author:<label>) — …` line per part from a layer the author made (below, *A layer the author made*), then one `cut: "<part>" opaque <n> = "<part>" <kept> + "<piece>" <taken> …` line per part an `assemble.cuts` entry cut (below; it ends `; under <n> px filled, <m> px left empty` when a cut of that part has `under`), then — with `config.limbs` (issue #349) — one `limb cut: "<part>" opaque <n> = "<segment part>" <px> + …` line per part a limb takes and one `limb cap:` line per joint, its cap's pixels from the cap image, from the layer and missing, and its box, followed on a short cap by one `LINT "<part>" cap at [x, y] … short_px left <n>, right <m>: no layer gives them …` line (issue #557) — from the upper segment where it gave a pixel (under `cap_shape: "disc"` always), under `edge: "feather"` the pixels faded, under `cut_band` the line the cut was placed on and the stroke pixels it and the joint line cross (issue #360) (§3, *Limbs: cut at their joints*), then one `fringe pushed back: "<part>" <px> px, where the painting shows "<holder>" <px> px, …` line per part whose fringe was cleared (below) | on the examples: `reference-sample` 0.83 / 98.0 % / 4,359 / 1,185, 259 holes, the largest 94 px; `reference-demo` 2.34 / 95.8 % / 10,340 / 1,564, 453 holes, the largest 70 px (default rule) — many slivers along part edges, no hole a region could hide | one large hole: part of the figure is in no layer — a plan entry is missing, hair left the head crop sideways (reference-demo's `hair_back` is taken from the full run for that reason), or See-through split one garment into two and left the space between them in neither (a skirt as two trouser legs); the `between` parts say where. When neither run holds it at all, an `assemble.patches` entry cuts it from the painting (below) |
|
|
2284
2327
|
| `propose` | `note:` lines, `LINT` lines, the `coverage` lines, `landmarks.png`, `basis.json` | no LINT line: every chain link lies on its mesh's art, and the hip is below the chest and the figure's top quarter; the `coverage:` summary says how many bones that verdict covers — a `not checked` bone was read by no check (its line says why), and a `ratio` or `fallback` bone in `basis.json` is a guess | a link off the art (a bone on the background) — move it onto the layer; `LINT <link> at … runs to … N degrees off the head's down axis …` — a fringe link that does not lean with the head (§3, the fringe in the head's frame): move it along the lock it drives; `LINT hairback_… on mesh '<part>' (side <r\|l>): <N> tree(s) for <M> visible hair mass(es) (mass 0 columns x=<minX>..<maxX>, y=<minY>..<maxY>; …)` — back-hair cloth trees whose count does not match the side's visible hair masses (`max(1, floor(w / (4 * grid)) + 1)` per mass) or whose link or tip leaves its mass's column range (issues #445, #461): omit `chains` on `cloth."back hair"` or draft trees per visible hair mass inside its columns; `LINT <a_limb> and <b_limb> on <part> (<from>) cross at [x, y] with no per-limb split: …` — two limbs sharing one layer cross in an X without a per-limb split (issue #446): declare `config.limbs.<covering>.split` (`{ "line": [[x, y], …] }` or `{ "edge": "painting" }`, §3 *Which limb*); `LINT foot_<s> at [x, y], the <s>_ankle joint and leg_<s>1's tip, lies on none of leg_<s>'s art within 1 px along its joint line (the nearest of it on that line is the pixel [x, y], D px from the joint): …` — an ankle keypoint off its leg's art on the line `assemble` sizes the ankle's cap by (issue #517): `propose` drafts every leg without its ankle cut, the shoe layer stays one part, and nothing is moved onto the art; move that ankle keypoint onto the leg's or the shoe's art and re-run `propose` (`(the line crosses none of it)` when no art lies on that line at all); `LINT stop on <chain> (<firstLink> at [x0, y0]) against <against> (at [ax, ay]) places <chain>_touch at [tx, ty], above <firstLink>'s root (rest R deg, \|rest\| + outward B deg): …` — a stop's contact point `<chain>_touch` lies above `links[0]`'s root in rig px or pushes `\|rest\| + outward` past `180°` (issue #447, §2 `stops`): stop against the descending limb bone below `firstLink`, set `links` to start at a link below `<chain>_touch`, or omit the stop on a rising limb; `LINT hip at [x, y] is not below chest at [x, y]: …` or `LINT hip at [x, y] is above 0.25 of the figure height (figure y T..B, so hip y must be at least L): a hip at the shoulders` — move `hip` down to the waist (and `chest` between it and the neck); a `hanging strand … -- no chain proposed` note — that strand hangs stiff until you add a chain down the x and rows it names (§3); a `no blink: …` or `blink without brows: …` note — the idle will not blink (or its brows will not drop), because no part came from an `eyewhite` (or `eyebrow`) layer; the note names the tags looked for; a `no eyewhite part for eye_<s> …` note — the iris and lash parts it names ride `head` and do not blink, because no `eyewhite-<s>` part made that side's eye bone: take the eyewhite from the other run, or keep them on `head`; a `no face part: face box derived from …` note (always the first) — `head`, `neck` and every face-height scale come from a box guessed off the hair and neck (§3): check `head` and `neck` on `landmarks_head.png` and move them; when it adds `the blink shuts the eyes over no face part`, read `BLINK_NO_HOLE` after `check` — an eye with no face under it can open a hole (the `reference-sample` with only its face removed: 587 px). Under `--keypoints`: the `lint rule set:` line first (joints, or screen and why), then the joint notes — `missing: the rule "…" placed bone X instead` is a bone still guessed; `not used — …` names why an arm's joints did not place its sleeve (for a wrist on no part, the nearest part and the `--wrist-snap` that would bind it); a `wrist snap: …` note is a wrist bound past 15 px by the flag — check that chain's tip on `landmarks.png`; a `one handwear part holds both wrists` note is two chains on one mesh — cut the part with `assemble.cuts` to separate them; `LINT chest at … is not between the neck joint …`, `LINT hip at … is not past chest …`, `LINT hip at … is nearer the neck joint …` and `LINT <chain><i> at … does not advance from <s>_shoulder toward <s>_wrist` are the joint-frame lines: move that bone between, past or along the joints they name |
|
|
2285
2328
|
| `compare` | one line per pair (origin, parent, tip, length, direction — or SKIP and why), then the per-row summary, the unmapped bones of each side, `required:`, each side's roles and the `controls:` line | proposal against the config you corrected: the rows you changed and no others. Config against the `rig.json` `build` writes for it: measured on both examples, every origin within 6.19e-7 px (the rig's offsets are written to 6 places) and every chain link's tip and length within 4.99e-4 px (`length` is written to 3), printed to three places as `0.000` (a signed figure as `+0.000` or `-0.000`); `parent … is the right's ancestor at depth 2, <bone>_ctl between` for every keyed mesh bone, no `DIFFERENT` and no `NOT MAPPED` | `parent DIFFERENT: left a, right b` — a bone hangs from another parent than the other side's; `frames not related` — a rig with no stage (`skeleton.width`/`height`), two configs at different `rig_scale`: the distance rows SKIP, so declare the frame in a `--map` file if the author knows it; `FAIL STRUCTURE_REQUIRED_PRESENT` — a bone the map requires is missing through its pairs |
|
|
2286
2329
|
| `rig` (inside `build`) | one line per mesh: vertices, triangles, bones, influences, `cover`; the `bones` line; the `idle keys` line; then rigc's gate lines (with A15's declared SKIP under `--idle-keys direct`); in `rig.json`, each chain link's (or its `_ctl`'s) `length` and `rotation` | `cover 1.00000` on every mesh, both gate summaries `0 failed` (the compile and the packed pages); every link's `length` the distance to the next link and `rotation` its direction (Spine degrees, counter-clockwise, y up, local to the parent), every offset under it — a child bone's `x, y`, a weight's bind `x, y`, a region's `x, y` — in that turned frame, and a region on a link carrying `rotation` that turns it back upright. Nothing moved: `flattenRig` (`src/rig.ts`) turns every offset back and gives the unturned numbers | `RIG_LATTICE_ONE_LOOP`: change that mesh's `grid` |
|
|
@@ -2897,7 +2940,7 @@ says why each requirement exists and what breaks without it.
|
|
|
2897
2940
|
| <a id="PROPOSE_CLOTH_DRAFTED"></a>`PROPOSE_CLOTH_DRAFTED` | with `--cloth` (issue #364): the config's `cloth` block holds a recipe for a tag the proposer drafts no chain on — no part has that tag, or every `back hair` part ends above the shoulders (a bun bone, no chain). A `bottomwear` recipe with no `bottomwear` part names every `topwear` part shaped like a skirt (issue #402) | leave that recipe out, or assemble the layer it is for, or declare the named part in `cloth.bottomwear.part` |
|
|
2898
2941
|
| <a id="PROPOSE_CLOTH_PART"></a>`PROPOSE_CLOTH_PART` | with `--cloth` (issues #402, #437): `cloth.bottomwear.part` (a string) names no part, a part or cut piece whose source is not `topwear` (a `bottomwear` part takes the recipe by its tag already), or a target whose box ends above the hip + 0.25 face heights, the topwear rule's waist line; or `cloth.<tag>.parts` (or `part` as a list, issue #437) names a part not in `parts.json`, a part no link of the drafted tree crosses (`spanCrosses == 0`), or parts out of root-to-tip depth order along the chain | name the `topwear` part or its cut skirt piece that holds the skirt (or leave `part` out), or list only parts in `parts.json` that the drafted tree links cross in root-to-tip order |
|
|
2899
2942
|
| <a id="PROPOSE_WRITE_CONFIG_FRESH"></a>`PROPOSE_WRITE_CONFIG_FRESH` | with `--write-config` (issue #537): the out path exists, or is a file the run writes itself (`proposal.json`, `basis.json` under `--out`); the config is written to a new file, never over the `--cloth` config or an earlier one | name a file that does not exist yet, beside the `--cloth` config |
|
|
2900
|
-
| <a id="PROPOSE_WRITE_CONFIG_CONFLICT"></a>`PROPOSE_WRITE_CONFIG_CONFLICT` | with `--write-config` (issue #552): an entry of `bones`, `constraints` or `motion.tracks` the recipe re-drafts or removes was changed by hand in the `--cloth` config — it equals neither the draft with the recipe nor the draft without it — so the write would drop the hand edit; the object is the entry (`config.constraints[40] "skirt0a_0_physics"` in CL207), the detail names the first field the hand edit holds and what the draft writes there. Also a name the config states twice where the recipe drafts it. Every conflict is named at once and nothing is written | take the draft by removing the hand edit (or the entry), or leave the recipe out for that part (its `cloth` block); state a name once |
|
|
2943
|
+
| <a id="PROPOSE_WRITE_CONFIG_CONFLICT"></a>`PROPOSE_WRITE_CONFIG_CONFLICT` | with `--write-config` (issue #552): an entry of `bones`, `constraints` or `motion.tracks` the recipe re-drafts or removes was changed by hand in the `--cloth` config — it equals neither the draft with the recipe nor the draft without it — so the write would drop the hand edit; the object is the entry (`config.constraints[40] "skirt0a_0_physics"` in CL207), the detail names the first field the hand edit holds and what the draft writes there. With `--from <previous config>` (issue #571) an entry equal to the previous recipe's draft is the recipe's too, and the conflict is an entry that equals neither the draft, the draft without the recipe, nor the previous recipe's draft: the detail says so and reads the first differing field against the previous recipe's draft. Also a name the config states twice where the recipe drafts it. Every conflict is named at once and nothing is written | take the draft by removing the hand edit (or the entry), or leave the recipe out for that part (its `cloth` block); state a name once |
|
|
2901
2944
|
| <a id="PROPOSE_WRITE_CONFIG_SPLICE"></a>`PROPOSE_WRITE_CONFIG_SPLICE` | with `--write-config` (issue #537): the `--cloth` config's text with the drafted sections spliced in does not parse to that config with them set, and the message names the first key that moved — the text states a key twice, so the splice lands on the first and the parse keeps the last | state each key once in the `--cloth` config |
|
|
2902
2945
|
| <a id="PROPOSE_CLOTH_BACK_HAIR_CHAINS"></a>`PROPOSE_CLOTH_BACK_HAIR_CHAINS` | with `--cloth` (issues #390, #445, #461): `cloth."back hair".chains` exceeds what the narrower side's opaque width at the eye-line root row holds at the part's mesh grid (`max(1, min(floor(w_r / grid), floor(w_l / grid)))`), or places a tree root outside the 15 px dilated art mask of the `back hair` part (e.g. an inner twin-tail root over the face where the hair has no pixel); the message names the part, each side's root-row width, the grid spacing, and any off-art root with its pixel alpha | lower `cloth."back hair".chains` to at most the max fit named in the message, or omit `chains` so `propose --cloth` derives the tree count per visible hair mass on each side from its root-row width |
|
|
2903
2946
|
| <a id="PROPOSE_ACCESSORY_BODY"></a>`PROPOSE_ACCESSORY_BODY` | an accessory has nothing above its pendant rows to hang its bone on | that part's plan entry, or author its bones by hand |
|
|
@@ -3336,7 +3379,7 @@ the two examples [observed]:
|
|
|
3336
3379
|
| <a id="FEET_STAY_LEVEL"></a>`FEET_STAY_LEVEL` | issue #348. Each `footwear` part whose slot's bone hangs from another bone (rig.json's `parent`; a part on `root` hangs from nothing and is not read), over every animation, the idle included, from the same `rigc render --folds` run, which then also writes `--geometry`: on each frame where a bone above the part's bone — its parent or any bone over that — has a world rotation other than its setup pose's (no threshold; the nearest is named), the drawing's turn: the least-squares rotation carrying the attachment's setup-pose vertices onto the frame's, both centred (a region's four corners turn exactly with its bone). **This is the reading of "the sole's angle"**: no rig data marks a sole, so level is the drawing's setup angle, the painting's own; a declared sole line would replace it if one ever exists | every read foot within ±10 degrees — HQ's line (rig-c/parts#347 ruling 4), not a figure measured here | not written: the shoes ride `root` | not written: the shoes ride `root` | SKIP when no frame turns a bone above any read foot (not counted as measured); not written at all when no `footwear` part hangs from a bone, or `motion.json` holds no animation, so the examples' `check.json` keep their keys |
|
|
3337
3380
|
| <a id="STRESS_OFF_CHAIN"></a>`STRESS_OFF_CHAIN` | each mesh from a See-through `handwear` layer whose weights ride exactly one chain the idle keys: the chain's first link turned 90°, −90° and 180° from its setup rotation (interim, issue #347: until a config block declares each limb and its joint's range), one `rigc build --profile spine` and one `rigc render --geometry` of a copy with its physics constraints left out; per turn, the part's worst edge as max(ratio, 1/ratio) against the setup pose, at four places, and the bones it is weighted to off the chain. A part riding two chains, or none, or not a weighted mesh, is listed under `not_read` with the reason, and so is a part a `config.limbs` segment made (issue #349): `LIMB_ONE_BONE` holds it to its one bone, so it turns rigidly by construction | 1.0000 — the part turning rigidly with its chain; float32 noise on rigid sleeves measured at most 1.0000090 | SKIP: `sleeves` rides 2 chains | SKIP: `sleeves` rides 2 chains | no part comes from a `handwear` layer or the motion has no idle (the line is not written); every `handwear` part not read (SKIP, each reason) |
|
|
3338
3381
|
| <a id="LIMB_ONE_BONE"></a>`LIMB_ONE_BONE` | issue #349. Each part `parts.json` records as a limb segment's (`limb`), read off `rig.json` alone, nothing rendered: the bones its attachments are bound to — a mesh's weights, or its slot's bone for a region — against its segment's bone (under `config.turn`, that bone's leaf twin counts as the bone) | every limb part bound to its one bone alone, weight 1 | not written: no limbs | not written: no limbs | never: written only when `parts.json` holds a limb part, so the examples' `check.json` keep their keys |
|
|
3339
|
-
| <a id="SEAM_OVER_RANGE"></a>`SEAM_OVER_RANGE` | issue #360. Each limb part `parts.json` records with a `range` (`config.limbs.<limb>.segments[k].range`): a copy of the rig with its physics constraints left out, its joint's bone keyed `rotate` to the range's min, its middle and its max (each angle once, rigc's sense, counter-clockwise on screen), one `rigc build --profile spine` and two `rigc render`s on one grid — one with the joint's own art drawn under the limb (the limb's parts at rest inside the cap's disc, one pixel in from every edge of what is opaque there, on the bone the joint's bone hangs from), one with it hidden (`--hide`); per pose, the seam figures of the second against the first | the rest-pose seam's: mean \|d\| ≤ 1.0 and ≤ 50 rig px over 40 at every pose; a `cap_shape: "disc"` joint also 0 frame px over 40 | not written: no limbs | not written: no limbs | never: written only when `parts.json` holds a limb record with a `range`, so the examples' `check.json` keep their keys |
|
|
3382
|
+
| <a id="SEAM_OVER_RANGE"></a>`SEAM_OVER_RANGE` | issue #360. Each limb part `parts.json` records with a `range` (`config.limbs.<limb>.segments[k].range`): a copy of the rig with its physics constraints left out, its joint's bone keyed `rotate` to the range's min, its middle and its max (each angle once, rigc's sense, counter-clockwise on screen), one `rigc build --profile spine` and two `rigc render`s on one grid — one with the joint's own art drawn under the limb (the limb's parts at rest inside the cap's disc, one pixel in from every edge of what is opaque there, on the bone the joint's bone hangs from), one with it hidden (`--hide`); per pose, the seam figures of the second against the first. Issue #557: per pose also the **contour step**, a reading and never a bar — on the frame with the joint's art hidden, lines across the upper segment (perpendicular to its bone's direction at the setup pose, origin to the joint) one frame pixel apart, from one cap radius before the joint line to one past it; on each, from the axis outward on each side, the outline's extent (the first sample within 40 of the background, out to two radii); the step on a side is the largest change between adjacent lines, in rig px, `left` and `right` as the cap's `short_px` names them. A limb whose outline the bend keeps reads one frame pixel, 1 / s rig px (1.161 at the synthetic fixtures' s 0.861413); the unfilled two-layer knee of the selftest reads 11.609 on the opening side at ±15° and the filled one 1.161. The row prints it; `check.json` carries it (`step_px` on each pose) only for a joint whose cap record carries `short_px` | the rest-pose seam's: mean \|d\| ≤ 1.0 and ≤ 50 rig px over 40 at every pose; a `cap_shape: "disc"` joint also 0 frame px over 40 | not written: no limbs | not written: no limbs | never: written only when `parts.json` holds a limb record with a `range`, so the examples' `check.json` keep their keys |
|
|
3340
3383
|
|
|
3341
3384
|
**`TIP_OVER_ROOT` is measured from geometry, not pixels (issue #118).** Until #118 the
|
|
3342
3385
|
line rendered each part alone at `--max 640` and took, per half of its frame box, the
|
package/docs/STABILITY.md
CHANGED
|
@@ -53,7 +53,7 @@ defaults are the help's too, and none is guessed from the art.
|
|
|
53
53
|
| `comfy` | `seethrough`: `--image`, `--out`; optional `--resolution`, `--steps`, `--seed`, `--prefix`, `--offload`, `--lama`, `--nf4`, `--host`, `--wait`, `--timeout`, `--poll` |
|
|
54
54
|
| `assemble` | `--propose-plan` with `--source`, `--full`, `--config`, optional `--head`; or `--source`, `--full`, `--config`, `--out`, optional `--head` (without it, one run: issue #359), `--seam` (near-white, silhouette), `--project` (core, visible), `--alt` (repeatable `<name>=<source>:<full>[:<head>]`, issue #454) |
|
|
55
55
|
| `propose` | `--head-box` with `--full`, `--canvas`; optional `--no-back-hair` |
|
|
56
|
-
| `propose` | `--parts`, `--source`, `--out`; optional `--compare` or `--from-config` (one of them), `--keypoints`, `--person` and `--wrist-snap` (each with `--keypoints`), `--full`, `--head` (the turn control's depths; not with `--from-config`), `--grid-rule` (`figure` or `part`, the drafted lattice's spacing; not with `--from-config`), `--cloth` (a config whose `cloth` block is the recipe, issue #364; not with `--from-config`), `--write-config` (a new file, with `--cloth` and not with `--from-config`, issue #537 — additive: without it nothing `propose` writes changes) |
|
|
56
|
+
| `propose` | `--parts`, `--source`, `--out`; optional `--compare` or `--from-config` (one of them), `--keypoints`, `--person` and `--wrist-snap` (each with `--keypoints`), `--full`, `--head` (the turn control's depths; not with `--from-config`), `--grid-rule` (`figure` or `part`, the drafted lattice's spacing; not with `--from-config`), `--cloth` (a config whose `cloth` block is the recipe, issue #364; not with `--from-config`), `--write-config` (a new file, with `--cloth` and not with `--from-config`, issue #537 — additive: without it nothing `propose` writes changes), `--from` (the config the recipe is changed from, only with `--write-config`, issue #571 — additive: without it nothing `--write-config` writes changes) |
|
|
57
57
|
| `compare` | `--left`, `--right`; optional `--map` |
|
|
58
58
|
| `rig` | `--config`, `--parts`, `--out`; optional `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon) |
|
|
59
59
|
| `check` | `--rig`, `--out`; optional `--parts`, `--source`, `--page-edges`, `--pack-shape`, `--requirements`, `--seam-pairs`, `--keep-stills`, `--set` or `--all-sets` (issue #454), `--environment` (issue #538, additive) |
|
|
@@ -190,15 +190,15 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
|
|
|
190
190
|
|
|
191
191
|
| file | written by | what is stable |
|
|
192
192
|
| --- | --- | --- |
|
|
193
|
-
| `parts.json`, `parts/<name>.png` | `assemble`, `build` | one record per part: `from` (`<run>:<tag>` with a See-through v3 tag, `<run>:author:<label>` for a layer the author made — issue #256, read by no role rule — or `painting:<name>` for a patch), `cut_from` on a cut's piece only (the plan part it was cut from, issue #230), `limb` on a limb segment's part only (issue #349: `name`, `segment`, `of`, `bone`, and below a joint `joint`, `range` when the config segment declares one (issue #360), and `cap` — `at`, `width`, `radius`, `box`, `layer_px`, `image_px` with a cap image, `image_layer` after it when that image was a hidden-art layer — issue #359, `<run>:<layer>:hidden` — `missing_px`, then `shape` and `upper_px` under the segment's `cap_shape
|
|
193
|
+
| `parts.json`, `parts/<name>.png` | `assemble`, `build` | one record per part: `from` (`<run>:<tag>` with a See-through v3 tag, `<run>:author:<label>` for a layer the author made — issue #256, read by no role rule — or `painting:<name>` for a patch), `cut_from` on a cut's piece only (the plan part it was cut from, issue #230), `limb` on a limb segment's part only (issue #349: `name`, `segment`, `of`, `bone`, and below a joint `joint`, `range` when the config segment declares one (issue #360), and `cap` — `at`, `width`, `radius`, `box`, `layer_px`, `image_px` with a cap image, `image_layer` after it when that image was a hidden-art layer — issue #359, `<run>:<layer>:hidden` — `missing_px`, `short_px` (`left`, `right`) after it only when `missing_px` is above 0 (issue #557, additive), then `shape` and `upper_px` under the segment's `cap_shape` — `upper_px` without the shape only when the upper segment gave a pixel (issue #557, additive) — and `edge`, `feather_radius` and `feathered_px` under its `edge`, and `cut_band`, `cut_offset`, `cut_at`, `cut_crossed`, `joint_crossed`, `band_px` and `stroke_luma_max` (when the band has a split) under its `cut_band`, issue #360), the box `x`, `y`, `w`, `h` (rig px), the pixel counts `src/parts.ts` documents (`opaque_px`, `visible_px`, `occluded_px`, `projected_core_px`, `source_px_taken`, `visible_not_projected_px`, `refused_drift_px`, `merged_px`, `seam_override_px`, and `fringe_pushed_back` when not empty); `rig_size`, `scale_rig_per_source`, `ghost_px`, `recomposite`, and `scene` (`figure_alpha_above`, `figure_px`, `figure_mask`) under `config.scene` only, with the figure's mask `figure_rig.png` beside the recomposite (issue #242); `single_run_pad` (`file_size`, `side`, `left`, `top`) last, only for one run whose file is not the `seethrough.resolution` square (issue #366) |
|
|
194
194
|
| `rig/rig.json`, `rig/motion.json`, `rig/images/` | `rig`, `build`, `compose` | rig-c's rig spec and motion spec, `spec` `rigc-rig/1` and `rigc-motion/1`: the format is rig-c's, and this package writes only what rig-c's parser accepts; a bone carries `inherit` only where config.bones declares one, rig-c's mode name as written (issue #348) |
|
|
195
195
|
| `rig/mesh_report.json` | `rig`, `build` | one row per mesh: `part`, `vertices`, `triangles`, `hull`, `bones` (the sorted names the attachment's weights bind in the written `rig.json`, in every mode — since 1.49.1 (issue #536) a rig with `turn` or `stops` names the twins and `<link>_d` bones there, where the row named the segment bones they replace: a fix of a wrong name, not a format change; a rig with neither writes the bytes it wrote), `max_influences`, `mean_influences`, `art_coverage`, then `grid` (a lattice); `mode` "contour" with `params`, `contour`, `regions` (`params.maxEdge` and `contour.outlineCut` only when the mesh declares `contour.maxEdge`; `params.bridges` and `contour.bridges` — `islands`, `units`, `joins`, and `faint` only when an island was left out as faint — only when it declares `contour.bridges`); or `mode` "influence" with `influence` (as declared), `reach` (`farthest`, `at`, `nearest`, `capped`, `widest`) and `lattice` (rig-c's `SegmentsLatticeReport`: `cols`, `rows`, `artCells`, `keptCells`, `passes`, `islands`) (issue #378); or `mode` "auto" with `settings`, `source`, `result`, `residuals`, `worst_residual`, `worst_region`, `motion_amplitude`, `termination`, `weights`, `regions`, `deformation`, `quality_report` (rig-c's `mesh-quality-report/1` document whole), `motion_report`, and, only when they apply, `stray_cleared` (after `source`, when `source.stray` is declared), `skinning_residual` and `replay` (`AutoMeshReport` in `src/rig.ts`); in every mode, `exponent` when the mesh declares one (after `grid`, or after `mode`); `influences`, as declared, when a lattice or contour mesh declares it (after `exponent`, or where `exponent` would be); `stray` and `stray_cleared` when a lattice mesh declares `stray` (after `influences`, or where `influences` would be); `ribs` when a contour or auto mesh declares them (after `exponent`, or after `mode`), and then `contour.ribs` (`source.contour.ribs` in the automatic mode) |
|
|
196
|
-
| `check/check.json` and the rest of `check/` | `check`, `build`, `compose` | the reference's fields in its order (`gate_spine_html_green`, `rigc_entry`, `pack_mode`, `environment` after it under `--environment` only (issue #538, additive: `path` as given, `sha256`, `steps`), `loop_max_diff`, the seam figures — `seam_mean`, `seam_px_over_40`, `seam_px_over_80` and, from issue #216, `seam_frame_scale`, the frame scale the bar divides the count by), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `seam_border` after the seam figures when `parts.json` declares a scene (issue #242), `seam_pairs` after the seam figures when the seam bar fails or `--seam-pairs` asks (each row's picture under `check/seam/`; `--keep-stills` leaves the setup-pose still and its `frames.json` under `check/setup_still/`), then the judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`), `ANIMATIONS_DO_NOT_FOLD` after them when `motion.json` holds an animation, the idle included — on every rig with an idle since issue #333 (issues #320, #363: `status`, `animations`, `fps`, `physics_left_out`, `folding` — each `animation`, `slot`, `attachment`, `frames`, `worst_frame`, `worst_triangles`, `triangles`, `physics_on`, `physics_off`, `physics_constraints`, `declared`, `line` — `folding_undeclared`, `declared_not_folding`, `bar`), `FEET_STAY_LEVEL` after it when a `footwear` part rides a bone that hangs from another and `motion.json` holds an animation (issue #348: `status`, `reason` on a SKIP only, `animations`, `fps`, `physics_left_out`, `bar_degrees`, `bar`, `feet` — each `part`, `bone`, `hangs_from`, `status`, `frames_posed`, `worst`), `LIMB_ONE_BONE` after it (and after `STRESS_OFF_CHAIN` where that is written) when `parts.json` holds a limb segment's part (issue #349: `status`, `parts` — each `part`, `limb`, `bone`, `status`, `others`), `SEAM_OVER_RANGE` after it when a limb record carries a `range` (issue #360: `status`, `frame_scale`, `physics_left_out`, `bar` — `mean`, `rig_px_over_40`, `level` — and `joints`, each `part`, `limb`, `segment`, `joint`, `bone`, `range`, `at`, `radius`, `under_px`, `status`, `poses` — each `degrees`, `status`, `mean`, `px_over_40`, `px_over_80`, `rig_px_over_40` — and `unread` on a joint not read), `SEAM_OVER_MOTION` after it when `parts.json` holds a limb segment or `--requirements` a `seam` (issue #493, a warning and not a bar: `status` — `WARN`, `CLEAR` or `SKIP` — `reason` on a SKIP only, `fps`, `floor`, `pairs` — each `pair`, `a`, `b`, `declared`, and `unread` on a pair not read — `animations`, `warnings`, `rows` — each `pair`, `animation`, `status`, `drawn`, `banded`, `opens`, `px`, `max_px`, `under_floor`), `CLOTH_SETTLES` after it when the rig declares a physics constraint and has an idle (issue #501, a warning and not a bar: `status` — `WARN` or `CLEAR` — `fps`, `frames`, `duration`, `floor_rig_px_per_s`, `floor`, `levers`, `warnings`, `rows` — each `tip`, `root`, `links`, `point`, `settles_at`, `max_rig_px_per_s`, `min_rig_px_per_s`), `RECOMPOSITE_HOLES` (its first figure `declared_scene` when `parts.json` declares a scene), `SETUP_POSE_VS_SOURCE`, `skipped`, a `requirements` block under `--requirements` (or `build`'s `config.requirements`) — `file` first from issue #556, additive: `path` as typed or declared, `sha256`, `keys` — and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes |
|
|
196
|
+
| `check/check.json` and the rest of `check/` | `check`, `build`, `compose` | the reference's fields in its order (`gate_spine_html_green`, `rigc_entry`, `pack_mode`, `environment` after it under `--environment` only (issue #538, additive: `path` as given, `sha256`, `steps`), `loop_max_diff`, the seam figures — `seam_mean`, `seam_px_over_40`, `seam_px_over_80` and, from issue #216, `seam_frame_scale`, the frame scale the bar divides the count by), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `seam_border` after the seam figures when `parts.json` declares a scene (issue #242), `seam_pairs` after the seam figures when the seam bar fails or `--seam-pairs` asks (each row's picture under `check/seam/`; `--keep-stills` leaves the setup-pose still and its `frames.json` under `check/setup_still/`), then the judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`), `ANIMATIONS_DO_NOT_FOLD` after them when `motion.json` holds an animation, the idle included — on every rig with an idle since issue #333 (issues #320, #363: `status`, `animations`, `fps`, `physics_left_out`, `folding` — each `animation`, `slot`, `attachment`, `frames`, `worst_frame`, `worst_triangles`, `triangles`, `physics_on`, `physics_off`, `physics_constraints`, `declared`, `line` — `folding_undeclared`, `declared_not_folding`, `bar`), `FEET_STAY_LEVEL` after it when a `footwear` part rides a bone that hangs from another and `motion.json` holds an animation (issue #348: `status`, `reason` on a SKIP only, `animations`, `fps`, `physics_left_out`, `bar_degrees`, `bar`, `feet` — each `part`, `bone`, `hangs_from`, `status`, `frames_posed`, `worst`), `LIMB_ONE_BONE` after it (and after `STRESS_OFF_CHAIN` where that is written) when `parts.json` holds a limb segment's part (issue #349: `status`, `parts` — each `part`, `limb`, `bone`, `status`, `others`), `SEAM_OVER_RANGE` after it when a limb record carries a `range` (issue #360: `status`, `frame_scale`, `physics_left_out`, `bar` — `mean`, `rig_px_over_40`, `level` — and `joints`, each `part`, `limb`, `segment`, `joint`, `bone`, `range`, `at`, `radius`, `under_px`, `status`, `poses` — each `degrees`, `status`, `mean`, `px_over_40`, `px_over_80`, `rig_px_over_40`, and `step_px` (`left`, `right`) only on a joint whose cap record carries `short_px` (issue #557, additive) — and `unread` on a joint not read), `SEAM_OVER_MOTION` after it when `parts.json` holds a limb segment or `--requirements` a `seam` (issue #493, a warning and not a bar: `status` — `WARN`, `CLEAR` or `SKIP` — `reason` on a SKIP only, `fps`, `floor`, `pairs` — each `pair`, `a`, `b`, `declared`, and `unread` on a pair not read — `animations`, `warnings`, `rows` — each `pair`, `animation`, `status`, `drawn`, `banded`, `opens`, `px`, `max_px`, `under_floor`), `CLOTH_SETTLES` after it when the rig declares a physics constraint and has an idle (issue #501, a warning and not a bar: `status` — `WARN` or `CLEAR` — `fps`, `frames`, `duration`, `floor_rig_px_per_s`, `floor`, `levers`, `warnings`, `rows` — each `tip`, `root`, `links`, `point`, `settles_at`, `max_rig_px_per_s`, `min_rig_px_per_s`), `RECOMPOSITE_HOLES` (its first figure `declared_scene` when `parts.json` declares a scene), `SETUP_POSE_VS_SOURCE`, `skipped`, a `requirements` block under `--requirements` (or `build`'s `config.requirements`) — `file` first from issue #556, additive: `path` as typed or declared, `sha256`, `keys` — and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes |
|
|
197
197
|
| `layers.json`, `<index>_<name>.png` | `layers --export` | the wrapper form `layers` reads (issue #359): `width`, `height`, then `layers` — each `name`, `filename`, `left`, `top`, `right`, `bottom`, `depth_median` (the input's own; `null` for a PSD, the manifest's order then being the draw order) — drawn layers in draw order, then the hidden-art layers; a PNG's name is its index and its layer's name, and the manifest, not the file name, says which layer it is |
|
|
198
198
|
| the template `.psd` | `layers --template` | one empty layer per name `templateLayerNames` (`src/layers.ts`) holds, bottom first; the same canvas writes the same bytes |
|
|
199
199
|
| `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections, `constraints` with `--cloth` (issue #364: a physics constraint per tree link named `<bone>_physics`, a dangling end's `<bone>_translate`, and a skirt swing's transforms `<swing>_<tree>`), `turn` with `--full`/`--head`, `proposed_rig_scale` (issue #354), and `notes` |
|
|
200
200
|
| `basis.json` | `propose` | spec `spine-parts-basis/1` |
|
|
201
|
-
| the `--write-config` file | `propose --cloth … --write-config` | the `--cloth` config read, byte for byte, but for the entries the recipe drafts (issue #537), measured against the same parts drafted without it: `meshes.<part>` and `regions.<part>` for each part the recipe moves, and `bones`, `constraints` and `motion.tracks` entry by entry by name (issue #552) — an entry the recipe drafts replaced or inserted, one it replaces removed, every other entry kept with its bytes and its place, and a hand edit to an entry the recipe owns refused by name (`PROPOSE_WRITE_CONFIG_CONFLICT`) rather than dropped; a section the input lacks is inserted |
|
|
201
|
+
| the `--write-config` file | `propose --cloth … --write-config` | the `--cloth` config read, byte for byte, but for the entries the recipe drafts (issue #537), measured against the same parts drafted without it: `meshes.<part>` and `regions.<part>` for each part the recipe moves, and `bones`, `constraints` and `motion.tracks` entry by entry by name (issue #552) — an entry the recipe drafts replaced or inserted, one it replaces removed, every other entry kept with its bytes and its place, and a hand edit to an entry the recipe owns refused by name (`PROPOSE_WRITE_CONFIG_CONFLICT`) rather than dropped; a section the input lacks is inserted; with `--from <previous config>` (issue #571) an entry equal to the previous recipe's draft is the recipe's as well |
|
|
202
202
|
| `config.json`, `mesh_draft.json` | `mesh-draft` | `config.json` is the config read, byte for byte, but for each drafted part's `meshes` entry; `mesh_draft.json` is spec `rig-parts-mesh-draft/1`: `rules`, `lattice_check`, `draft_check`, `figure_overdraw`, and per lattice part `part`, `mode`, `lattice`, `candidate` (with `bridges` — `units`, `joins` of `span` and `area_px2`, `area_px2`, `overdraw`, and `faint` only when an island was left out as faint — only when the candidate is a bridged one, issue #326), `delta` (under `--more-triangles` only), `readings`, `gate` (each line's `reading`, `pass`, `text`, `margin`), `note`; and `not_compared` (issue #307), only when the config holds an `auto` or `contour` entry: per such entry, in config order, `part`, `mode` and `note` — the part was neither drafted nor compared, and a config of lattices alone writes no such key |
|
|
203
203
|
| `rig.json`, `motion.json`, `mesh_report.json`, `images/`, `remesh.json` | `remesh` | the input revision's files, `rig.json` and `mesh_report.json` with the part's attachment and row replaced (under `--delta` the row is the input's with its counts, `bones` and influences recomputed and `mode` "delta"), every other file byte for byte; `remesh.json` is spec `rig-parts-remesh/1`: `request` (`part`, `mesh`, `mesh_sha256`, `idle_keys`, `page_edges`, `pack_shape`; under `--delta`, issue #526: `part`, `delta` — the mesh edit as read — `delta_sha256`, `ops`, `applied` — `moved`, `added`, `removed`, `constrained` — `idle_keys`, `page_edges`, `pack_shape`), `versions` (`parts`, `rigc`), `input` (`rig` — `path`, `bytes`, `sha256` per file — `config_sha256`, `parts_json_sha256`), `baseline` (`reproduced`, `not_compared`; under `--delta` `reproduced` holds one sentence: no config is built, the input is the baseline), `target` (`before`, `after`: `vertices`, `triangles`, `hull`), `preserved` (`identical`, `spliced`), `revision` (per file as `input.rig`), `check` (`input`, `revision`: `pass`, `measured`, `not_met`, `skipped`, `physics`, then `line` and `status` — issue #541, additive, the spec stays `rig-parts-remesh/1`: `line` is the last line `check` prints on that tree, byte for byte, and `status` every block's status as that tree's `check.json` records it, by name in the file's order — `gate`, `loop` and `seam` read from their figures, every other block its own `status`, a block the file does not write absent; `scope`) |
|
|
204
204
|
| `rig.json`, `motion.json`, `mesh_report.json`, `images/`, `retune.json` | `retune` | the input revision's files, `rig.json` with `constraints` and `invariants.detached` replaced and `motion.json` with `animations.idle` replaced by the config's build, every other file byte for byte; `retune.json` is spec `rig-parts-retune/1`: `request` (`idle_keys`, `page_edges`, `pack_shape`), `versions` (`parts`, `rigc`), `input` (`rig` — `path`, `bytes`, `sha256` per file — `config_sha256`, `parts_json_sha256`), `held` (`compared`, `not_compared`), `changed` (`constraints` — each `name`, `type`, `fields` — `added`, `removed`, `idle`: `tracks_changed`, `duration` as before and after), `preserved` (`identical`, `spliced`), `revision` (per file as `input.rig`), `check` (`input`, `revision`: `pass`, `measured`, `not_met`, `skipped`, `physics`, then `line` and `status` — issue #541, additive, the spec stays `rig-parts-retune/1`: `line` is the last line `check` prints on that tree, byte for byte, and `status` every block's status as that tree's `check.json` records it, by name in the file's order — `gate`, `loop` and `seam` read from their figures, every other block its own `status`, a block the file does not write absent; `scope`) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-parts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.51.0",
|
|
4
4
|
"description": "AI-authored 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through rig-c before anything is written. A CLI for agents that cannot see the image.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/assemble.ts
CHANGED
|
@@ -2701,7 +2701,18 @@ export interface LimbCount {
|
|
|
2701
2701
|
before: number;
|
|
2702
2702
|
shares: Array<{ part: string; px: number }>;
|
|
2703
2703
|
/** The joints whose cap this part's segments carry, in `config.limbs` order. */
|
|
2704
|
-
joints: Array<{
|
|
2704
|
+
joints: Array<{
|
|
2705
|
+
limb: string;
|
|
2706
|
+
joint: string;
|
|
2707
|
+
part: string;
|
|
2708
|
+
upper: string;
|
|
2709
|
+
cap: CapRecord;
|
|
2710
|
+
/** Issue #557: the parts the lower and the upper segment take, the half-width about the joint, and the short pixels within it. */
|
|
2711
|
+
from?: string;
|
|
2712
|
+
upperFrom?: string;
|
|
2713
|
+
half?: number;
|
|
2714
|
+
shortInHalf?: number;
|
|
2715
|
+
}>;
|
|
2705
2716
|
}
|
|
2706
2717
|
|
|
2707
2718
|
/**
|
|
@@ -2720,6 +2731,41 @@ function cutPhrase(k: CapRecord, upper: string, lower: string): string {
|
|
|
2720
2731
|
return `cut_band ${k.cut_band ?? 0}: the cut ${where}, through [${(k.cut_at ?? k.at).join(', ')}], crosses ${k.cut_crossed ?? 0} stroke px; the joint line crosses ${k.joint_crossed ?? 0} (${stroke})`;
|
|
2721
2732
|
}
|
|
2722
2733
|
|
|
2734
|
+
/**
|
|
2735
|
+
* Issue #557: the `LINT` line of a short cap — one whose disc, on the upper
|
|
2736
|
+
* side of the cut, holds pixels no layer of the limb gives (`short_px`, by
|
|
2737
|
+
* side of the limb's axis: `left` where a pixel centre's offset across the
|
|
2738
|
+
* axis is 0 or above, the side toward smaller x when the limb hangs down the
|
|
2739
|
+
* rig, `right` the other) — naming the limb, the joint, the part, both
|
|
2740
|
+
* figures, the radius and what filled the rest; with the half-disc, how many
|
|
2741
|
+
* of them `cap_shape: "disc"` would keep. Null for a cap that is not short.
|
|
2742
|
+
*/
|
|
2743
|
+
export function shortCapLint(j: LimbCount['joints'][number]): string | null {
|
|
2744
|
+
const k = j.cap;
|
|
2745
|
+
if (k.short_px === undefined) return null;
|
|
2746
|
+
const n = k.short_px.left + k.short_px.right;
|
|
2747
|
+
const where =
|
|
2748
|
+
j.from === undefined || j.upperFrom === undefined || j.upperFrom === j.from
|
|
2749
|
+
? `the layer of "${j.from ?? j.part}" has no pixel of the limb`
|
|
2750
|
+
: `neither the layer of "${j.from}" nor that of "${j.upperFrom}" has a pixel of the limb`;
|
|
2751
|
+
const filled = [
|
|
2752
|
+
...(k.image_px === undefined ? [] : [`${k.image_px} px from ${k.image_layer === undefined ? 'the cap image' : `the hidden-art layer ${k.image_layer}`}`]),
|
|
2753
|
+
`${k.layer_px} px from "${j.part}"'s layer`,
|
|
2754
|
+
`${k.upper_px ?? 0} px from "${j.upper}"`,
|
|
2755
|
+
].join(', ');
|
|
2756
|
+
const half = j.half ?? 0;
|
|
2757
|
+
const disc =
|
|
2758
|
+
k.shape === 'disc' || j.shortInHalf === undefined
|
|
2759
|
+
? ''
|
|
2760
|
+
: j.shortInHalf === 0
|
|
2761
|
+
? `; under cap_shape "disc" the disc is the half-width about the joint, ${half} px, and none of them lies within it`
|
|
2762
|
+
: `; under cap_shape "disc" the disc is the half-width about the joint, ${half} px, and ${j.shortInHalf} of them lie within it`;
|
|
2763
|
+
return (
|
|
2764
|
+
` LINT "${j.part}" cap at [${k.at[0]}, ${k.at[1]}] (limb "${j.limb}", joint "${j.joint}", radius ${k.radius} px): short_px left ${k.short_px.left}, right ${k.short_px.right}: ` +
|
|
2765
|
+
`no layer gives them — ${n} px of the disc on the upper side of the cut, where ${where}; the rest of the disc is filled, ${filled}${disc}`
|
|
2766
|
+
);
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2723
2769
|
/**
|
|
2724
2770
|
* The lines `assemble` prints for the limbs (issue #349), one per part a limb
|
|
2725
2771
|
* took — ` limb cut: "<of>" opaque <n> = "<part>" <px> + …` — and one per
|
|
@@ -2733,7 +2779,8 @@ export function limbLines(counts: readonly LimbCount[]): string[] {
|
|
|
2733
2779
|
const k = j.cap;
|
|
2734
2780
|
const base = k.image_px === undefined ? `${k.layer_px} px from the layer` : `${k.image_px} px from ${k.image_layer === undefined ? 'the cap image' : `the hidden-art layer ${k.image_layer}`}, ${k.layer_px} px from the layer`;
|
|
2735
2781
|
// Issue #360: what cap_shape "disc" and edge "feather" did, said only when declared, so a limb without them prints what it did.
|
|
2736
|
-
|
|
2782
|
+
// Issue #557: the half-disc says it only when the neighbouring segment gave a pixel, so a full cap prints what it printed.
|
|
2783
|
+
const from = k.upper_px === undefined ? base : `${base}, ${k.upper_px} px from "${j.upper}"`;
|
|
2737
2784
|
const radius =
|
|
2738
2785
|
k.shape === undefined
|
|
2739
2786
|
? `a disc of radius ${k.radius} px (the limb ${k.width} px across the joint line)`
|
|
@@ -2749,6 +2796,8 @@ export function limbLines(counts: readonly LimbCount[]): string[] {
|
|
|
2749
2796
|
out.push(
|
|
2750
2797
|
` limb cap: "${j.limb}" joint "${j.joint}" at [${k.at[0]}, ${k.at[1]}], on "${j.part}": ${radius}, box [${k.box.join(', ')}] — on the upper side of the line ${from}, ${miss}${shaped.map((q) => `; ${q}`).join('')}`,
|
|
2751
2798
|
);
|
|
2799
|
+
const lint = shortCapLint(j);
|
|
2800
|
+
if (lint !== null) out.push(lint);
|
|
2752
2801
|
}
|
|
2753
2802
|
}
|
|
2754
2803
|
return out;
|
|
@@ -3051,6 +3100,11 @@ function cutLimbs(
|
|
|
3051
3100
|
let imagePx = 0;
|
|
3052
3101
|
let upperPx = 0;
|
|
3053
3102
|
let missing = 0;
|
|
3103
|
+
// Issue #557: the cap's shortfall by side of the limb's axis (left: across >= 0), and how much of it lies within
|
|
3104
|
+
// the half-width about the joint, the disc cap_shape "disc" would keep.
|
|
3105
|
+
let shortLeft = 0;
|
|
3106
|
+
let shortRight = 0;
|
|
3107
|
+
let shortInHalf = 0;
|
|
3054
3108
|
for (let y = Math.max(0, box[1]); y < Math.min(H, box[1] + box[3]); y++) {
|
|
3055
3109
|
for (let x = Math.max(0, box[0]); x < Math.min(W, box[0] + box[2]); x++) {
|
|
3056
3110
|
if (!inDisc(line.at, radius, x, y) || pastJoint(cut, x, y)) continue;
|
|
@@ -3062,11 +3116,18 @@ function cutLimbs(
|
|
|
3062
3116
|
} else if (ownLower[p] === li && lowerSrc.r.data[p * 4 + 3] > 0) {
|
|
3063
3117
|
lower.data.set(lowerSrc.r.data.subarray(p * 4, p * 4 + 4), p * 4);
|
|
3064
3118
|
layerPx++;
|
|
3065
|
-
} else if (
|
|
3119
|
+
} else if (upper.data[p * 4 + 3] > 0) {
|
|
3066
3120
|
// Issue #360: the disc is filled from the art — the upper segment's, where the lower's layer has none.
|
|
3121
|
+
// Issue #557: under either shape — the half-disc too takes the neighbouring segment's pixels, so a cap is
|
|
3122
|
+
// short only where no layer of the limb gives a pixel.
|
|
3067
3123
|
lower.data.set(upper.data.subarray(p * 4, p * 4 + 4), p * 4);
|
|
3068
3124
|
upperPx++;
|
|
3069
|
-
} else
|
|
3125
|
+
} else {
|
|
3126
|
+
missing++;
|
|
3127
|
+
if (jointOffsets(line, x, y).across >= 0) shortLeft++;
|
|
3128
|
+
else shortRight++;
|
|
3129
|
+
if (inDisc(line.at, half, x, y)) shortInHalf++;
|
|
3130
|
+
}
|
|
3070
3131
|
}
|
|
3071
3132
|
}
|
|
3072
3133
|
if (img !== null && imagePx === 0 && hidden === null) {
|
|
@@ -3111,7 +3172,8 @@ function cutLimbs(
|
|
|
3111
3172
|
...(img === null ? {} : { image_px: imagePx }),
|
|
3112
3173
|
...(hidden === null ? {} : { image_layer: hidden.layer }),
|
|
3113
3174
|
missing_px: missing,
|
|
3114
|
-
...(
|
|
3175
|
+
...(missing === 0 ? {} : { short_px: { left: shortLeft, right: shortRight } }),
|
|
3176
|
+
...(disc ? { shape: 'disc' as const, upper_px: upperPx } : upperPx === 0 ? {} : { upper_px: upperPx }),
|
|
3115
3177
|
...(sg.edge === 'feather' ? { edge: 'feather' as const, feather_radius: band, feathered_px: featheredPx } : {}),
|
|
3116
3178
|
...(placed === undefined
|
|
3117
3179
|
? {}
|
|
@@ -3128,7 +3190,7 @@ function cutLimbs(
|
|
|
3128
3190
|
if (hidden !== null) readHidden(hidden.layer, `config.limbs.${l}.segments[${k}].cap (part "${sg.part}")`);
|
|
3129
3191
|
parts[at].record.cap = cap;
|
|
3130
3192
|
const c = counts.find((q) => q.of === sg.from);
|
|
3131
|
-
if (c !== undefined) c.joints.push({ limb: l, joint: String(sg.joint), part: sg.part, upper: segs[k - 1].part, cap });
|
|
3193
|
+
if (c !== undefined) c.joints.push({ limb: l, joint: String(sg.joint), part: sg.part, upper: segs[k - 1].part, cap, from: sg.from, upperFrom: upFrom, half: pyRound(half, 3), shortInHalf });
|
|
3132
3194
|
});
|
|
3133
3195
|
}
|
|
3134
3196
|
refuseIfAny(problems);
|
package/src/build.ts
CHANGED
|
@@ -1504,14 +1504,18 @@ export function limbCheckLines(line: LimbLine): string[] {
|
|
|
1504
1504
|
* The seam over the range (issue #360) as the console prints it: the status, what was posed and the bar, then each
|
|
1505
1505
|
* joint at each angle on a line of its own — its status and the seam figures there — or, not read, why.
|
|
1506
1506
|
*/
|
|
1507
|
-
export function sweepCheckLines(line: SweepLine): string[] {
|
|
1507
|
+
export function sweepCheckLines(line: SweepLine, steps: ReadonlyMap<string, { left: number; right: number }> = new Map()): string[] {
|
|
1508
1508
|
const s = line.frame_scale;
|
|
1509
1509
|
return [
|
|
1510
1510
|
`${SWEEP_LINE}: ${line.status} — ${line.joints.length} joint(s) bent to their declared range's ends and middle${line.physics_left_out === 0 ? '' : `, ${line.physics_left_out} physics constraint(s) left out`}; at each, the pose against the same pose with the joint's own art drawn under the limb, on the rest-pose seam's bar (mean |d| <= ${line.bar.mean.toFixed(1)}, <= ${line.bar.rig_px_over_40} rig px over ${line.bar.level})${s === null ? '' : ` at frame scale ${s}`}`,
|
|
1511
1511
|
...line.joints.flatMap((j) => {
|
|
1512
1512
|
const head = `joint "${j.joint}" (${j.limb}, "${j.part}" on "${j.bone}", range [${j.range.join(', ')}]${j.shape === 'disc' ? `, cap_shape "disc": no pixel over ${line.bar.level} in the disc` : ''})`;
|
|
1513
1513
|
if (j.unread !== undefined) return [`${head}: FAIL — not read: ${j.unread}`];
|
|
1514
|
-
|
|
1514
|
+
// Issue #557: the contour step, a reading on every row (check.json carries it only for a short cap).
|
|
1515
|
+
return j.poses.map((p) => {
|
|
1516
|
+
const st = p.step_px ?? steps.get(`${j.part} ${p.degrees}`);
|
|
1517
|
+
return `${head} at ${p.degrees}°: ${p.status} — mean |d| ${p.mean}/255, ${p.px_over_40} frame px over ${line.bar.level} (${p.px_over_80} over ${SEAM_PX_LEVEL_HIGH}), ${p.rig_px_over_40.toFixed(2)} rig px${st === undefined ? '' : `; contour step ${st.left} px left, ${st.right} px right (a reading)`}`;
|
|
1518
|
+
});
|
|
1515
1519
|
}),
|
|
1516
1520
|
];
|
|
1517
1521
|
}
|
|
@@ -1610,7 +1614,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1610
1614
|
const limbs = fig[LIMB_LINE];
|
|
1611
1615
|
if (limbs !== undefined) for (const [i, l] of limbCheckLines(limbs).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1612
1616
|
const sweep = fig[SWEEP_LINE];
|
|
1613
|
-
if (sweep !== undefined) for (const [i, l] of sweepCheckLines(sweep).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1617
|
+
if (sweep !== undefined) for (const [i, l] of sweepCheckLines(sweep, r.sweepSteps).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1614
1618
|
for (const [i, l] of motionSeamLines(r.motionSeam).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1615
1619
|
for (const [i, l] of clothLines(r.cloth).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1616
1620
|
for (const name of REPORTED_LINES) log(` ${judgementLine(name, fig[name])}`);
|
package/src/check.ts
CHANGED
|
@@ -2141,6 +2141,11 @@ export interface CheckReport {
|
|
|
2141
2141
|
motionSeam: MotionSeamLine;
|
|
2142
2142
|
/** `CLOTH_SETTLES` (issue #501): a warning, never a bar — in `check.json` only when the rig declares a physics constraint and has an idle; its SKIP is printed either way. */
|
|
2143
2143
|
cloth: ClothLine;
|
|
2144
|
+
/**
|
|
2145
|
+
* Issue #557: the contour step at each pose `SEAM_OVER_RANGE` read, by `<part> <degrees>` ({@link contourStep}) —
|
|
2146
|
+
* printed on every row; `check.json` carries it only for a joint whose cap is short ({@link SweepPose}).
|
|
2147
|
+
*/
|
|
2148
|
+
sweepSteps?: Map<string, { left: number; right: number }>;
|
|
2144
2149
|
}
|
|
2145
2150
|
|
|
2146
2151
|
/**
|
|
@@ -2980,6 +2985,10 @@ export interface SweepJoint {
|
|
|
2980
2985
|
radius: number;
|
|
2981
2986
|
/** Issue #360: the cap's record says `shape: "disc"` — the disc is held to no pixel uncovered ({@link sweepLine}). */
|
|
2982
2987
|
disc?: true;
|
|
2988
|
+
/** Issue #557: the upper segment's bone (the limb record one segment up), whose direction the contour step is read across; null when parts.json has none. */
|
|
2989
|
+
upperBone?: string | null;
|
|
2990
|
+
/** Issue #557: the cap's record carries `short_px` — the step is then written into check.json ({@link SweepPose}). */
|
|
2991
|
+
short?: { left: number; right: number };
|
|
2983
2992
|
}
|
|
2984
2993
|
|
|
2985
2994
|
/** Every limb record in `parts.json` that declares a `range`, in `parts.json`'s order. */
|
|
@@ -2988,7 +2997,20 @@ export function sweepJoints(parts: PartsFile): SweepJoint[] {
|
|
|
2988
2997
|
for (const p of parts.parts) {
|
|
2989
2998
|
const l = p.limb;
|
|
2990
2999
|
if (l === undefined || l.range === undefined || l.cap === undefined || l.joint === undefined) continue;
|
|
2991
|
-
|
|
3000
|
+
const up = parts.parts.find((q) => q.limb?.name === l.name && q.limb.segment === l.segment - 1)?.limb?.bone ?? null;
|
|
3001
|
+
out.push({
|
|
3002
|
+
part: p.name,
|
|
3003
|
+
limb: l.name,
|
|
3004
|
+
segment: l.segment,
|
|
3005
|
+
joint: l.joint,
|
|
3006
|
+
bone: l.bone,
|
|
3007
|
+
range: [l.range[0], l.range[1]],
|
|
3008
|
+
at: [l.cap.at[0], l.cap.at[1]],
|
|
3009
|
+
radius: l.cap.radius,
|
|
3010
|
+
...(l.cap.shape === 'disc' ? { disc: true as const } : {}),
|
|
3011
|
+
upperBone: up,
|
|
3012
|
+
...(l.cap.short_px === undefined ? {} : { short: { left: l.cap.short_px.left, right: l.cap.short_px.right } }),
|
|
3013
|
+
});
|
|
2992
3014
|
}
|
|
2993
3015
|
return out;
|
|
2994
3016
|
}
|
|
@@ -3052,6 +3074,90 @@ export function sweepUnread(j: SweepJoint, rig: Record<string, unknown>, limbSlo
|
|
|
3052
3074
|
return null;
|
|
3053
3075
|
}
|
|
3054
3076
|
|
|
3077
|
+
/** The setup pose's world transform of every bone `rig.json` declares (rigc's `computeExactFrameTransforms`, through `src/coords.ts`). */
|
|
3078
|
+
function setupWorld(rig: Record<string, unknown>): Map<string, BoneTransform> {
|
|
3079
|
+
const bones = (Array.isArray(rig.bones) ? rig.bones : []).filter(isRecord);
|
|
3080
|
+
const posable: Parameters<typeof computeExactFrameTransforms>[0][number][] = bones.map((b) => ({
|
|
3081
|
+
name: String(b.name),
|
|
3082
|
+
...(typeof b.parent === 'string' && b.parent !== '' ? { parent: b.parent } : {}),
|
|
3083
|
+
...Object.fromEntries((['x', 'y', 'rotation', 'scaleX', 'scaleY', 'shearX', 'shearY'] as const).filter((k) => typeof b[k] === 'number').map((k) => [k, b[k] as number])),
|
|
3084
|
+
...(typeof b.inherit === 'string' ? { inherit: b.inherit } : {}),
|
|
3085
|
+
}));
|
|
3086
|
+
return computeExactFrameTransforms(posable);
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
/**
|
|
3090
|
+
* Issue #557: the direction the contour step is read across — the upper
|
|
3091
|
+
* segment's, from its bone's origin to the joint's bone's origin at the setup
|
|
3092
|
+
* pose (the first of the two directions `src/limbs.ts` `jointLine` sums), as a
|
|
3093
|
+
* unit vector in rig px, y down: the Spine world's difference with its y
|
|
3094
|
+
* turned. The upper segment does not move while the joint bends, so lines
|
|
3095
|
+
* across it are the same lines at every pose. Null when either bone is not in
|
|
3096
|
+
* `rig.json` or the two origins coincide.
|
|
3097
|
+
*/
|
|
3098
|
+
export function sweepAxis(rig: Record<string, unknown>, j: Pick<SweepJoint, 'bone' | 'upperBone'>): [number, number] | null {
|
|
3099
|
+
if (j.upperBone === undefined || j.upperBone === null) return null;
|
|
3100
|
+
const world = setupWorld(rig);
|
|
3101
|
+
const up = world.get(j.upperBone);
|
|
3102
|
+
const me = world.get(j.bone);
|
|
3103
|
+
if (up === undefined || me === undefined) return null;
|
|
3104
|
+
const dx = me.worldX - up.worldX;
|
|
3105
|
+
const dy = up.worldY - me.worldY;
|
|
3106
|
+
const l = edgeLength(dx, dy);
|
|
3107
|
+
return l === 0 ? null : [dx / l, dy / l];
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
/** Issue #557: how far the contour step's lines run out from the axis, in cap radii — past the limb's edge on either side, which lies at one radius. */
|
|
3111
|
+
export const STEP_REACH_RADII = 2;
|
|
3112
|
+
|
|
3113
|
+
/**
|
|
3114
|
+
* Issue #557: the contour step at a bent pose — the step the eye sees in the
|
|
3115
|
+
* limb's outline where the cut meets it. On the frame `image` (rigc's render,
|
|
3116
|
+
* over `background`), lines across the limb perpendicular to `axis` (rig px,
|
|
3117
|
+
* unit, y down) at every frame pixel along it from one cap radius before the
|
|
3118
|
+
* joint line to one radius past it; on each, from the axis outward on each
|
|
3119
|
+
* side, the silhouette's extent: the distance, in rig px, of the first sample
|
|
3120
|
+
* whose largest channel lies within {@link SEAM_PX_LEVEL} of the background
|
|
3121
|
+
* (samples one frame pixel apart, out to {@link STEP_REACH_RADII} radii; a
|
|
3122
|
+
* sample is the frame pixel `map` carries the rig point onto, nearest). The
|
|
3123
|
+
* step on a side is the largest change in that extent between two adjacent
|
|
3124
|
+
* lines, to 3 places. `left` is the side the cap's `short_px` calls left
|
|
3125
|
+
* (across the axis at or above 0, `src/limbs.ts` `jointOffsets`), `right` the
|
|
3126
|
+
* other. A straight full limb reads about one frame pixel — the sampling's
|
|
3127
|
+
* own; a notch the bend opens in the outline reads its depth.
|
|
3128
|
+
*/
|
|
3129
|
+
export function contourStep(image: Raster, background: readonly number[], map: ScaleTranslate, at: readonly [number, number], axis: readonly [number, number], radius: number): { left: number; right: number } {
|
|
3130
|
+
const d = 1 / map.sx;
|
|
3131
|
+
const across: [number, number] = [-axis[1], axis[0]];
|
|
3132
|
+
const solid = (px: number, py: number): boolean => {
|
|
3133
|
+
const fx = Math.round(map.sx * (px - 0.5) + map.tx);
|
|
3134
|
+
const fy = Math.round(map.sy * (py - 0.5) + map.ty);
|
|
3135
|
+
if (fx < 0 || fy < 0 || fx >= image.width || fy >= image.height) return false;
|
|
3136
|
+
const o = (fy * image.width + fx) * 4;
|
|
3137
|
+
return Math.max(Math.abs(image.data[o] - background[0]), Math.abs(image.data[o + 1] - background[1]), Math.abs(image.data[o + 2] - background[2])) > SEAM_PX_LEVEL;
|
|
3138
|
+
};
|
|
3139
|
+
const along = Math.floor(radius * map.sx);
|
|
3140
|
+
const out = Math.ceil(STEP_REACH_RADII * radius * map.sx);
|
|
3141
|
+
const extent = (k: number, side: 1 | -1): number => {
|
|
3142
|
+
const cx = at[0] + k * d * axis[0];
|
|
3143
|
+
const cy = at[1] + k * d * axis[1];
|
|
3144
|
+
let m = 0;
|
|
3145
|
+
while (m <= out && solid(cx + side * m * d * across[0], cy + side * m * d * across[1])) m++;
|
|
3146
|
+
return m * d;
|
|
3147
|
+
};
|
|
3148
|
+
const step = (side: 1 | -1): number => {
|
|
3149
|
+
let best = 0;
|
|
3150
|
+
let prev = extent(-along, side);
|
|
3151
|
+
for (let k = -along + 1; k <= along; k++) {
|
|
3152
|
+
const e = extent(k, side);
|
|
3153
|
+
best = Math.max(best, Math.abs(e - prev));
|
|
3154
|
+
prev = e;
|
|
3155
|
+
}
|
|
3156
|
+
return round3(best);
|
|
3157
|
+
};
|
|
3158
|
+
return { left: step(1), right: step(-1) };
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3055
3161
|
/**
|
|
3056
3162
|
* The throwaway copy (see {@link SWEEP_LINE}): `rig` with one underlay slot per
|
|
3057
3163
|
* read joint — a region on the parent of the joint's bone, centred on its box
|
|
@@ -3076,13 +3182,7 @@ export function sweepCopy(
|
|
|
3076
3182
|
files: ReadonlySet<string> = new Set(),
|
|
3077
3183
|
): { rig: Record<string, unknown>; motion: Record<string, unknown>; slots: string[]; images: Array<[string, Raster]> } {
|
|
3078
3184
|
const bones = (Array.isArray(rig.bones) ? rig.bones : []).filter(isRecord);
|
|
3079
|
-
const
|
|
3080
|
-
name: String(b.name),
|
|
3081
|
-
...(typeof b.parent === 'string' && b.parent !== '' ? { parent: b.parent } : {}),
|
|
3082
|
-
...Object.fromEntries((['x', 'y', 'rotation', 'scaleX', 'scaleY', 'shearX', 'shearY'] as const).filter((k) => typeof b[k] === 'number').map((k) => [k, b[k] as number])),
|
|
3083
|
-
...(typeof b.inherit === 'string' ? { inherit: b.inherit } : {}),
|
|
3084
|
-
}));
|
|
3085
|
-
const world = computeExactFrameTransforms(posable);
|
|
3185
|
+
const world = setupWorld(rig);
|
|
3086
3186
|
const slots = (Array.isArray(rig.slots) ? rig.slots : []).filter(isRecord).map((s) => ({ ...s }));
|
|
3087
3187
|
const skins = isRecord(rig.skins) ? rig.skins : {};
|
|
3088
3188
|
const skin: Record<string, unknown> = { ...(isRecord(skins.default) ? skins.default : {}) };
|
|
@@ -3119,6 +3219,13 @@ export interface SweepPose {
|
|
|
3119
3219
|
px_over_40: number;
|
|
3120
3220
|
px_over_80: number;
|
|
3121
3221
|
rig_px_over_40: number;
|
|
3222
|
+
/**
|
|
3223
|
+
* Issue #557, written only for a joint whose cap record carries `short_px`:
|
|
3224
|
+
* the contour step at this pose ({@link contourStep}), rig px, per side of
|
|
3225
|
+
* the limb. A reading, never a bar; a full cap's row prints it and writes
|
|
3226
|
+
* what it wrote.
|
|
3227
|
+
*/
|
|
3228
|
+
step_px?: { left: number; right: number };
|
|
3122
3229
|
}
|
|
3123
3230
|
|
|
3124
3231
|
/** One joint's row: its limb record, its disc, how many of the joint's own art pixels the disc holds, and each pose — or, not read, why. */
|
|
@@ -3163,6 +3270,8 @@ export function sweepLine(
|
|
|
3163
3270
|
frames: (animation: string) => { under: FrameSet; bare: FrameSet },
|
|
3164
3271
|
physicsLeftOut: number,
|
|
3165
3272
|
problems: Problem[],
|
|
3273
|
+
step: (j: SweepJoint, bare: FrameSet) => { left: number; right: number } | null = () => null,
|
|
3274
|
+
printed: Map<string, { left: number; right: number }> = new Map(),
|
|
3166
3275
|
): SweepLine {
|
|
3167
3276
|
const seen: { grid: FrameSet | null } = { grid: null };
|
|
3168
3277
|
const rows: SweepRow[] = joints.map((j, i) => {
|
|
@@ -3212,7 +3321,10 @@ export function sweepLine(
|
|
|
3212
3321
|
`mean <= ${SEAM_MEAN_BAR.toFixed(1)} and <= ${SEAM_PX_BAR} rig px over ${SEAM_PX_LEVEL} are required, the rest-pose seam's bar — the bend uncovers the joint, and a cap that covers the turn (the limb's art within that radius on the upper side of the joint line, carried by "${j.part}") keeps it covered`,
|
|
3213
3322
|
});
|
|
3214
3323
|
}
|
|
3215
|
-
|
|
3324
|
+
// Issue #557: the contour step on the bent frame — a reading, recorded only where the cap is short.
|
|
3325
|
+
const st = step(j, bare);
|
|
3326
|
+
if (st !== null) printed.set(`${j.part} ${a}`, st);
|
|
3327
|
+
return { degrees: a, status: fail ? 'FAIL' : 'PASS', mean, px_over_40: f.over40, px_over_80: f.over80, rig_px_over_40: Number(seamRigPxText(f.over40, scale)), ...(st === null || j.short === undefined ? {} : { step_px: st }) };
|
|
3216
3328
|
});
|
|
3217
3329
|
return { ...head, status: poses.some((p) => p.status === 'FAIL') ? 'FAIL' : 'PASS', poses };
|
|
3218
3330
|
});
|
|
@@ -4068,6 +4180,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
4068
4180
|
// joints' own art under the limbs, physics left out, rendered twice by rigc on one grid — none without a range
|
|
4069
4181
|
let sweep: SweepLine | null = null;
|
|
4070
4182
|
const sweepProblems: Problem[] = [];
|
|
4183
|
+
const sweepSteps = new Map<string, { left: number; right: number }>();
|
|
4071
4184
|
const joints = inp.parts === null ? [] : sweepJoints(inp.parts);
|
|
4072
4185
|
if (joints.length > 0) {
|
|
4073
4186
|
const parts = inp.parts as PartsFile;
|
|
@@ -4126,7 +4239,13 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
4126
4239
|
}
|
|
4127
4240
|
});
|
|
4128
4241
|
}
|
|
4129
|
-
|
|
4242
|
+
// Issue #557: the contour step, read across the upper segment on each bent frame without the underlays.
|
|
4243
|
+
const axes = joints.map((j) => sweepAxis(base, j));
|
|
4244
|
+
const stepOf = (j: SweepJoint, bare: FrameSet): { left: number; right: number } | null => {
|
|
4245
|
+
const axis = axes[joints.indexOf(j)];
|
|
4246
|
+
return axis === null ? null : contourStep(bare.frames[0].image, bare.background, rigToFrameMap(inp.stage, parts.rig_size[1], bare.viewport), j.at, axis, j.radius);
|
|
4247
|
+
};
|
|
4248
|
+
sweep = sweepLine(joints, under.map((u) => u.px), unread, (a) => sets.get(a) as { under: FrameSet; bare: FrameSet }, stripped === null ? 0 : stripped.physics.length, sweepProblems, stepOf, sweepSteps);
|
|
4130
4249
|
} finally {
|
|
4131
4250
|
rmSync(sweepDir, { recursive: true, force: true });
|
|
4132
4251
|
}
|
|
@@ -4450,6 +4569,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
4450
4569
|
besideIdle: named,
|
|
4451
4570
|
motionSeam,
|
|
4452
4571
|
cloth,
|
|
4572
|
+
...(sweepSteps.size === 0 ? {} : { sweepSteps }),
|
|
4453
4573
|
};
|
|
4454
4574
|
}
|
|
4455
4575
|
|
package/src/clothconfig.ts
CHANGED
|
@@ -39,6 +39,8 @@ function canon(v: unknown): string {
|
|
|
39
39
|
/** One edit `--write-config` makes, with the words the write line prints for it. */
|
|
40
40
|
export interface ClothEdit extends ConfigEdit {
|
|
41
41
|
label: string;
|
|
42
|
+
/** Issue #571: the input's entry was the previous recipe's draft (`--from`), and only that made it the recipe's. */
|
|
43
|
+
previous?: true;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
/**
|
|
@@ -95,31 +97,44 @@ function shown(v: unknown): string {
|
|
|
95
97
|
* - removed (P present, D absent — the recipe replaced a plain chain with its trees): `I` equal to `P` is removed;
|
|
96
98
|
* - every other input entry is kept, bytes and order;
|
|
97
99
|
* - an owned or removed `I` that is none of those — changed by hand — is a conflict, as is a name the input states twice.
|
|
100
|
+
*
|
|
101
|
+
* With `V`, the previous recipe's draft (`previous`, issue #571 — `--from`; null without it): `I` equal to `V` is the
|
|
102
|
+
* recipe's as `I` equal to `P` is — an owned name is replaced, and a name in `V` and not in `D` is removed — and a
|
|
103
|
+
* conflict is an `I` equal to none of `D`, `P` and `V`, its first differing field read against `V` where `V` holds the
|
|
104
|
+
* name. Without `previous` every rule, and every word of the refusal, is the one above.
|
|
98
105
|
*/
|
|
99
|
-
function entryEdits(path: readonly string[], identity: (e: unknown) => string | null, drafted: readonly unknown[], plain: readonly unknown[], input: readonly unknown[], conflicts: Problem[]): ClothEdit[] {
|
|
106
|
+
function entryEdits(path: readonly string[], identity: (e: unknown) => string | null, drafted: readonly unknown[], plain: readonly unknown[], input: readonly unknown[], conflicts: Problem[], previous: readonly unknown[] | null): ClothEdit[] {
|
|
100
107
|
const section = path.join('.');
|
|
101
108
|
const D = byName(drafted, identity, 'drafted', section);
|
|
102
109
|
const P = byName(plain, identity, 'plain', section);
|
|
110
|
+
const V = previous === null ? null : byName(previous, identity, 'previous', section);
|
|
103
111
|
const keys = input.map(identity);
|
|
104
112
|
const where = new Map<string, number[]>();
|
|
105
113
|
keys.forEach((k, i) => {
|
|
106
114
|
if (k !== null) where.set(k, [...(where.get(k) ?? []), i]);
|
|
107
115
|
});
|
|
108
116
|
const owned = [...D.keys()].filter((k) => !P.has(k) || canon(P.get(k)) !== canon(D.get(k)));
|
|
109
|
-
const removed = [...P.keys()].filter((k) => !D.has(k));
|
|
117
|
+
const removed = [...P.keys(), ...(V === null ? [] : [...V.keys()].filter((k) => !P.has(k)))].filter((k) => !D.has(k));
|
|
110
118
|
const replace: string[] = [];
|
|
111
119
|
const remove: string[] = [];
|
|
120
|
+
const vouched = new Set<string>();
|
|
112
121
|
const conflict = (k: string, i: number, drafts: boolean): void => {
|
|
113
122
|
const author = input[i];
|
|
114
|
-
const
|
|
123
|
+
const prev = V !== null && V.has(k) ? V.get(k) : undefined;
|
|
124
|
+
const field = firstField(author, prev !== undefined ? prev : P.has(k) ? P.get(k) : D.get(k));
|
|
115
125
|
const fieldOf = (e: unknown, f: string): string => (isPlain(e) && e[f] !== undefined ? `${f} ${shown(e[f])}` : `no ${f}`);
|
|
116
126
|
const held = field === null ? `the entry ${shown(author)}` : fieldOf(author, field);
|
|
117
127
|
const draft = D.get(k);
|
|
118
128
|
const draftSays = !drafts ? 'the draft removes the entry (the recipe drafts that part afresh)' : field === null ? `the draft writes ${shown(draft)}` : `the draft writes ${fieldOf(draft, field)}`;
|
|
129
|
+
// Issue #571: with --from, the entry equals none of the three drafts; the field is read against the previous recipe's, the draft a person edited from.
|
|
130
|
+
const says =
|
|
131
|
+
V === null
|
|
132
|
+
? `it holds ${held} where ${draftSays}`
|
|
133
|
+
: `it equals neither the draft, the draft without the recipe, nor the previous recipe's draft: it holds ${held} where ${prev === undefined ? 'the previous recipe\'s draft has no such entry' : `the previous recipe's draft writes ${field === null ? shown(prev) : fieldOf(prev, field)}`} and ${draftSays}`;
|
|
119
134
|
conflicts.push({
|
|
120
135
|
code: 'PROPOSE_WRITE_CONFIG_CONFLICT',
|
|
121
136
|
object: `config.${section}[${i}] "${k}"`,
|
|
122
|
-
detail: `${section} entry "${k}" is the recipe's, and it was changed by hand:
|
|
137
|
+
detail: `${section} entry "${k}" is the recipe's, and it was changed by hand: ${says}, so the write would drop the hand edit; take the draft by removing the hand edit (or the entry), or leave the recipe out for that part (its config.cloth block)`,
|
|
123
138
|
});
|
|
124
139
|
};
|
|
125
140
|
for (const k of [...owned, ...removed]) {
|
|
@@ -134,19 +149,27 @@ function entryEdits(path: readonly string[], identity: (e: unknown) => string |
|
|
|
134
149
|
}
|
|
135
150
|
if (at.length === 0) continue;
|
|
136
151
|
const I = canon(input[at[0]]);
|
|
152
|
+
const isPrevious = V !== null && V.has(k) && I === canon(V.get(k));
|
|
137
153
|
if (D.has(k)) {
|
|
138
154
|
if (I === canon(D.get(k))) continue;
|
|
139
155
|
if (P.has(k) && I === canon(P.get(k))) replace.push(k);
|
|
140
|
-
else
|
|
141
|
-
|
|
142
|
-
|
|
156
|
+
else if (isPrevious) {
|
|
157
|
+
replace.push(k);
|
|
158
|
+
vouched.add(k);
|
|
159
|
+
} else conflict(k, at[0], true);
|
|
160
|
+
} else if (P.has(k) && I === canon(P.get(k))) remove.push(k);
|
|
161
|
+
else if (isPrevious) {
|
|
162
|
+
remove.push(k);
|
|
163
|
+
vouched.add(k);
|
|
164
|
+
} else conflict(k, at[0], false);
|
|
143
165
|
}
|
|
166
|
+
const mark = (k: string): { previous?: true } => (vouched.has(k) ? { previous: true } : {});
|
|
144
167
|
const edits: ClothEdit[] = [];
|
|
145
168
|
const work = [...keys];
|
|
146
|
-
for (const k of replace) edits.push({ path: [...path, work.indexOf(k)], value: D.get(k), label: `${section}["${k}"] replaced
|
|
169
|
+
for (const k of replace) edits.push({ path: [...path, work.indexOf(k)], value: D.get(k), label: `${section}["${k}"] replaced`, ...mark(k) });
|
|
147
170
|
for (const k of remove) {
|
|
148
171
|
const i = work.indexOf(k);
|
|
149
|
-
edits.push({ path: [...path, i], value: undefined, label: `${section}["${k}"] removed
|
|
172
|
+
edits.push({ path: [...path, i], value: undefined, label: `${section}["${k}"] removed`, ...mark(k) });
|
|
150
173
|
work.splice(i, 1);
|
|
151
174
|
}
|
|
152
175
|
const order = [...D.keys()];
|
|
@@ -174,27 +197,53 @@ function entryEdits(path: readonly string[], identity: (e: unknown) => string |
|
|
|
174
197
|
* the input holds it as an array, and any other section, or one the input lacks, whole. Every conflict is collected and
|
|
175
198
|
* refused in one throw, `PROPOSE_WRITE_CONFIG_CONFLICT`, before anything is written.
|
|
176
199
|
*
|
|
200
|
+
* `previous` (issue #571, `propose --write-config --from <config>`) is the same parts drafted a third time, with the
|
|
201
|
+
* recipe of the config the recipe is changed from: an array entry equal to its draft is the recipe's ({@link entryEdits}),
|
|
202
|
+
* and an array section the two drafts agree at is read entry by entry where the previous draft differs from them. An
|
|
203
|
+
* object entry (`meshes.<part>`, `regions.<part>`) the recipe drafts is replaced whole already, so `previous` changes
|
|
204
|
+
* nothing there. Null — no `--from` — is the writer before #571, rule for rule and byte for byte.
|
|
205
|
+
*
|
|
177
206
|
* `whole` is the selftest's mutant only (issue #552): `bones`, `constraints` and `motion.tracks` replaced whole, as
|
|
178
207
|
* the #537 writer did; no flag or environment reaches it.
|
|
179
208
|
*/
|
|
180
|
-
export function clothConfigEdits(drafted: Proposal, plain: Proposal, input: unknown, whole = false): ClothEdit[] {
|
|
209
|
+
export function clothConfigEdits(drafted: Proposal, plain: Proposal, input: unknown, whole = false, previous: Proposal | null = null): ClothEdit[] {
|
|
181
210
|
const edits: ClothEdit[] = [];
|
|
182
211
|
const conflicts: Problem[] = [];
|
|
183
212
|
const d = JSON.parse(JSON.stringify(drafted)) as Record<string, unknown>;
|
|
184
213
|
const p = JSON.parse(JSON.stringify(plain)) as Record<string, unknown>;
|
|
214
|
+
const v = previous === null || whole ? null : (JSON.parse(JSON.stringify(previous)) as Record<string, unknown>);
|
|
215
|
+
const arrayOf = (path: readonly string[]): (typeof ARRAY_SECTIONS)[number] | undefined => (whole ? undefined : ARRAY_SECTIONS.find((x) => x.path.join('.') === path.join('.')));
|
|
216
|
+
const prevAt = (path: readonly string[]): unknown[] | null => {
|
|
217
|
+
if (v === null) return null;
|
|
218
|
+
const c = valueAt(v, path);
|
|
219
|
+
return Array.isArray(c) ? c : [];
|
|
220
|
+
};
|
|
185
221
|
const one = (path: readonly string[], a: unknown, b: unknown): void => {
|
|
186
|
-
const s =
|
|
222
|
+
const s = arrayOf(path);
|
|
187
223
|
const mine = valueAt(input, path);
|
|
188
|
-
if (s !== undefined && Array.isArray(b) && (a === undefined || Array.isArray(a)) && Array.isArray(mine)) edits.push(...entryEdits(path, s.identity, b, Array.isArray(a) ? a : [], mine, conflicts));
|
|
224
|
+
if (s !== undefined && Array.isArray(b) && (a === undefined || Array.isArray(a)) && Array.isArray(mine)) edits.push(...entryEdits(path, s.identity, b, Array.isArray(a) ? a : [], mine, conflicts, prevAt(path)));
|
|
189
225
|
else edits.push({ path, value: b, label: path.join('.') });
|
|
190
226
|
};
|
|
227
|
+
// Issue #571: an array section the two drafts agree at, where the previous draft differs — its entries the previous recipe drafted and this one does not.
|
|
228
|
+
const prevOnly = (path: readonly string[], b: unknown): void => {
|
|
229
|
+
const s = arrayOf(path);
|
|
230
|
+
const mine = valueAt(input, path);
|
|
231
|
+
const c = valueAt(v, path);
|
|
232
|
+
if (v === null || s === undefined || !Array.isArray(b) || !Array.isArray(mine) || canon(c) === canon(b)) return;
|
|
233
|
+
edits.push(...entryEdits(path, s.identity, b, b, mine, conflicts, prevAt(path)));
|
|
234
|
+
};
|
|
191
235
|
for (const section of PROPOSAL_CONFIG_SECTIONS) {
|
|
192
236
|
const a = p[section];
|
|
193
237
|
const b = d[section];
|
|
194
|
-
if (JSON.stringify(a) === JSON.stringify(b))
|
|
238
|
+
if (JSON.stringify(a) === JSON.stringify(b)) {
|
|
239
|
+
if (isPlain(b)) for (const k of Object.keys(b)) prevOnly([section, k], b[k]);
|
|
240
|
+
else prevOnly([section], b);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
195
243
|
if (isPlain(a) && isPlain(b)) {
|
|
196
244
|
for (const k of [...Object.keys(b), ...Object.keys(a).filter((k) => !(k in b))]) {
|
|
197
245
|
if (JSON.stringify(a[k]) !== JSON.stringify(b[k])) one([section, k], a[k], b[k]);
|
|
246
|
+
else prevOnly([section, k], b[k]);
|
|
198
247
|
}
|
|
199
248
|
} else one([section], a, b);
|
|
200
249
|
}
|
|
@@ -221,6 +270,11 @@ export function writtenClothConfig(where: string, text: string, edits: readonly
|
|
|
221
270
|
return r.text;
|
|
222
271
|
}
|
|
223
272
|
|
|
273
|
+
/** Issue #571: how many edits the previous recipe's draft vouched for — an entry that equalled it, and only it. */
|
|
274
|
+
export function previousCount(edits: readonly ClothEdit[]): number {
|
|
275
|
+
return edits.filter((e) => e.previous === true).length;
|
|
276
|
+
}
|
|
277
|
+
|
|
224
278
|
/** What each edit names, in order: what the write line prints. */
|
|
225
279
|
export function editPaths(edits: readonly ClothEdit[]): string[] {
|
|
226
280
|
return edits.map((e) => e.label);
|
package/src/parts.ts
CHANGED
|
@@ -152,11 +152,21 @@ export interface CapRecord {
|
|
|
152
152
|
/** Issue #359: with `image_px`, the hidden-art layer the cap's image came from (`<run>:<layer>:hidden`), when it came from one rather than the config's cap file. */
|
|
153
153
|
image_layer?: string;
|
|
154
154
|
missing_px: number;
|
|
155
|
+
/**
|
|
156
|
+
* Issue #557, written only when `missing_px` is above 0: the missing pixels
|
|
157
|
+
* — the disc's on the upper side of the cut that no layer of the limb gives
|
|
158
|
+
* — by side of the limb's axis at the joint line, `left` where a pixel
|
|
159
|
+
* centre's offset across the axis (`jointOffsets` in `src/limbs.ts`) is 0 or
|
|
160
|
+
* above (the side toward smaller x when the limb hangs down the rig) and
|
|
161
|
+
* `right` the other; the two add up to `missing_px`. A full cap writes no key.
|
|
162
|
+
*/
|
|
163
|
+
short_px?: { left: number; right: number };
|
|
155
164
|
/**
|
|
156
165
|
* Issue #360, written only under the segment's `cap_shape: "disc"`: the
|
|
157
166
|
* shape; the cap's pixels taken from the upper segment's art where the
|
|
158
167
|
* lower's layer had none (`upper_px`). `radius` is then the half-width about the
|
|
159
|
-
* joint, at most half of `width`.
|
|
168
|
+
* joint, at most half of `width`. Issue #557: without the shape the half-disc
|
|
169
|
+
* takes them too, and `upper_px` is written there only when above 0.
|
|
160
170
|
*/
|
|
161
171
|
shape?: 'disc';
|
|
162
172
|
upper_px?: number;
|
|
@@ -197,6 +207,7 @@ const CAP_KEYS = [
|
|
|
197
207
|
'image_px',
|
|
198
208
|
'image_layer',
|
|
199
209
|
'missing_px',
|
|
210
|
+
'short_px',
|
|
200
211
|
'shape',
|
|
201
212
|
'upper_px',
|
|
202
213
|
'edge',
|
|
@@ -609,7 +620,9 @@ function checkParts(raw: unknown, path: string): PartsFile {
|
|
|
609
620
|
(!('image_px' in co) || isCount(co.image_px)) &&
|
|
610
621
|
(!('image_layer' in co) || ('image_px' in co && typeof co.image_layer === 'string' && co.image_layer !== '')) &&
|
|
611
622
|
// Issue #360: a disc cap carries its count, a feathered edge its band and count, and neither carries them alone.
|
|
612
|
-
('shape' in co ? co.shape === 'disc' && isCount(co.upper_px) : !('upper_px' in co)) &&
|
|
623
|
+
('shape' in co ? co.shape === 'disc' && isCount(co.upper_px) : !('upper_px' in co) || (isCount(co.upper_px) && co.upper_px > 0)) &&
|
|
624
|
+
// Issue #557: the shortfall by side, when written, adds up to the missing pixels, and is never written for none.
|
|
625
|
+
(!('short_px' in co) || shortSides(co.short_px, co.missing_px)) &&
|
|
613
626
|
('edge' in co ? co.edge === 'feather' && typeof co.feather_radius === 'number' && Number.isFinite(co.feather_radius) && co.feather_radius >= 0 && isCount(co.feathered_px) : !('feather_radius' in co) && !('feathered_px' in co)) &&
|
|
614
627
|
// Issue #360 (item 14): a placed cut carries its band, line and counts, the line inside the band; none of them rides alone.
|
|
615
628
|
('cut_band' in co
|
|
@@ -628,7 +641,7 @@ function checkParts(raw: unknown, path: string): PartsFile {
|
|
|
628
641
|
fail(
|
|
629
642
|
'PARTS_FIELD_TYPE',
|
|
630
643
|
`${object} "cap"`,
|
|
631
|
-
`is ${show(c)}; {at: [x, y], width, radius, box: [x, y, w, h], layer_px, image_px?, image_layer? (with image_px), missing_px, shape? ("disc", with upper_px), edge? ("feather", with feather_radius and feathered_px), cut_band? (with cut_offset inside the band, cut_at, cut_crossed at most joint_crossed, band_px, stroke_luma_max? 0..255)} is required`,
|
|
644
|
+
`is ${show(c)}; {at: [x, y], width, radius, box: [x, y, w, h], layer_px, image_px?, image_layer? (with image_px), missing_px, short_px? ({left, right}, adding up to missing_px, above 0), shape? ("disc", with upper_px; without it upper_px? above 0), edge? ("feather", with feather_radius and feathered_px), cut_band? (with cut_offset inside the band, cut_at, cut_crossed at most joint_crossed, band_px, stroke_luma_max? 0..255)} is required`,
|
|
632
645
|
);
|
|
633
646
|
}
|
|
634
647
|
}
|
|
@@ -659,6 +672,12 @@ function checkParts(raw: unknown, path: string): PartsFile {
|
|
|
659
672
|
}
|
|
660
673
|
|
|
661
674
|
const isCount = (v: unknown): v is number => Number.isInteger(v) && (v as number) >= 0;
|
|
675
|
+
/** Issue #557: a cap's `short_px` — `{left, right}`, two counts and no other key, adding up to `missing`, above 0. */
|
|
676
|
+
function shortSides(v: unknown, missing: unknown): boolean {
|
|
677
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v)) return false;
|
|
678
|
+
const o = v as Record<string, unknown>;
|
|
679
|
+
return Object.keys(o).sort().join(',') === 'left,right' && isCount(o.left) && isCount(o.right) && o.left + o.right === missing && o.left + o.right > 0;
|
|
680
|
+
}
|
|
662
681
|
|
|
663
682
|
/**
|
|
664
683
|
* The `single_run_pad` block (issue #366): every key known and present, each
|