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 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. Conflicting neighbour groups are refused by name. Each sibling island takes only its
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. Their own sheet is
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.73.0",
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
  }