rig-parts 1.83.2 → 1.85.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 +5 -2
- package/docs/AUTHORING.md +240 -0
- package/docs/STABILITY.md +32 -1
- package/package.json +1 -1
- package/src/artjoints.ts +94 -0
- package/src/assemble.ts +42 -3
- package/src/build.ts +19 -1
- package/src/check.ts +72 -6
- package/src/cutmaterial.ts +285 -0
- package/src/cutmaterialio.ts +63 -0
- package/src/requirements.ts +15 -2
- package/src/rig.ts +4 -3
- package/src/setuppainting.ts +126 -0
- package/src/turn.ts +4 -0
- package/src/turncoverage.ts +156 -0
- package/src/turnfield.ts +49 -0
package/cli.ts
CHANGED
|
@@ -723,6 +723,8 @@ usage:
|
|
|
723
723
|
rig-parts assemble --source <painting.png> --full <dir|psd> [--head <dir|psd>]
|
|
724
724
|
--config <config.json> --out <dir> [--seam near-white|silhouette]
|
|
725
725
|
[--project core|visible] [--alt <name>=<source>:<full>[:<head>]]
|
|
726
|
+
[--requirements <file.json>]
|
|
727
|
+
With cut_material declared, records original source edges bound to emitted parts for check.
|
|
726
728
|
Merge the full-body and head-crop See-through runs into rig-space parts:
|
|
727
729
|
<out>/rig/parts/<name>.png (each cropped to its alpha box), <out>/rig/parts.json,
|
|
728
730
|
<out>/render/recomposite_rig.png and <out>/render/${ERROR_MAP_FILE} (the error
|
|
@@ -1082,7 +1084,7 @@ const TAKES = {
|
|
|
1082
1084
|
compare: { valued: ['--left', '--right', '--map'], switches: [] },
|
|
1083
1085
|
check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements', '--set', '--environment', '--wind-band'], switches: ['--seam-pairs', '--keep-stills', '--all-sets', '--no-extremes'] },
|
|
1084
1086
|
loop: { valued: ['--frames', '--out'], switches: ['--palette'] },
|
|
1085
|
-
assemble: { valued: ['--source', '--full', '--head', '--config', '--out', '--seam', '--project', '--alt', '--write-config'], switches: ['--propose-plan'] },
|
|
1087
|
+
assemble: { valued: ['--source', '--full', '--head', '--config', '--out', '--seam', '--project', '--alt', '--write-config', '--requirements'], switches: ['--propose-plan'] },
|
|
1086
1088
|
'comfy seethrough': { valued: ['--image', '--out', '--resolution', '--steps', '--seed', '--prefix', '--host', '--wait', '--timeout', '--poll', '--config', '--depth-run'], switches: ['--offload', '--no-offload', '--lama', '--nf4'] },
|
|
1087
1089
|
'comfy paint': { valued: ['--config', '--out', '--seeds', '--seed0', '--host', '--wait', '--timeout', '--poll'], switches: [] },
|
|
1088
1090
|
inputs: { valued: ['--source', '--config', '--out'], switches: [] },
|
|
@@ -1707,6 +1709,7 @@ function cmdAssemble(args: string[]): number {
|
|
|
1707
1709
|
}
|
|
1708
1710
|
// --head is optional (issue #359): without it the full run is the one run, and it must hold the face.
|
|
1709
1711
|
for (const f of ['--source', '--full', '--config']) if (!flags.has(f)) return usage(`assemble needs ${f}`);
|
|
1712
|
+
if (propose && flags.has('--requirements')) return usage('--requirements belongs to assemble output, not --propose-plan');
|
|
1710
1713
|
if (propose && (flags.has('--out') || flags.has('--seam') || flags.has('--project') || alts.length > 0)) return usage('--propose-plan prints to the console; it takes none of --out, --seam, --project, --alt');
|
|
1711
1714
|
if (!propose && flags.has('--write-config')) return usage('--write-config belongs to assemble --propose-plan');
|
|
1712
1715
|
if (!propose && !flags.has('--out')) return usage('assemble needs --out <dir>');
|
|
@@ -1739,7 +1742,7 @@ function cmdAssemble(args: string[]): number {
|
|
|
1739
1742
|
}
|
|
1740
1743
|
const out = flags.get('--out') as string;
|
|
1741
1744
|
assembleStage(
|
|
1742
|
-
{ source, full, head, config, seam: seam as SeamRule, project: project as ProjectRule, ...(alts.length === 0 ? {} : { alts }) },
|
|
1745
|
+
{ source, full, head, config, ...(flags.has('--requirements') ? { requirements: flags.get('--requirements') as string } : {}), seam: seam as SeamRule, project: project as ProjectRule, ...(alts.length === 0 ? {} : { alts }) },
|
|
1743
1746
|
{ partsJson: join(out, 'rig', 'parts.json'), partsDir: join(out, 'rig', 'parts'), recomposite: join(out, 'render', 'recomposite_rig.png'), errorMap: join(out, 'render', ERROR_MAP_FILE) },
|
|
1744
1747
|
console.log,
|
|
1745
1748
|
);
|
package/docs/AUTHORING.md
CHANGED
|
@@ -4067,6 +4067,93 @@ consumer acceptance work; assembly provenance does not establish motion quality.
|
|
|
4067
4067
|
|
|
4068
4068
|
<a id="source-before-scissors-issue-819"></a>
|
|
4069
4069
|
|
|
4070
|
+
A hand polygon ending at a joint supplies no material for that joint's turn
|
|
4071
|
+
(issue #888). An `overlap` keeps pixels on the **parent**; it does not grow
|
|
4072
|
+
the cut piece. No source for material past the joint means stop and name the
|
|
4073
|
+
tool gap, following the source ladder below; do not hide a tear with crops or
|
|
4074
|
+
under-pieces. With an explicit `cut_material` declaration, `assemble` prints
|
|
4075
|
+
each original polygon edge's length through
|
|
4076
|
+
opaque source art (`alpha > 8`) and contiguous material past it on the piece.
|
|
4077
|
+
The same reading covers a limb's original structural joint line and the
|
|
4078
|
+
lower segment together with its generated hybrid band, when present.
|
|
4079
|
+
Assembly readings are SKIP-class observations; only check judges the declared bend.
|
|
4080
|
+
Without a declaration the API reading is SKIP and the CLI keeps its existing output.
|
|
4081
|
+
Limb source intersections use the final segmentation ownership map separately
|
|
4082
|
+
for each adjacent source; another limb's island is excluded before capture.
|
|
4083
|
+
|
|
4084
|
+
To enforce a bend, add `cut_material` to the existing
|
|
4085
|
+
`spine-parts-requirements/1` file (the other fields remain required):
|
|
4086
|
+
|
|
4087
|
+
```json
|
|
4088
|
+
{
|
|
4089
|
+
"spec": "spine-parts-requirements/1", "fps": 12, "requirements": [],
|
|
4090
|
+
"cut_material": [{
|
|
4091
|
+
"part": "hand", "joint": "wrist", "at": [8, 12], "axis": [0, 1],
|
|
4092
|
+
"reach_px": 3, "bend_degrees": [-30, 30]
|
|
4093
|
+
}]
|
|
4094
|
+
}
|
|
4095
|
+
```
|
|
4096
|
+
|
|
4097
|
+
These are hand-computable fixture coordinates, not recommended rig values.
|
|
4098
|
+
`at` is an independently supplied joint centre in stage pixels (y down);
|
|
4099
|
+
`axis` identifies an original opaque cut inward within reach, checked against
|
|
4100
|
+
the source evidence. Every polygon edge retains its own canonical inward
|
|
4101
|
+
normal; a declaration matching none of the eligible edges is refused.
|
|
4102
|
+
Correspondence accepts exact perpendicularity to the original edge direction
|
|
4103
|
+
(zero dot product), or exact collinearity with its stored canonical inward
|
|
4104
|
+
(zero cross product); the inward cross-product sign must also agree.
|
|
4105
|
+
Positive scalar multiples such as `[1,1]` and `[2,2]` are equivalent; rounded
|
|
4106
|
+
unit components are never compared. Orientation arithmetic must be finite and
|
|
4107
|
+
the direction magnitude representable, otherwise the declaration is refused.
|
|
4108
|
+
`reach_px` is the author's explicit joint reach, and `bend_degrees` is the
|
|
4109
|
+
continuous interval in stage coordinates, positive clockwise, inside
|
|
4110
|
+
`[-180, 180]`. Nothing is inferred from a name or an animation. A limb's joint
|
|
4111
|
+
name and centre must match its recorded lower segment; segment0 and a
|
|
4112
|
+
standalone generated band have no lower structural cut to read.
|
|
4113
|
+
|
|
4114
|
+
Run `assemble --requirements <file>` (or `build --requirements <file>`)
|
|
4115
|
+
first, then `check --requirements <file>` against that assembly. Only an
|
|
4116
|
+
explicit `cut_material` declaration writes `cut_material_sources.json`
|
|
4117
|
+
beside `parts.json`: original edge endpoints, inward direction, opaque
|
|
4118
|
+
source intervals split at pixel-cell boundaries (ordered, nonoverlapping
|
|
4119
|
+
`opaque_t` spans inside `[0,1]` on their actual edge), source labels and
|
|
4120
|
+
transformed RGBA digests including placement and size, bound to the emitted `parts.json` and every base PNG by SHA-256.
|
|
4121
|
+
`check` also verifies the complete source payload digest, the interval census
|
|
4122
|
+
and canonical perpendiculars. Listed source edges must contain positive
|
|
4123
|
+
opaque intervals; an empty array cannot hide an edge beside another one.
|
|
4124
|
+
`check` never reconstructs discarded source pixels or polygons from siblings.
|
|
4125
|
+
Missing or stale evidence is unavailable, not zero support. Assemble again to
|
|
4126
|
+
refresh it. Alternate-set source evidence is not interchangeable with the base
|
|
4127
|
+
assembly; this instrument currently reads the base parts only.
|
|
4128
|
+
|
|
4129
|
+
The gate clips every original opaque edge interval to the declared reach.
|
|
4130
|
+
Let its tangent be `(-ny, nx)` for inward normal `n`. A point relative to the
|
|
4131
|
+
joint has **both** coordinates `(t, d)`, where `d` is the actual cut plane's
|
|
4132
|
+
normal offset. Inverse rotation gives `(t*cos(theta) - d*sin(theta),
|
|
4133
|
+
t*sin(theta) + d*cos(theta))`. Required outward material, measured from the
|
|
4134
|
+
actual cut plane, is `max(0, d*(1-cos(theta)) - t*sin(theta))`; the normal offset
|
|
4135
|
+
is never dropped or projected onto the joint plane.
|
|
4136
|
+
|
|
4137
|
+
The entire actual line segment is tested against every relevant transparent
|
|
4138
|
+
pixel cell's outward normal shadow. Segment/polygon incidence can change only
|
|
4139
|
+
when a rotating endpoint crosses an edge, the rotating source line crosses a
|
|
4140
|
+
vertex, or edges become parallel. All critical angles are solved analytically;
|
|
4141
|
+
interval interiors and boundary angles cover the whole declared continuous
|
|
4142
|
+
range, including a fixed angle. A strict intersection supplies a witness and
|
|
4143
|
+
its contiguous depth, ending at the first transparent cell. Midpoint rays are
|
|
4144
|
+
additional diagnostics, never the coverage proof. No angular sampling rate or
|
|
4145
|
+
calibrated tolerance is used. Controls plant a between-ray hole and translated
|
|
4146
|
+
and oblique cut edges whose inverse rotations are computed by hand.
|
|
4147
|
+
|
|
4148
|
+
`CHECK_CUT_JOINT_MATERIAL` names part, joint, original edge length, length
|
|
4149
|
+
within reach and found/required depth on the same deficient ray. The interval
|
|
4150
|
+
and pixel-cell geometry are measured; subpixel texture filtering, another
|
|
4151
|
+
motion, material outside the declared reach and consumer compositing remain
|
|
4152
|
+
outside this gate. An absent source crossing or unrepresentable arithmetic is
|
|
4153
|
+
`CHECK_CUT_MATERIAL_MEASURED`, never PASS. Without a declaration the API reading
|
|
4154
|
+
is SKIP; the CLI preserves its existing stdout and adds no sidecar, `parts.json` field,
|
|
4155
|
+
`check.json` field or counted bar to an undeclared build.
|
|
4156
|
+
|
|
4070
4157
|
**Scissors are for structure, never for repair.** `cuts`, `patches` and
|
|
4071
4158
|
`author:<label>` layers separate something that must move independently and
|
|
4072
4159
|
that See-through has no layer for: a joint, an accessory, or a rear piece that
|
|
@@ -4934,6 +5021,9 @@ documents do not ship). Each is an input this package built; none is an author's
|
|
|
4934
5021
|
| <a id="CHECK_INPUT_PRESENT"></a><a id="CHECK_INPUT_IS_JSON"></a><a id="CHECK_PART_PNG_PRESENT"></a><a id="CHECK_PART_PNG_MATCHES_BOX"></a><a id="CHECK_PART_SLOT_PRESENT"></a><a id="CHECK_RIG_STAGE_PRESENT"></a><a id="CHECK_RIG_STAGE_IS_THE_CANVAS"></a><a id="CHECK_RIG_ROOT_BONE"></a><a id="CHECK_IDLE_PRESENT"></a>`CHECK_INPUT_PRESENT`, `CHECK_INPUT_IS_JSON`, `CHECK_PART_PNG_PRESENT`, `CHECK_PART_PNG_MATCHES_BOX`, `CHECK_PART_SLOT_PRESENT`, `CHECK_RIG_STAGE_PRESENT`, `CHECK_RIG_STAGE_IS_THE_CANVAS`, `CHECK_RIG_ROOT_BONE`, `CHECK_IDLE_PRESENT` | the rig directory is incomplete or disagrees with `parts.json` (`CHECK_PART_SLOT_PRESENT`: a part with no slot of its own name, which the judgement lines render it by; `CHECK_INPUT_PRESENT` on `--set` / `--all-sets`: no `parts.json`, no `alternates`, or the named set is not in `alternates`). `motion.json` is required even with no animation in it, because `rigc build` takes `--motion` (a spec with `"animations": {}` builds green); `CHECK_IDLE_PRESENT` is an `idle` that is there with no positive `duration` — an absent `idle` is measured around (§7). `CHECK_INPUT_PRESENT` on `…/parts.json`: `--parts` named a directory without `parts.json` — most often `parts/` itself — or, without `--parts`, the rig directory has none while the directory above it does (build's layout, `<out>/rig` under `<out>/parts.json`). The detail says it: "--parts names the directory holding parts.json and parts/, not parts/ itself (after build, that is build's --out, whose rig is <out>/rig); without --parts it is --rig", and names the parent when the parent holds `parts.json`. With neither (no `--parts`, and no `parts.json` beside `rig.json` or above it), the rig is measured without parts and nothing is refused (§7) | re-run rig (`build` does both); for `…/parts.json`, `--parts` — the directory above `parts/` |
|
|
4935
5022
|
| <a id="CHECK_SOURCE_SIZE"></a>`CHECK_SOURCE_SIZE` | the painting `--source` names is not this rig's: no single `rig_scale` takes its width and height to the stage's (assemble makes the canvas `trunc(w × rig_scale)` x `trunc(h × rig_scale)`), or, with `parts.json` read, its `scale_rig_per_source` does not; or the stage is not whole pixels. Refused before anything is built | the painting the rig was made from — the stage's own size, or the painting `assemble` was given |
|
|
4936
5023
|
| <a id="CHECK_SOURCE_PNG"></a>`CHECK_SOURCE_PNG` | `--source` names a file that does not read as a PNG | a PNG |
|
|
5024
|
+
| <a id="CHECK_SETUP_MATCHES_PAINTING"></a>`CHECK_SETUP_MATCHES_PAINTING` | region, slot, failing-pixel box, slot pixelcount, complete region pixelcount and declared limit | compare the source painting beside the actual setup; follow [Source before scissors](#source-before-scissors), then rerun with the same authored limit |
|
|
5025
|
+
| <a id="CHECK_SETUP_PAINTING_SOURCE"></a>`CHECK_SETUP_PAINTING_SOURCE` | a declaration without a source painting | supply `--source`; the assembled composite is not an independent reference |
|
|
5026
|
+
| <a id="CHECK_SETUP_PAINTING_MEASURED"></a>`CHECK_SETUP_PAINTING_MEASURED` | missing native setup/ownership or unsupported clipping | supply observable textured setup geometry; an unavailable instrument never passes |
|
|
4937
5027
|
| <a id="CHECK_SOURCE_GRID"></a>`CHECK_SOURCE_GRID` | the setup-pose still and its black-tinted twin (the coverage `SETUP_POSE_VS_SOURCE` reads) did not come back on one grid over one opaque background | a rigc problem; report it |
|
|
4938
5028
|
| <a id="CHECK_RIGC_VERSION"></a>`CHECK_RIGC_VERSION` | `rigc --version` is below 1.4.0, or prints no version: its `render` has no `--geometry`, which `TEXTURE_STRETCH` reads. Refused before anything is built | `bun install` (this package depends on rig-c ^2.20.4), or put a newer `rigc` first on `PATH` |
|
|
4939
5029
|
| <a id="CHECK_RIGC_ENTRY"></a><a id="CHECK_RIGC_ENTRY_READS"></a>`CHECK_RIGC_ENTRY`, `CHECK_RIGC_ENTRY_READS` | `rigc --version` names no entry (a rigc below 2.0.0), or names one in neither launcher form (`entry: cli.ts — @esotericsoftware/spine-core <v> present`, `entry: cli_core.ts — @esotericsoftware/spine-core absent — …`); `check.json`'s `rigc_entry` records the one that gated the build | `bun install` |
|
|
@@ -4987,6 +5077,8 @@ documents do not ship). Each is an input this package built; none is an author's
|
|
|
4987
5077
|
| <a id="REQUIREMENTS_TARGET"></a>`REQUIREMENTS_TARGET` | a scene target places one bone twice for one animation, places it for an animation no requirement measures, has a key outside the animation's `[0, duration]`, or sits in a group the animation translates | one placement per bone and animation, inside it, for an animation a requirement reads |
|
|
4988
5078
|
| <a id="REQUIREMENTS_SELF_TARGET"></a>`REQUIREMENTS_SELF_TARGET` | a `contact` whose `target` is its own `bone` and `point` (0 apart by construction), or an `aim` whose `target` is a point of the aimed bone itself — its tip lies on its own axis (0 degrees by construction), its origin gives no line at all (issue #540): a PASS would say nothing. Refused before anything is built, beside the file's other refusals; a bone's tip against its own origin is a contact like any other (it reads the bone's length), and a stage target is never one | a different bone (or a stage point, for an aim), or for a contact the bone's other point |
|
|
4989
5079
|
| <a id="CHECK_REQUIREMENTS_FRAMES"></a>`CHECK_REQUIREMENTS_FRAMES` | a requirement render did not write every frame rigc sampled, or the as-declared, released and full renders of a follow sampled different frames | a rigc problem; report it |
|
|
5080
|
+
| <a id="CHECK_CUT_JOINT_MATERIAL"></a>`CHECK_CUT_JOINT_MATERIAL` | A declared `cut_material` source ray within reach needs more depth during its continuous bend interval than its final piece supplies; the message names part, joint, original opaque edge length, found and required material | follow [Source before scissors](#source-before-scissors); obtain source material past the joint, or explicitly declare the consumer's smaller required bend; no crop or under-piece repair |
|
|
5081
|
+
| <a id="CHECK_CUT_MATERIAL_MEASURED"></a>`CHECK_CUT_MATERIAL_MEASURED` | Original source evidence is missing, malformed or stale, a part/joint correspondence is unresolved, the inward axis disagrees, or no opaque crossing lies within reach | rerun `assemble --requirements <file>` or `build --requirements <file>` to bind original source readings to the actual emitted parts; correct an independently declared correspondence/reach; never infer original art from emitted siblings |
|
|
4990
5082
|
| <a id="CHECK_REQUIREMENT_MET"></a>`CHECK_REQUIREMENT_MET` | a declared requirement measured outside its bar; the figure, its frame and the bar are quoted | the rig or its motion (§7, *Declared requirements*) — or the bar, if the scene asks less |
|
|
4991
5083
|
| <a id="CHECK_REQUIREMENT_MEASURABLE"></a>`CHECK_REQUIREMENT_MEASURABLE` | a declared requirement is NOT MEASURABLE: what it asks is undefined in the frames, and the reason is quoted. Not a pass | the declaration (a bone with a length, a target off the bone's origin, a least drive the constraint reaches) or the rig |
|
|
4992
5084
|
| <a id="REQUIREMENTS_MAY_FOLD"></a>`REQUIREMENTS_MAY_FOLD` | a `may_fold` entry names a slot with no mesh attachment — a region is one quad and cannot fold (issue #320). Refused before anything is built. Before issue #333 an entry naming the idle was refused here too; the bar reads the idle now, and `idle` is an animation name like any other | a mesh slot |
|
|
@@ -5118,6 +5210,7 @@ and stay absent).
|
|
|
5118
5210
|
| <a id="RIG_TURN_SURFACE_OVERLAP_DEPTH"></a>`RIG_TURN_SURFACE_OVERLAP_DEPTH` | same-group overlap neighbours demand incompatible emitted offsets at one target setup point | separate the conflicting support or provide coincident neighbour fields; the refusal names the fields and target vertex |
|
|
5119
5211
|
| <a id="RIG_TURN_SURFACE_MESH_BINDINGS"></a>`RIG_TURN_SURFACE_MESH_BINDINGS` | an authored surface mesh has unresolved/nonclosing weights or shares a frame moved by a unit/rear turn carrier; declared stop draw aliases resolve to their links for setup sampling | provide weighted bindings closing at 1 in the authored frame; `<link>_d` resolves only for links declared in `config.stops`; use independent chains for parts with different roles |
|
|
5120
5212
|
| <a id="RIG_TURN_SURFACE_MESH_ATTACHMENT"></a>`RIG_TURN_SURFACE_MESH_ATTACHMENT` | a surface mesh has an alternate drawing without its own registered depth sampling frame | use the base attachment; explicit islands retain their own slot, attachment and bindings |
|
|
5213
|
+
| <a id="RIG_TURN_BODY_UNCOVERED"></a>`RIG_TURN_BODY_UNCOVERED` | **Warning:** a shown part without a body role overlaps a moved `turn.body` surface at setup alpha >= 128 and has different emitted endpoint displacement | declare the intended body role and inspect both endpoints; no role or depth is inferred |
|
|
5121
5214
|
| <a id="RIG_TURN_SURFACE_DEPTH_REQUIRED"></a>`RIG_TURN_SURFACE_DEPTH_REQUIRED` | a part with role `"surface"` in `config.turn.roles` or `config.turn.body.roles` carries no depth sheet in `parts.json` | run `assemble` with `config.depth` or provide `<part>_depth.png` in `parts.json` |
|
|
5122
5215
|
| <a id="RIG_TURN_GRID_BUDGET"></a>`RIG_TURN_GRID_BUDGET` | a `"surface"` grid exceeds 2,400 triangles on one slot or 4,000 triangles across all `"surface"` slots | lower `config.turn.grid` / `config.turn.body.grid` `[cols, rows]` within the per-slot and total budgets |
|
|
5123
5216
|
| <a id="RIG_TURN_HEAD_BONE"></a>`RIG_TURN_HEAD_BONE` | `buildRig` found no `"head"` (or body turn root) bone when emitting 2.5D turn carriers | declare `"head"` (or the body turn root bone) in `config.bones` |
|
|
@@ -5548,6 +5641,7 @@ Under `--source <painting.png>`, a second reported line, also never a FAIL
|
|
|
5548
5641
|
| line | reports | bar | `reference-demo` | `reference-sample` | SKIP when |
|
|
5549
5642
|
| --- | --- | --- | --- | --- | --- |
|
|
5550
5643
|
| <a id="SETUP_POSE_VS_SOURCE"></a>`SETUP_POSE_VS_SOURCE` | the setup pose against the painting, by `assemble`'s own `measureRecomposite` over the stage's pixels — `mean_abs` and `within_share` (mean channel, within 8), `error_px` (max channel over 40), `uncovered_error_px` (of those, where the pose has alpha 128 or less), `hole_count` and the largest hole's box — with `painting_px`, `stage_px`, `render_px` and `render_scale` beside them. The painting is resampled onto the stage as assemble resamples it (lanczos3, alpha dropped). rigc renders onto an opaque grey, so the pose over white and its alpha are read off a second still of the same pose with every slot tinted black; both are carried onto the stage with the seam's map turned round (bilinear) | none — status `REPORTED`; assemble applies no bar to the figures it measures, so none is derived here | 3.146 / 93.43 % / 12,399 / 1,732, 518 holes, the largest 61 px at 351,157 6x20 (assemble's recomposite: 2.391 / 95.81 % / 11,050 / 1,564, 453) | 1.095 / 97.30 % / 5,348 / 1,239, 313 holes, the largest 88 px at 479,416 6x29 (assemble's: 0.835 / 97.95 % / 4,512 / 1,185, 259) | not written without `--source` |
|
|
5644
|
+
| <a id="SETUP_MATCHES_PAINTING"></a>`SETUP_MATCHES_PAINTING` | declared region count, native box, limit and actual top contributor rows | PASS/FAIL against authored limits; no limit is SKIP; undeclared regions are not judged |
|
|
5551
5645
|
|
|
5552
5646
|
The figures are assemble's by construction, not a second definition beside them;
|
|
5553
5647
|
what differs is what they are taken of — the pose rigc draws, not the flat stack —
|
|
@@ -6020,6 +6114,132 @@ on a bone of length 0 are NOT MEASURABLE and the run is not PASS (`CK68`–`CK79
|
|
|
6020
6114
|
frame the slide peaks; a seam on a rig with no `parts.json` is refused before anything
|
|
6021
6115
|
is built (`CK83`–`CK85`, `RQ30`–`RQ35`).
|
|
6022
6116
|
|
|
6117
|
+
### Art joints on a fixed skeleton
|
|
6118
|
+
|
|
6119
|
+
When art is fitted to a skeleton whose setup must stay fixed, declare
|
|
6120
|
+
`fixed_skeleton` in the existing `--requirements` file (issue #880):
|
|
6121
|
+
|
|
6122
|
+
```json
|
|
6123
|
+
{
|
|
6124
|
+
"spec": "spine-parts-requirements/1",
|
|
6125
|
+
"fps": 12,
|
|
6126
|
+
"requirements": [],
|
|
6127
|
+
"fixed_skeleton": {
|
|
6128
|
+
"source": "independent joint measurements on the fitted art",
|
|
6129
|
+
"within_px": 4,
|
|
6130
|
+
"joints": [
|
|
6131
|
+
{ "name": "shoulder", "bone": "upper_arm", "at": [26, 57] }
|
|
6132
|
+
]
|
|
6133
|
+
}
|
|
6134
|
+
}
|
|
6135
|
+
```
|
|
6136
|
+
|
|
6137
|
+
The block explicitly identifies this as a fixed-skeleton comparison; a generation
|
|
6138
|
+
control pose does not. `source` is required nonempty provenance text, recorded
|
|
6139
|
+
without interpretation. Each `at` is an independently measured point on the art
|
|
6140
|
+
**as placed on the rig's stage**, in stage pixels from its top-left, x right and y
|
|
6141
|
+
down, the same space as a requirement's `target.stage`. Convert painting/source
|
|
6142
|
+
coordinates through the actual placement before declaring them: raw source pixels,
|
|
6143
|
+
bone-local offsets and normalized coordinates are not stage pixels. Each nonempty
|
|
6144
|
+
`name` is unique and each `bone` explicitly names its matching bone origin in
|
|
6145
|
+
`rig.json`. No anatomical names, hidden joints, correspondences or positions are
|
|
6146
|
+
inferred; only the listed measurements are covered. An unmeasured joint must not
|
|
6147
|
+
be supplied as the bone's own position.
|
|
6148
|
+
|
|
6149
|
+
`check` uses the existing setup-still renderer, requesting `--geometry`, and reads
|
|
6150
|
+
`geometry.json`'s `setup.bones` through its existing geometry reader. It converts
|
|
6151
|
+
the renderer's setup-world origin to stage pixels through the coordinate door;
|
|
6152
|
+
it never substitutes an animation's first frame or a bone's parent-local `x,y`.
|
|
6153
|
+
This also works for an external `rig.json` with no `parts.json` or idle. The
|
|
6154
|
+
Euclidean distance, art point and setup origin are printed **for every declared
|
|
6155
|
+
joint** and written to `check.json`'s `ART_JOINT_DISTANCE`, along with `source`,
|
|
6156
|
+
`instrument` and `within_px`. This is a setup-placement check, not a motion check
|
|
6157
|
+
or automatic detection of joints in an image.
|
|
6158
|
+
|
|
6159
|
+
`within_px` is optional and, when present, must be finite and nonnegative. The
|
|
6160
|
+
example's `4` is an authored demonstration, not a calibrated or default tolerance.
|
|
6161
|
+
Distance **greater than** the declared limit fails; equality passes. Without a
|
|
6162
|
+
limit, the block and every joint are **SKIP**, with the measured distances and
|
|
6163
|
+
`within_px: null`; this line is excluded from measured bars and contributes no
|
|
6164
|
+
PASS. Other independent checks may still pass. With no `fixed_skeleton` block,
|
|
6165
|
+
this measurement adds no output or render call. `fps` and `requirements` remain
|
|
6166
|
+
required by the enclosing file; they do not change this setup measurement.
|
|
6167
|
+
|
|
6168
|
+
| rule | change |
|
|
6169
|
+
| --- | --- |
|
|
6170
|
+
| <a id="CHECK_ART_JOINT_DISTANCE"></a>`CHECK_ART_JOINT_DISTANCE` | The named joint is farther from its setup bone origin than `fixed_skeleton.within_px`; the message gives distance and limit. Correct a wrong art measurement or bone correspondence, or supply art aligned to the fixed skeleton. Choose any changed limit explicitly from the consumer's requirement; bones and art are never moved by this check. |
|
|
6171
|
+
| <a id="CHECK_ART_JOINT_MEASURED"></a>`CHECK_ART_JOINT_MEASURED` | Supply a finite renderer setup origin and a finite measurable distance; missing geometry is never taken as zero. The existing `CHECK_GEOMETRY_FILE` refusal names missing/malformed geometry. |
|
|
6172
|
+
|
|
6173
|
+
`REQUIREMENTS_FIELD` and `REQUIREMENTS_RESOLVES` cover malformed declarations:
|
|
6174
|
+
supply nonempty provenance and a nonempty joint list, unique joint names, finite
|
|
6175
|
+
`[x, y]` points, known bone names and an explicit valid limit or omit the limit.
|
|
6176
|
+
Unknown fields are refused.
|
|
6177
|
+
|
|
6178
|
+
| line | measures | bar | SKIP |
|
|
6179
|
+
| --- | --- | --- | --- |
|
|
6180
|
+
| <a id="CUT_JOINT_MATERIAL"></a>`CUT_JOINT_MATERIAL` | Original source edge intervals versus final opaque pixel-cell material over each explicit `cut_material` joint reach and continuous bend interval; full two-coordinate swept-segment coverage plus contiguous normal support | every required source interval has material through its inverse-rotation sweep; a hole between diagnostic rays also fails | no declaration: API SKIP, unchanged CLI stdout, no JSON key or counted bar; missing/stale source evidence or no crossing within reach: named unavailable refusal |
|
|
6181
|
+
| <a id="ART_JOINT_DISTANCE"></a>`ART_JOINT_DISTANCE` | Each independently measured art joint versus its explicitly named bone's renderer setup origin, in stage pixels; printed and recorded per joint | distance <= the declared `fixed_skeleton.within_px`, with no rounding slack | no limit declared: every row and the block retain their distances as SKIP, never a pass; no declaration: no line |
|
|
6182
|
+
|
|
6183
|
+
### Setup against the painting
|
|
6184
|
+
|
|
6185
|
+
`check --source <painting> --requirements <file>` can declare
|
|
6186
|
+
`setup_painting` in the existing `spine-parts-requirements/1` file:
|
|
6187
|
+
|
|
6188
|
+
```json
|
|
6189
|
+
{
|
|
6190
|
+
"spec": "spine-parts-requirements/1",
|
|
6191
|
+
"fps": 12,
|
|
6192
|
+
"requirements": [],
|
|
6193
|
+
"setup_painting": {
|
|
6194
|
+
"rgb_tolerance": 60,
|
|
6195
|
+
"regions": [
|
|
6196
|
+
{ "name": "torso", "box": [10, 20, 40, 60], "max_pixels": 0 }
|
|
6197
|
+
]
|
|
6198
|
+
}
|
|
6199
|
+
}
|
|
6200
|
+
```
|
|
6201
|
+
|
|
6202
|
+
These are authored limits, not defaults or calibrated figures. A region's
|
|
6203
|
+
`box` is `[x, y, width, height]` in the declared stage's native pixel grid,
|
|
6204
|
+
x right and y down, with exclusive far edges. Regions must fit the stage.
|
|
6205
|
+
`rgb_tolerance` is an integer from 0 through 255. Count a pixel once when its
|
|
6206
|
+
maximum absolute RGB-channel difference is **strictly greater** than that
|
|
6207
|
+
value. The region passes when its total count is at most its authored
|
|
6208
|
+
nonnegative integer `max_pixels`. Without that limit the region reports
|
|
6209
|
+
**SKIP**, including a zero-count region. An undeclared region has no verdict.
|
|
6210
|
+
Without `setup_painting`, existing output bytes and rendering remain unchanged.
|
|
6211
|
+
|
|
6212
|
+
The observation is the actual renderer's setup pose, without animation or
|
|
6213
|
+
physics advancement, from the already gated setup candidate. Its viewport is
|
|
6214
|
+
the declared stage, scale 1; no fitted alignment or flat part composite is
|
|
6215
|
+
substituted. Source PNG readers preserve straight RGBA. This optional check
|
|
6216
|
+
first composites the painting over opaque white at its **original source
|
|
6217
|
+
resolution**, rounding each RGB channel to RGBA8, then passes that opaque
|
|
6218
|
+
image through the existing `sourceInRig` Lanczos3 resampling onto the stage.
|
|
6219
|
+
The rendered setup is composited over white on the native stage grid. This
|
|
6220
|
+
ordering excludes hidden RGB from both resampling taps and figure detection;
|
|
6221
|
+
the global `sourceInRig` and legacy checks retain their existing RGB semantics.
|
|
6222
|
+
The figure domain is the union of rendered
|
|
6223
|
+
alpha above zero and the painting's existing `figureSilhouette`: border-median
|
|
6224
|
+
background difference, morphological opening and hole filling. This includes
|
|
6225
|
+
missing output over source figure and extra rendered support. It is not an
|
|
6226
|
+
independent segmentation oracle for arbitrary backgrounds.
|
|
6227
|
+
|
|
6228
|
+
`SETUP_MATCHES_PAINTING` reports each region's box, pixel count and limit.
|
|
6229
|
+
Diagnostic slot rows partition that count and report their failing-pixel box.
|
|
6230
|
+
The slot is the **last actual textured drawing with sampled alpha above zero**
|
|
6231
|
+
at that pixel, measured by rig-c's rasterizer, or explicit `(uncovered)`.
|
|
6232
|
+
For translucent stacks this names the top contributor, not the exclusive
|
|
6233
|
+
material owner or the cause of the difference. Overlapping region declarations
|
|
6234
|
+
are measured independently. Clipped drawings whose ownership cannot be observed
|
|
6235
|
+
are refused rather than attributed by a part's bounding box.
|
|
6236
|
+
|
|
6237
|
+
Invalid limits, repeated names, unknown fields and out-of-stage boxes use the existing `REQUIREMENTS_FIELD` refusal; no value is guessed.
|
|
6238
|
+
|
|
6239
|
+
This optional declaration is independent of motion requirements and
|
|
6240
|
+
`fixed_skeleton`; it does not replace their schemas or validation. The named
|
|
6241
|
+
refusals are in the check/build rule table and the observation in the line table.
|
|
6242
|
+
|
|
6023
6243
|
## 8. What one character costs
|
|
6024
6244
|
|
|
6025
6245
|
| step | reference-sample | reference-demo | source |
|
|
@@ -6324,6 +6544,26 @@ art. An authored `parts.json` depth reference remains an active standalone
|
|
|
6324
6544
|
guards still apply. With binding inactive there is no added reference, file or
|
|
6325
6545
|
legacy-byte change. #773 adds no image generation, geometry or motion model.
|
|
6326
6546
|
|
|
6547
|
+
### Missing body-turn coverage
|
|
6548
|
+
|
|
6549
|
+
`RIG_TURN_BODY_UNCOVERED` warns when a shown part without a
|
|
6550
|
+
`turn.body.roles` entry overlaps a moved body surface. It names both parts,
|
|
6551
|
+
both endpoint times, their actual emitted displacement vectors and the maximum
|
|
6552
|
+
relative displacement at a setup painting-pixel centre. Both drawings require
|
|
6553
|
+
alpha >= 128 and actual covering setup triangles; bounding boxes and transparent
|
|
6554
|
+
pixels alone do not count. Excluded alpha does not count. The diagnostic reads
|
|
6555
|
+
positive bindings in the declared torso subtree, excluding head/neck subtrees.
|
|
6556
|
+
Explicit body roles and bindings wholly inheriting an emitted body-turn carrier
|
|
6557
|
+
count as declared coverage; identical inherited motion needs no second role. Differences no larger than the producer's six-place
|
|
6558
|
+
encoding unit (0.000001 rig px) are ignored. This is a warning about a missing
|
|
6559
|
+
declaration, not an automatic seam verdict or a rule for limb crossings.
|
|
6560
|
+
|
|
6561
|
+
Warnings belong to the stage report only. They add no rig, motion or PNG field
|
|
6562
|
+
and never assign a role, depth, binding or draw order. A part already bound to
|
|
6563
|
+
`chest` does not inherit a weighted topwear surface's per-vertex deform. Declare
|
|
6564
|
+
its actual intended role, then inspect setup, both turn endpoints and their diff;
|
|
6565
|
+
an explicit role alone does not establish that two drawings remain coincident.
|
|
6566
|
+
|
|
6327
6567
|
### Unit depth precedence
|
|
6328
6568
|
|
|
6329
6569
|
For `"unit"` roles, a transported part's sheet is recorded and validated but is not
|
package/docs/STABILITY.md
CHANGED
|
@@ -54,7 +54,7 @@ defaults are the help's too, and none is guessed from the art.
|
|
|
54
54
|
| `inputs` | `--source`, `--config`, `--out` |
|
|
55
55
|
| `comfy` | `paint`: `--config`, `--out`; optional `--seeds`, `--seed0`, `--host`, `--wait`, `--timeout`, `--poll` |
|
|
56
56
|
| `comfy` | `seethrough`: `--image`, `--out`; optional `--resolution`, `--steps`, `--seed`, `--prefix`, `--offload`, `--no-offload`, `--lama`, `--nf4`, `--host`, `--wait`, `--timeout`, `--poll`, `--config` (See-through settings and optional depth block), `--depth-run` (`full` or `head`, required with the block) |
|
|
57
|
-
| `assemble` | `--propose-plan` with `--source`, `--full`, `--config`, optional `--head`, `--write-config` (must name `--config` itself, issue #356); 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) |
|
|
57
|
+
| `assemble` | `--propose-plan` with `--source`, `--full`, `--config`, optional `--head`, `--write-config` (must name `--config` itself, issue #356); 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), `--requirements` (source-bound cut-material evidence only when the file declares `cut_material`, issue #888) |
|
|
58
58
|
| `propose` | `--head-box` with `--full`, `--canvas`; optional `--no-back-hair`, `--write-config` (existing config, issue #356) |
|
|
59
59
|
| `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` (existing config for an ordinary proposal, issue #356; a new file with `--cloth`, issue #537; not with `--from-config` — 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), `--only-missing` (only with `--parts` and `--write-config`, not with `--cloth` or `--from-config`, issue #671 — additive: without it nothing `--write-config` writes changes) |
|
|
60
60
|
| `propose` | `--parts`, `--source`, `--out`, `--full`, `--head`, `--write-config` together: the [neck draft](AUTHORING.md#a-neck-that-moves-on-its-own) for an existing config with `turn.body` and one neck plan row; reassembles under `<out>/rig`, preserves the sampled turn and drafts a rigid neck unit (issue #826) |
|
|
@@ -491,6 +491,37 @@ inverse-bind deform offsets in motion, without adding a Core attachment depth
|
|
|
491
491
|
field. Head yaw/pitch and independent body yaw use their existing turn groups.
|
|
492
492
|
Blended surfaces retain the documented per-group masks and setup-overlap
|
|
493
493
|
neighbour field rules in [AUTHORING](AUTHORING.md#per-layer-depth-sheets-773).
|
|
494
|
+
`RIG_TURN_BODY_UNCOVERED` is a report-only warning for a shown torso part
|
|
495
|
+
without a body role that has opaque setup-triangle overlap with a moved body
|
|
496
|
+
surface and different emitted endpoint displacement. It names the part, surface,
|
|
497
|
+
times, vectors and relative travel. Head/neck bindings, transparent/excluded
|
|
498
|
+
alpha, explicit/inherited body roles and identical inherited motion are excluded as
|
|
499
|
+
described in [AUTHORING](AUTHORING.md#missing-body-turn-coverage). It assigns
|
|
500
|
+
no role or depth and changes no emitted file bytes.
|
|
494
501
|
The unit-depth precedence and its primary-surface sampling are documented in
|
|
495
502
|
[AUTHORING](AUTHORING.md#unit-depth-precedence).
|
|
496
503
|
The separate ordering scalar is unchanged. No-image outputs retain legacy bytes.
|
|
504
|
+
|
|
505
|
+
### Declared setup-painting regions
|
|
506
|
+
|
|
507
|
+
The optional `setup_painting` key in `spine-parts-requirements/1` adds a
|
|
508
|
+
`SETUP_MATCHES_PAINTING` observation only when declared. It requires authored
|
|
509
|
+
`rgb_tolerance` and named native-stage boxes. Each optional `max_pixels` is
|
|
510
|
+
that whole region's limit; omission is SKIP, never PASS. Existing files without
|
|
511
|
+
the key retain their output bytes and renderer calls. Other requirement fields
|
|
512
|
+
remain independent. The [authoring contract](AUTHORING.md#setup-against-the-painting)
|
|
513
|
+
defines white compositing, the figure domain, strict RGB comparison, top
|
|
514
|
+
contributor attribution and named measurement refusals. The check observes the
|
|
515
|
+
actual renderer setup at scale 1; it neither fits the art nor duplicates core
|
|
516
|
+
validation. Only this declared check composites source RGBA over white at its
|
|
517
|
+
original resolution **before** existing Lanczos3 stage resampling. Setup RGBA
|
|
518
|
+
is composited on the native stage grid. Source readers preserve alpha; the
|
|
519
|
+
global RGB-only source helper and checks without this declaration are unchanged.
|
|
520
|
+
|
|
521
|
+
The optional `cut_material` block in `--requirements` declares a part, joint,
|
|
522
|
+
independent stage-pixel centre, inward axis, reach and continuous bend interval.
|
|
523
|
+
`assemble --requirements` and `build --requirements` write source-edge evidence
|
|
524
|
+
only for that explicit block; `check --requirements` refuses unavailable or
|
|
525
|
+
stale evidence rather than reconstructing original art from emitted siblings.
|
|
526
|
+
Without the block the API reading is SKIP; CLI stdout and emitted bytes remain unchanged.
|
|
527
|
+
See [Source before scissors](AUTHORING.md#source-before-scissors).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-parts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.85.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/artjoints.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Explicit art-joint measurements on a fixed skeleton (issue #880). */
|
|
2
|
+
import { refuseIfAny, type Problem } from './errors.ts';
|
|
3
|
+
import { cropToSpineY } from './coords.ts';
|
|
4
|
+
import { edgeLength } from './length.ts';
|
|
5
|
+
|
|
6
|
+
export interface ArtJoint {
|
|
7
|
+
name: string;
|
|
8
|
+
bone: string;
|
|
9
|
+
/** Measured art point in stage pixels, x right and y down. */
|
|
10
|
+
at: [number, number];
|
|
11
|
+
}
|
|
12
|
+
export interface FixedSkeleton {
|
|
13
|
+
/** Where the independent art measurements came from; never interpreted. */
|
|
14
|
+
source: string;
|
|
15
|
+
joints: ArtJoint[];
|
|
16
|
+
/** Maximum Euclidean distance in stage pixels. Absent means measurement only. */
|
|
17
|
+
within_px?: number;
|
|
18
|
+
}
|
|
19
|
+
const object = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
20
|
+
const finite = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);
|
|
21
|
+
const free = (k: string, v: unknown): boolean => (k.startsWith('x-') && k.length > 2) || ((k === 'note' || k.endsWith('_note')) && typeof v === 'string');
|
|
22
|
+
|
|
23
|
+
/** Validate without deriving any art point, bone correspondence or limit. */
|
|
24
|
+
export function fixedSkeletonProblems(value: unknown, at: string): Problem[] {
|
|
25
|
+
const problems: Problem[] = [];
|
|
26
|
+
const fail = (where: string, detail: string): void => { problems.push({ code: 'REQUIREMENTS_FIELD', object: where, detail }); };
|
|
27
|
+
const keys = (v: Record<string, unknown>, allowed: string[], where: string): void => {
|
|
28
|
+
for (const k of Object.keys(v)) if (!allowed.includes(k) && !free(k, v[k])) fail(`${where}.${k}`, `is unknown; fields are ${allowed.join(', ')}`);
|
|
29
|
+
};
|
|
30
|
+
if (!object(value)) return [{ code: 'REQUIREMENTS_FIELD', object: at, detail: 'requires {source, joints, within_px?}; omit fixed_skeleton for a rig without this declaration' }];
|
|
31
|
+
keys(value, ['source', 'joints', 'within_px'], at);
|
|
32
|
+
if (typeof value.source !== 'string' || value.source.trim() === '') fail(`${at}.source`, 'requires the provenance of the independent art-joint measurements as nonempty text');
|
|
33
|
+
if ('within_px' in value && (!finite(value.within_px) || value.within_px < 0)) fail(`${at}.within_px`, `is ${JSON.stringify(value.within_px)}; a finite nonnegative stage-pixel limit is required, or omit it for a SKIP-class measurement`);
|
|
34
|
+
if (!Array.isArray(value.joints) || value.joints.length === 0) fail(`${at}.joints`, 'requires a nonempty list of {name, bone, at: [x, y]} measured on the art in stage pixels (x right, y down)');
|
|
35
|
+
else {
|
|
36
|
+
const seen = new Set<string>();
|
|
37
|
+
value.joints.forEach((joint, i) => {
|
|
38
|
+
const where = `${at}.joints[${i}]`;
|
|
39
|
+
if (!object(joint)) { fail(where, 'requires {name, bone, at: [x, y]}'); return; }
|
|
40
|
+
keys(joint, ['name', 'bone', 'at'], where);
|
|
41
|
+
for (const k of ['name', 'bone']) if (typeof joint[k] !== 'string' || joint[k].trim() === '') fail(`${where}.${k}`, 'requires a nonempty name');
|
|
42
|
+
if (typeof joint.name === 'string') {
|
|
43
|
+
if (seen.has(joint.name)) fail(`${where}.name`, `declares joint "${joint.name}" twice; each measured joint is declared once`);
|
|
44
|
+
seen.add(joint.name);
|
|
45
|
+
}
|
|
46
|
+
if (!Array.isArray(joint.at) || joint.at.length !== 2 || !joint.at.every(finite)) fail(`${where}.at`, 'requires a finite [x, y] measured on the art in stage pixels (x right, y down)');
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return problems;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const ART_JOINT_LINE = 'ART_JOINT_DISTANCE';
|
|
53
|
+
export interface ArtJointRow extends ArtJoint {
|
|
54
|
+
/** Bone origin in the same stage-pixel space as at. */
|
|
55
|
+
bone_at: [number, number];
|
|
56
|
+
distance_px: number;
|
|
57
|
+
status: 'PASS' | 'FAIL' | 'SKIP';
|
|
58
|
+
}
|
|
59
|
+
export interface ArtJointLine {
|
|
60
|
+
status: 'PASS' | 'FAIL' | 'SKIP';
|
|
61
|
+
instrument: 'rigc-geometry/1 setup.bones';
|
|
62
|
+
source: string;
|
|
63
|
+
within_px: number | null;
|
|
64
|
+
joints: ArtJointRow[];
|
|
65
|
+
reason?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Compare independent art points to renderer setup origins, never animation frame zero. */
|
|
69
|
+
export function artJointLine(
|
|
70
|
+
fixed: FixedSkeleton,
|
|
71
|
+
stage: { x: number; y: number; height: number },
|
|
72
|
+
setup: (bone: string) => { worldX: number; worldY: number },
|
|
73
|
+
problems: Problem[],
|
|
74
|
+
): ArtJointLine {
|
|
75
|
+
refuseIfAny(fixedSkeletonProblems(fixed, 'fixed_skeleton'));
|
|
76
|
+
const limit = fixed.within_px;
|
|
77
|
+
const joints = fixed.joints.map<ArtJointRow>(j => {
|
|
78
|
+
const world = setup(j.bone);
|
|
79
|
+
if (!Number.isFinite(world.worldX) || !Number.isFinite(world.worldY)) refuseIfAny([{ code: 'CHECK_ART_JOINT_MEASURED', object: `joint "${j.name}" (bone "${j.bone}")`, detail: 'has no finite renderer setup origin; a missing position is never a zero distance' }]);
|
|
80
|
+
const boneAt: [number, number] = [world.worldX - stage.x, cropToSpineY(world.worldY - stage.y, stage.height)];
|
|
81
|
+
const distance = edgeLength(j.at[0] - boneAt[0], j.at[1] - boneAt[1]);
|
|
82
|
+
if (!Number.isFinite(distance)) refuseIfAny([{ code: 'CHECK_ART_JOINT_MEASURED', object: `joint "${j.name}" (bone "${j.bone}")`, detail: 'has no finite art-to-setup distance; finite stage-pixel coordinates are required' }]);
|
|
83
|
+
const status = limit === undefined ? 'SKIP' : distance > limit ? 'FAIL' : 'PASS';
|
|
84
|
+
if (status === 'FAIL') problems.push({ code: 'CHECK_ART_JOINT_DISTANCE', object: `joint "${j.name}" (bone "${j.bone}")`, detail: `art at [${j.at.join(', ')}] is ${distance} stage px from setup origin [${boneAt.join(', ')}]; distance <= declared fixed_skeleton.within_px ${limit} px is required — correct the art measurement/correspondence if wrong, or supply art aligned to the fixed skeleton; bones are not moved` });
|
|
85
|
+
return { name: j.name, bone: j.bone, at: [...j.at] as [number, number], bone_at: boneAt, distance_px: distance, status };
|
|
86
|
+
});
|
|
87
|
+
return { status: limit === undefined ? 'SKIP' : joints.some(j => j.status === 'FAIL') ? 'FAIL' : 'PASS', instrument: 'rigc-geometry/1 setup.bones', source: fixed.source, within_px: limit ?? null, joints,
|
|
88
|
+
...(limit === undefined ? { reason: 'fixed_skeleton.within_px is absent; distances are measured, no limit is invented and no joint passes' } : {}) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function artJointLines(line: ArtJointLine): string[] {
|
|
92
|
+
return [`${ART_JOINT_LINE}: ${line.status}${line.reason === undefined ? '' : ` — ${line.reason}`}; source ${JSON.stringify(line.source)}; ${line.instrument}`,
|
|
93
|
+
...line.joints.map(j => `${ART_JOINT_LINE}: joint "${j.name}" (bone "${j.bone}") ${j.status} — art [${j.at.join(', ')}], setup [${j.bone_at.join(', ')}], distance ${j.distance_px} px; ${line.within_px === null ? 'no declared limit (SKIP)' : `limit <= ${line.within_px} px`}`)];
|
|
94
|
+
}
|
package/src/assemble.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { edgeLength } from './length.ts';
|
|
2
|
+
import { materialSource, type MaterialSource, materialRows, polygonMaterialEdges, type MaterialRow, type MaterialEdge, type PlacedArt } from './cutmaterial.ts';
|
|
1
3
|
import { unionTakenSources } from './takesource.ts';
|
|
2
4
|
import { authoredUnitDepths, type AuthoredUnitDepth } from './authoredunit.ts';
|
|
3
5
|
import { paintingExtensionMasks, paintingExtensionProblems, type PaintingExtension } from './paintingextension.ts';
|
|
@@ -3934,10 +3936,10 @@ function cutLimbs(
|
|
|
3934
3936
|
readHidden: (layer: string, by: string) => void = () => undefined,
|
|
3935
3937
|
tips?: Record<string, [number, number]>,
|
|
3936
3938
|
sourceGroups: ReadonlyMap<string, string> = new Map(),
|
|
3937
|
-
): { parts: LimbPart[]; pieces: Built[]; counts: LimbCount[]; capFrom: Array<CapFrom | null
|
|
3939
|
+
): { parts: LimbPart[]; pieces: Built[]; counts: LimbCount[]; capFrom: Array<CapFrom | null>; material: MaterialRow[]; sources: MaterialSource[] } {
|
|
3938
3940
|
const logical = (source: string): string => sourceGroups.get(source) ?? source;
|
|
3939
3941
|
const names = Object.keys(limbs);
|
|
3940
|
-
if (names.length === 0) return { parts: [], pieces: [], counts: [], capFrom: [] };
|
|
3942
|
+
if (names.length === 0) return { parts: [], pieces: [], counts: [], capFrom: [], material: [], sources: [] };
|
|
3941
3943
|
const problems: Problem[] = [];
|
|
3942
3944
|
const places = bonePlaces(bones, tips);
|
|
3943
3945
|
const parts: LimbPart[] = [];
|
|
@@ -4413,7 +4415,30 @@ function cutLimbs(
|
|
|
4413
4415
|
});
|
|
4414
4416
|
}
|
|
4415
4417
|
refuseIfAny(problems);
|
|
4416
|
-
|
|
4418
|
+
const material: MaterialRow[] = [];
|
|
4419
|
+
const sourceReadings: MaterialSource[] = [];
|
|
4420
|
+
for (const l of names) limbs[l].segments.forEach((sg, k) => {
|
|
4421
|
+
if (k === 0) return;
|
|
4422
|
+
const line = (lines.get(l) as Array<JointLine | null>)[k];
|
|
4423
|
+
if (line === null) return;
|
|
4424
|
+
const lo = parts.findIndex(p => p.part === sg.part);
|
|
4425
|
+
const source = [...new Set([limbs[l].segments[k - 1].from, sg.from])].flatMap(name => {
|
|
4426
|
+
const b = builtOf(name);
|
|
4427
|
+
if (b === null) return [];
|
|
4428
|
+
// Match segmentation and cap source ownership, after crossing/hand reassignment.
|
|
4429
|
+
// Evidence must never borrow another limb's island or mutate its source raster.
|
|
4430
|
+
const image = { ...b.r, data: b.r.data.slice() };
|
|
4431
|
+
const owned = owners.get(name), li = names.indexOf(l);
|
|
4432
|
+
for (let p = 0; p < W * H; p++) if (owned?.[p] !== li) image.data.fill(0, p * 4, p * 4 + 4);
|
|
4433
|
+
return [{ x: 0, y: 0, image }];
|
|
4434
|
+
});
|
|
4435
|
+
const own = [lo, ...(sg.hybrid === undefined ? [] : [parts.findIndex(p => p.part === sg.hybrid?.part)])].filter(i => i >= 0).map(i => ({ x: 0, y: 0, image: (pieces[i] as Built).r }));
|
|
4436
|
+
const t: [number, number] = [-line.axis[1], line.axis[0]], r = edgeLength(W, H);
|
|
4437
|
+
const edge: MaterialEdge = { a: [line.at[0] - r * t[0], line.at[1] - r * t[1]], b: [line.at[0] + r * t[0], line.at[1] + r * t[1]], inward: line.axis };
|
|
4438
|
+
sourceReadings.push(materialSource(sg.part, [...new Set([limbs[l].segments[k - 1].from, sg.from])], [edge], source));
|
|
4439
|
+
material.push(...materialRows(sg.part, undefined, [edge], source, own).map(row => ({ ...row, joint: String(sg.joint) })));
|
|
4440
|
+
});
|
|
4441
|
+
return { parts, pieces: pieces as Built[], counts, capFrom, material, sources: sourceReadings };
|
|
4417
4442
|
}
|
|
4418
4443
|
|
|
4419
4444
|
// ---------------------------------------------------------------------------
|
|
@@ -4470,6 +4495,9 @@ export interface AssembleResult {
|
|
|
4470
4495
|
figures: RecompositeFigures;
|
|
4471
4496
|
/** What each `assemble.cuts` entry took, counted when it was made (`cutLines`); empty without cuts. */
|
|
4472
4497
|
cuts: CutCount[];
|
|
4498
|
+
/** Console-only source-edge support (#888); never serialized into parts.json. */
|
|
4499
|
+
cutMaterial: MaterialRow[];
|
|
4500
|
+
materialSources: MaterialSource[];
|
|
4473
4501
|
/** What each `config.limbs` limb cut, per part it took and per joint (`limbLines`); empty without limbs (issue #349). */
|
|
4474
4502
|
limbs: LimbCount[];
|
|
4475
4503
|
/** The full-run layers the plan does not take from the full run whose uncovered error holds a body (`uncoveredErrorLayers`); empty otherwise. */
|
|
@@ -5016,11 +5044,14 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5016
5044
|
const mergedIn = (b: Built, region: Mask): number => count(b.trace.lastSel ?? null, region) + count(b.trace.lastRing ?? null, region);
|
|
5017
5045
|
const pieces: Array<Built | null> = cuts.map(() => null);
|
|
5018
5046
|
const cutCounts: CutCount[] = [];
|
|
5047
|
+
const cutMaterial: MaterialRow[] = [];
|
|
5048
|
+
const materialSources: MaterialSource[] = [];
|
|
5019
5049
|
plan.forEach(([name], pi) => {
|
|
5020
5050
|
const mine = cuts.map((q, i) => i).filter((i) => cuts[i].from === name);
|
|
5021
5051
|
const b = built[pi];
|
|
5022
5052
|
if (mine.length === 0 || b === null) return;
|
|
5023
5053
|
const before = opaqueOf(b.r);
|
|
5054
|
+
const sourceBeforeCuts: PlacedArt = { x: 0, y: 0, image: { ...b.r, data: b.r.data.slice() } };
|
|
5024
5055
|
const rest = newMask(W, H);
|
|
5025
5056
|
rest.data.fill(1);
|
|
5026
5057
|
// The base's own pixels: `rest`, and every cut's band, which it keeps as well as the piece.
|
|
@@ -5120,6 +5151,8 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5120
5151
|
if (cuts[i].under_fill === 'colour') c.underFill = 'colour';
|
|
5121
5152
|
}
|
|
5122
5153
|
cutCounts.push(c);
|
|
5154
|
+
const piece = pieces[i];
|
|
5155
|
+
if (piece !== null) { materialSources.push(materialSource(cuts[i].into, [b.key], polygonMaterialEdges(cuts[i].polygon), [sourceBeforeCuts])); cutMaterial.push(...materialRows(cuts[i].into, undefined, polygonMaterialEdges(cuts[i].polygon), [sourceBeforeCuts], [{ x: 0, y: 0, image: piece.r }])); }
|
|
5123
5156
|
});
|
|
5124
5157
|
});
|
|
5125
5158
|
// Parent partition counts and under use the removed parent art above. Only
|
|
@@ -5131,6 +5164,10 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5131
5164
|
if (opaqueOf(r) === 0) empty.push({ code: 'ASSEMBLE_CUT_PIXELS', object: `config.assemble.cuts[${i}] (part "${cuts[i].into}")`, detail: `takes 0 opaque pixels from selected ${source.key}; draw its polygon over that layer's art` });
|
|
5132
5165
|
const writers = (source.writers as Int32Array).slice();
|
|
5133
5166
|
pieces[i] = { ...source, r, writers, region: m, merged: mergedIn(source, m) };
|
|
5167
|
+
for (let j = materialSources.length - 1; j >= 0; j--) if (materialSources[j].part === cuts[i].into) materialSources.splice(j, 1);
|
|
5168
|
+
materialSources.push(materialSource(cuts[i].into, [source.key], polygonMaterialEdges(cuts[i].polygon), [{ x: 0, y: 0, image: source.r }]));
|
|
5169
|
+
for (let j = cutMaterial.length - 1; j >= 0; j--) if (cutMaterial[j].part === cuts[i].into) cutMaterial.splice(j, 1);
|
|
5170
|
+
cutMaterial.push(...materialRows(cuts[i].into, undefined, polygonMaterialEdges(cuts[i].polygon), [{ x: 0, y: 0, image: source.r }], [{ x: 0, y: 0, image: r }]));
|
|
5134
5171
|
});
|
|
5135
5172
|
refuseIfAny(empty);
|
|
5136
5173
|
// In cut order, as the config lists them.
|
|
@@ -5638,6 +5675,8 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5638
5675
|
figure: inside === null ? null : figureImage(inside, W, H),
|
|
5639
5676
|
figures,
|
|
5640
5677
|
cuts: cutCounts,
|
|
5678
|
+
cutMaterial: [...cutMaterial, ...lim.material],
|
|
5679
|
+
materialSources: [...materialSources, ...lim.sources],
|
|
5641
5680
|
limbs: lim.counts,
|
|
5642
5681
|
uncovered,
|
|
5643
5682
|
holeKinds: kinds,
|