rig-parts 1.73.0 → 1.74.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/README.md +4 -1
- package/docs/AUTHORING.md +41 -1
- package/package.json +1 -1
- package/src/build.ts +1 -0
- package/src/rig.ts +35 -4
- package/src/turn.ts +19 -0
package/README.md
CHANGED
|
@@ -595,7 +595,10 @@ and [`CONFIG_TURN_SURFACE_REQUIRED`](docs/AUTHORING.md#CONFIG_TURN_SURFACE_REQUI
|
|
|
595
595
|
fields in proportion to its skin weights. Inside the existing setup alpha
|
|
596
596
|
overlap with a single-group neighbour, a blended attachment's vertex takes
|
|
597
597
|
that neighbour's existing emitted triangle field at its setup position (#804), without changing skin weights,
|
|
598
|
-
setup or idle.
|
|
598
|
+
setup or idle. A `motion.blink.still` unit neighbour supplies both emitted
|
|
599
|
+
pieces, each on its own bone and resolved depth; transparent padding does not
|
|
600
|
+
let either piece lend its field to the other's art (#807). Missing emitted
|
|
601
|
+
split support is refused by name. Conflicting neighbour groups are refused by name. Each sibling island takes only its
|
|
599
602
|
own group; parts without a blended attachment keep their existing role behavior
|
|
600
603
|
and output bytes. Unlisted meshes do not consume turn depth sheets. See the
|
|
601
604
|
[blended surface contract](docs/AUTHORING.md#per-layer-depth-sheets-773).
|
package/docs/AUTHORING.md
CHANGED
|
@@ -2854,6 +2854,36 @@ shin's tip against the stage point it stands on.
|
|
|
2854
2854
|
with `exponent` 6 on `legwear`, 1.6. The same dance on `demo` as tracked, whose legs ride the
|
|
2855
2855
|
hip, reads 18.36.
|
|
2856
2856
|
|
|
2857
|
+
|
|
2858
|
+
### Equal-depth units in a sampled turn
|
|
2859
|
+
|
|
2860
|
+
With `turn.roles`, `turn.depths` remains a legacy layer-order table; it does
|
|
2861
|
+
not override sampled unit motion. Its See-through values (larger means farther
|
|
2862
|
+
back) are not interchangeable with `turn.unitDepths`, whose values are calibrated
|
|
2863
|
+
scalar depths in rig pixels (larger means nearer the viewer).
|
|
2864
|
+
|
|
2865
|
+
To make a mirrored pair of `"unit"` parts travel equally, author equal
|
|
2866
|
+
`turn.unitDepths.<part>` values. A part entry wins over a bone entry, including
|
|
2867
|
+
zero. Otherwise the producer samples the primary surface at each unit's setup
|
|
2868
|
+
landmark, calibrating `pivot + sampledZ - surfaceZRef`; mirrored landmarks on an
|
|
2869
|
+
asymmetric depth sheet can therefore travel differently. Only when that sample
|
|
2870
|
+
is unavailable does it fall back to the unit's own sheet. No symmetry is inferred
|
|
2871
|
+
from part names or positions, and equal legacy `depths` do not establish it.
|
|
2872
|
+
|
|
2873
|
+
Equal unit depths yield the same signed translation within a frame on both yaw
|
|
2874
|
+
and pitch; the sign reverses between negative and positive angles. Their local
|
|
2875
|
+
keys compensate for the parent carrier, so compare the complete inherited pose
|
|
2876
|
+
when measuring screen travel. For ears, the existing behind-cheek depth check
|
|
2877
|
+
still applies to an explicit override; use a depth at or behind both cheeks.
|
|
2878
|
+
|
|
2879
|
+
The build report prints `WARN RIG_TURN_UNIT_DEPTH_SUPPORT` when an implicit unit
|
|
2880
|
+
sample lies outside the surface's source pixel-index bounds or its sampled art
|
|
2881
|
+
alpha is below 255. It names the unit, source surface and sheet, raw crop
|
|
2882
|
+
coordinate, clamped art index and support alpha. Scalar-sheet alpha is not art
|
|
2883
|
+
support. This warning retains the extended scalar sample, all motion arithmetic
|
|
2884
|
+
and existing refusal behavior; it is not a gate. Explicit `unitDepths` entries
|
|
2885
|
+
bypass that sample and its warning, including an explicit zero.
|
|
2886
|
+
|
|
2857
2887
|
## 4. The command order
|
|
2858
2888
|
|
|
2859
2889
|
Two steps are external: the See-through runs (by any route; the optional `comfy seethrough` adapter is only a client for a ComfyUI box). Everything else is this tool.
|
|
@@ -4528,6 +4558,7 @@ and stay absent).
|
|
|
4528
4558
|
| <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` |
|
|
4529
4559
|
| <a id="RIG_TURN_SHARED_DEPTH_SCALE"></a>`RIG_TURN_SHARED_DEPTH_SCALE` | `"surface"` parts bound to one head or body turn declare different `near` polarities or `zScale` values | use one shared `near` and `zScale` across all `"surface"` parts in the turn group |
|
|
4530
4560
|
| <a id="RIG_TURN_UNIT_DEPTH_REQUIRED"></a>`RIG_TURN_UNIT_DEPTH_REQUIRED` | a `"unit"` bone has neither a `unitDepths` entry nor a readable `"surface"` depth sheet | provide a `"surface"` depth sheet or an explicit `unitDepths.<part>` value |
|
|
4561
|
+
| <a id="RIG_TURN_UNIT_DEPTH_SUPPORT"></a>`RIG_TURN_UNIT_DEPTH_SUPPORT` | WARN only: an implicit unit depth sample is outside the source surface pixel-index bounds or its sampled art alpha is below 255 | author an explicit calibrated `unitDepths.<part>` value when this cross-part extrapolation is unintended; existing scalar sampling and refusal behavior are retained |
|
|
4531
4562
|
| <a id="RIG_TURN_EAR_DEPTH_BEHIND_CHEEK"></a>`RIG_TURN_EAR_DEPTH_BEHIND_CHEEK` | an ear bone in `config.turn.roles` has a calibrated depth at or in front of the cheek surface depth | place the ear behind the cheek surface (for example via `config.turn.unitDepths`) |
|
|
4532
4563
|
|
|
4533
4564
|
## 7. The bars `check` enforces, and what only an eye answers today
|
|
@@ -5644,7 +5675,16 @@ Two same-group fields with incompatible emitted offsets raise
|
|
|
5644
5675
|
[`RIG_TURN_SURFACE_OVERLAP_DEPTH`](#RIG_TURN_SURFACE_OVERLAP_DEPTH).
|
|
5645
5676
|
|
|
5646
5677
|
Unit/rear neighbours contribute their actual emitted rigid carrier motion,
|
|
5647
|
-
including its resolved unit depth or rear counter field.
|
|
5678
|
+
including its resolved unit depth or rear counter field. A unit region split by
|
|
5679
|
+
`motion.blink.still` supplies both emitted drawings: the lower slot and
|
|
5680
|
+
`<part>_still`, each with its own bone and resolved unit depth (#807). Support
|
|
5681
|
+
uses the shown region's image size, offsets and rotation, with transparent
|
|
5682
|
+
padding excluded from each piece's crop rectangle. The original source-alpha
|
|
5683
|
+
condition still applies; the lower piece cannot lend its depth to upper art
|
|
5684
|
+
through its transparent padding. A missing split slot, shown attachment,
|
|
5685
|
+
image or setup frame raises
|
|
5686
|
+
[`RIG_TURN_SURFACE_OVERLAP_SUPPORT`](#RIG_TURN_SURFACE_OVERLAP_SUPPORT);
|
|
5687
|
+
a slot explicitly showing nothing supplies no attachment. Their own sheet is
|
|
5648
5688
|
not resampled to invent a spatial bend or a new refusal. Generated surface
|
|
5649
5689
|
grids keep their Core generator: Core's grid builder and transform evaluator
|
|
5650
5690
|
provide inspection metadata for the same existing topology and vertex field.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-parts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.74.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/build.ts
CHANGED
|
@@ -1528,6 +1528,7 @@ export function rigStage(input: RigStageInput, rigc: RigcRunner, scratch: string
|
|
|
1528
1528
|
if (from !== null) log(` animations beside the idle, from ${from.file} as written: ${Object.keys(from.table).join(', ') || 'none'}`);
|
|
1529
1529
|
log(faceAxisLine(rig.faceAxis));
|
|
1530
1530
|
if (rig.turn !== null) log(turnLine(rig.turn));
|
|
1531
|
+
for (const w of rig.turnWarnings ?? []) log(` WARN ${w.code}: ${w.object} — ${w.detail}`);
|
|
1531
1532
|
log(
|
|
1532
1533
|
rig.idleKeys === 'ctl'
|
|
1533
1534
|
? ` idle keys ctl: ${rig.meshKeyed.length} mesh-driving bone(s) keyed by the idle, each keyed through a same-origin <bone>_ctl parent`
|
package/src/rig.ts
CHANGED
|
@@ -138,6 +138,7 @@ import { explicitAttachment, type ExplicitMeshReport } from './explicitmesh.ts';
|
|
|
138
138
|
* order the objects are built in, and every number is rounded by `pyRound`).
|
|
139
139
|
*/
|
|
140
140
|
import { type AutoSpec, type BoneEntry, type InfluenceMeshSpec, type WeightRule, type CharacterConfig, type ConfigConstraint, type ConfigStop, CONSTRAINT_FOLLOWS, constraintForRig, contourRegionsOf, type ContourSpec, DEFAULT_BUST_BONE_L, DEFAULT_BUST_BONE_R, type Point, type RibsSpec, ROOT_BONE, stopForRig, turnRidersByBone, weightsABone } from './config.ts';
|
|
141
|
+
import { RUNTIME_PI } from 'rig-c/src/core/world.ts';
|
|
141
142
|
import { type BoneTransform, computeExactFrameTransforms, computeWorldTransforms, cropToSpineY, normaliseDegrees, paintingToSetupPoint, setupToPaintingPoint, toBoneLocal, toWorld } from './coords.ts';
|
|
142
143
|
import { type Problem, refuseIfAny, PartsError } from './errors.ts';
|
|
143
144
|
import { recordedFocusProblems } from './setfocus.ts';
|
|
@@ -1131,6 +1132,8 @@ export interface AttachmentSetsFile {
|
|
|
1131
1132
|
}
|
|
1132
1133
|
|
|
1133
1134
|
export interface RigOutput {
|
|
1135
|
+
/** Sample-support warnings for the build report only; no emitted-file field. */
|
|
1136
|
+
turnWarnings?: Problem[];
|
|
1134
1137
|
depthReferences?: Array<{ part: string; size: [number, number]; depth: PartDepthReference }>;
|
|
1135
1138
|
rig: RigSpec;
|
|
1136
1139
|
/** Internal explicit-layout provenance; serialized only as an external companion. */
|
|
@@ -3652,8 +3655,10 @@ export function buildWaistPairSource(
|
|
|
3652
3655
|
for (const [bName, bEntry] of B) {
|
|
3653
3656
|
boneLandmarks.set(bName, [bEntry.x, bEntry.y]);
|
|
3654
3657
|
}
|
|
3658
|
+
const turnWarnings: Problem[] = [];
|
|
3655
3659
|
if (cfg.turn?.roles !== undefined || cfg.turn?.body !== undefined) {
|
|
3656
3660
|
buildDepth25DTurn(rig, motion, {
|
|
3661
|
+
warnings: turnWarnings,
|
|
3657
3662
|
turn: cfg.turn,
|
|
3658
3663
|
parts: parts.parts,
|
|
3659
3664
|
images,
|
|
@@ -3662,7 +3667,7 @@ export function buildWaistPairSource(
|
|
|
3662
3667
|
boneLandmarks,
|
|
3663
3668
|
hasAlternateFaceSet,
|
|
3664
3669
|
surfaceMeshes: turnSurfaceMeshes(cfg, rig, parts.rig_size),
|
|
3665
|
-
neighbourMeshes: turnNeighbourMeshes(cfg, rig, parts),
|
|
3670
|
+
neighbourMeshes: turnNeighbourMeshes(cfg, rig, parts, new Map(outImages)),
|
|
3666
3671
|
});
|
|
3667
3672
|
}
|
|
3668
3673
|
bindFaceSurfaceDeform(motion, (merged) => {
|
|
@@ -3679,7 +3684,7 @@ export function buildWaistPairSource(
|
|
|
3679
3684
|
boneLandmarks,
|
|
3680
3685
|
hasAlternateFaceSet,
|
|
3681
3686
|
surfaceMeshes: turnSurfaceMeshes(cfg, rig, parts.rig_size),
|
|
3682
|
-
neighbourMeshes: turnNeighbourMeshes(cfg, rig, parts),
|
|
3687
|
+
neighbourMeshes: turnNeighbourMeshes(cfg, rig, parts, new Map(outImages)),
|
|
3683
3688
|
});
|
|
3684
3689
|
}
|
|
3685
3690
|
if (cfg.turn?.roles === undefined && (cfg.turn === undefined || cfg.turn.body === undefined || cfg.turn.depths !== undefined)) {
|
|
@@ -3716,6 +3721,7 @@ export function buildWaistPairSource(
|
|
|
3716
3721
|
autoMotion,
|
|
3717
3722
|
faceAxis,
|
|
3718
3723
|
turn: turned?.rows ?? null,
|
|
3724
|
+
...(turnWarnings.length === 0 ? {} : { turnWarnings }),
|
|
3719
3725
|
...(sets === undefined ? {} : { sets }),
|
|
3720
3726
|
...(explicitLayout.plan === undefined ? {} : { layout: explicitLayout.plan }),
|
|
3721
3727
|
};
|
|
@@ -4200,7 +4206,7 @@ function turnSurfaceMeshes(cfg: CharacterConfig, rig: RigSpec, canvas: readonly
|
|
|
4200
4206
|
}
|
|
4201
4207
|
|
|
4202
4208
|
/** Actual shown support; Core's own grid topology is inspection metadata only. */
|
|
4203
|
-
function turnNeighbourMeshes(cfg: CharacterConfig, rig: RigSpec, parts: PartsFile) {
|
|
4209
|
+
export function turnNeighbourMeshes(cfg: CharacterConfig, rig: RigSpec, parts: PartsFile, emittedImages: ReadonlyMap<string, Raster>) {
|
|
4204
4210
|
const meshes: NonNullable<Parameters<typeof buildDepth25DTurn>[2]['neighbourMeshes']>[number][] = [];
|
|
4205
4211
|
const canvas = parts.rig_size;
|
|
4206
4212
|
const aliases = new Map<string, string>((rig.stops ?? []).flatMap(s => s.links.map(link => [`${link}_d`, link])));
|
|
@@ -4209,8 +4215,17 @@ function turnNeighbourMeshes(cfg: CharacterConfig, rig: RigSpec, parts: PartsFil
|
|
|
4209
4215
|
if (cfg.meshes[part] !== undefined && [cfg.turn?.roles?.[part], cfg.turn?.body?.roles?.[part]].includes('surface')) continue;
|
|
4210
4216
|
const record = parts.parts.find(p => p.name === part);
|
|
4211
4217
|
if (record === undefined) continue;
|
|
4218
|
+
const split = cfg.regions[part] !== undefined && cfg.motion.blink?.still?.[part] !== undefined
|
|
4219
|
+
&& [cfg.turn?.roles?.[part], cfg.turn?.body?.roles?.[part]].includes('unit');
|
|
4220
|
+
const splitSlots = [part, `${part}${STILL_SUFFIX}`];
|
|
4221
|
+
const failSupport = (name: string, detail: string): never => { throw new PartsError([{ code: 'RIG_TURN_SURFACE_OVERLAP_SUPPORT', object: `turn neighbour "${name}"`, detail }]); };
|
|
4222
|
+
if (split) for (const name of splitSlots) {
|
|
4223
|
+
const shown = rig.slots.find(slot => slot.name === name);
|
|
4224
|
+
if (shown === undefined) failSupport(name, 'has no emitted split slot; supply both lower and still slots from the region build');
|
|
4225
|
+
else if (shown.attachment !== null && rig.skins.default[name]?.[shown.attachment] === undefined) failSupport(`${name}/${shown.attachment}`, 'has no shown attachment in the default skin; supply the emitted split drawing');
|
|
4226
|
+
}
|
|
4212
4227
|
for (const slot of rig.slots) {
|
|
4213
|
-
if (slot.name !== part && !slot.name.startsWith(`${part}__island`)) continue;
|
|
4228
|
+
if (slot.name !== part && !slot.name.startsWith(`${part}__island`) && !(split && slot.name === splitSlots[1])) continue;
|
|
4214
4229
|
for (const [attachment, att] of Object.entries(rig.skins.default[slot.name] ?? {})) {
|
|
4215
4230
|
if (attachment !== slot.attachment) continue;
|
|
4216
4231
|
if ('type' in att && att.weights !== undefined && att.weights.length > 0) { meshes.push({ part, slot: slot.name, attachment, weights: att.weights, triangles: att.triangles, canvas }); continue; }
|
|
@@ -4227,6 +4242,22 @@ function turnNeighbourMeshes(cfg: CharacterConfig, rig: RigSpec, parts: PartsFil
|
|
|
4227
4242
|
weights: bound.bindings.map(row => row.map(w => ({ bone: w.bone, x: f32(w.x), y: f32(w.y), weight: f32(w.weight) }))),
|
|
4228
4243
|
gridSamples: geometry.points.map(([x, y]) => setupToPaintingPoint(place(x, y), canvas, canvas)),
|
|
4229
4244
|
});
|
|
4245
|
+
} else if (!('type' in att) && split) {
|
|
4246
|
+
const image = emittedImages.get(att.image) ?? failSupport(`${slot.name}/${attachment}`, `has no emitted image "${att.image}"; supply the split region PNG to measure its support`);
|
|
4247
|
+
if (!Number.isFinite(att.x) || !Number.isFinite(att.y) || (att.rotation !== undefined && !Number.isFinite(att.rotation))
|
|
4248
|
+
|| !Number.isSafeInteger(image.width) || !Number.isSafeInteger(image.height) || image.width <= 2 * PAD || image.height <= 2 * PAD) failSupport(`${slot.name}/${attachment}`, `has offsets (${att.x}, ${att.y}), rotation ${att.rotation ?? 0} and image ${image.width}x${image.height}; require finite offsets/rotation and positive integer source dimensions after padding ${PAD} on each side`);
|
|
4249
|
+
// Core stores the region offsets/angle as float32. Its region
|
|
4250
|
+
// rotation uses RUNTIME_PI, not the bone matrix's y-column angle.
|
|
4251
|
+
const angle = f32(att.rotation ?? 0) * RUNTIME_PI / 180, cos = Math.cos(angle), sin = Math.sin(angle);
|
|
4252
|
+
const x = f32(att.x), y = f32(att.y), halfWidth = image.width / 2 - PAD, halfHeight = image.height / 2 - PAD;
|
|
4253
|
+
// Transparent PAD belongs to neither piece's alpha support. Keeping
|
|
4254
|
+
// both padded rectangles would let the lower lend its field to the
|
|
4255
|
+
// upper rows (and vice versa) beside the cut. Source alpha still
|
|
4256
|
+
// decides ownership inside each piece's own emitted crop rectangle.
|
|
4257
|
+
const corners = [[-halfWidth, -halfHeight], [-halfWidth, halfHeight], [halfWidth, halfHeight], [halfWidth, -halfHeight]];
|
|
4258
|
+
meshes.push({ part, slot: slot.name, attachment, canvas, triangles: [0, 1, 2, 0, 2, 3],
|
|
4259
|
+
weights: corners.map(([px, py]) => [{ bone: slot.bone, x: px * cos + x - py * sin, y: py * cos + y + px * sin, weight: 1 }]),
|
|
4260
|
+
});
|
|
4230
4261
|
} else if (!('type' in att)) {
|
|
4231
4262
|
const points = [[record.x - PAD, record.y - PAD], [record.x - PAD, record.y + record.h + PAD], [record.x + record.w + PAD, record.y + record.h + PAD], [record.x + record.w + PAD, record.y - PAD]];
|
|
4232
4263
|
meshes.push({ part, slot: slot.name, attachment, canvas, triangles: [0, 1, 2, 0, 2, 3], weights: points.map(p => { const q = paintingToSetupPoint(p, canvas, canvas), local = toBoneLocal(frame, q[0], q[1]); return [{ bone: slot.bone, x: f32(local[0]), y: f32(local[1]), weight: 1 }]; }) });
|
package/src/turn.ts
CHANGED
|
@@ -1478,6 +1478,8 @@ export function sampleUnitPartDepth(scalar: Raster, art: Raster, ref: PartDepthR
|
|
|
1478
1478
|
}
|
|
1479
1479
|
|
|
1480
1480
|
export interface Depth25DTurnOptions {
|
|
1481
|
+
/** Report-only diagnostics; never serialized into the rig or motion. */
|
|
1482
|
+
warnings?: Problem[];
|
|
1481
1483
|
turn: TurnSpec;
|
|
1482
1484
|
parts: ReadonlyArray<{ name: string; x: number; y: number; w: number; h: number; depth?: PartDepthReference }>;
|
|
1483
1485
|
images: ReadonlyMap<string, Raster>;
|
|
@@ -1684,6 +1686,23 @@ function buildDepth25DTurnGroup(
|
|
|
1684
1686
|
const padY = Math.floor((ext.height - rec.h) / 2);
|
|
1685
1687
|
const lx = Math.max(0, Math.min(ext.width - 1, Math.round(pt[0] - rec.x + padX)));
|
|
1686
1688
|
const ly = Math.max(0, Math.min(ext.height - 1, Math.round(pt[1] - rec.y + padY)));
|
|
1689
|
+
// The scalar extension has opaque alpha even where the source art
|
|
1690
|
+
// supplies no opaque support. Report that extrapolation, not its value.
|
|
1691
|
+
if (opts.warnings !== undefined) {
|
|
1692
|
+
const art = opts.images.get(rec.name);
|
|
1693
|
+
const rawX = pt[0] - rec.x, rawY = pt[1] - rec.y;
|
|
1694
|
+
const outside = rawX < 0 || rawY < 0 || rawX > rec.w - 1 || rawY > rec.h - 1;
|
|
1695
|
+
const artX = Math.max(0, Math.min(rec.w - 1, lx - padX));
|
|
1696
|
+
const artY = Math.max(0, Math.min(rec.h - 1, ly - padY));
|
|
1697
|
+
const alpha = art?.data[(artY * rec.w + artX) * 4 + 3];
|
|
1698
|
+
if (alpha !== undefined && (outside || alpha !== 255)) {
|
|
1699
|
+
opts.warnings.push({
|
|
1700
|
+
code: 'RIG_TURN_UNIT_DEPTH_SUPPORT',
|
|
1701
|
+
object: `${group.fieldPrefix}.roles.${part}`,
|
|
1702
|
+
detail: `unit part "${part}" (bone "${bName}") samples surface "${rec.name}" sheet "${rec.depth.image}" at raw crop [${pt[0]}, ${pt[1]}] (${outside ? 'outside' : 'inside'} source sample bounds [${rec.x}, ${rec.y}]..[${rec.x + rec.w - 1}, ${rec.y + rec.h - 1}]); sampled art [${artX}, ${artY}] has art alpha ${alpha}, expected 255 without clamping; the existing extended scalar sample is retained; author ${group.fieldPrefix}.unitDepths.${part} for an explicit calibrated depth`,
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1687
1706
|
const zUnit = levelToDepthZ(ext.data[(ly * ext.width + lx) * 4], rec.depth);
|
|
1688
1707
|
resolved = pyRound(pivot + (zUnit - zRef), 4);
|
|
1689
1708
|
}
|