rig-parts 1.83.0 → 1.83.1

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/docs/AUTHORING.md CHANGED
@@ -668,8 +668,19 @@ Taken layers replace the target's tag, keeping the donor's tag and
668
668
  `depth_median`, so existing hair/blink classification continues to read `from`.
669
669
  Larger depth draws behind smaller depth. Numeric ties keep manifest order only
670
670
  inside one original run. Equal or null depths **across** runs are ambiguous
671
- and refused, even if the plan gives an order. A supplied plan reversing the
672
- mixed-run depth order is also refused; `--propose-plan` drafts the depth order.
671
+ and refused, even if the plan gives an order. Plan-order checks compare only
672
+ different original runs' layers whose original supports intersect (both alpha
673
+ > 8, in the target-run canvas, without dilation). Disjoint supports have no
674
+ visible order; intersecting bounding boxes alone do not establish overlap.
675
+ Within one role band, larger depth must draw behind smaller depth. In a two-run
676
+ plan, full-run rows follow the proposer's head-fallback roles before scalar
677
+ depth: back hair, body, headwear/earwear, face, head details, front hair. Thus
678
+ fallback headwear is after the body and before the face, and fallback front
679
+ hair stays over the face even when its donor depth is larger. Roles come from
680
+ source tags, never part names. Head-run rows and single-run plans keep scalar
681
+ depth order. A supplied plan reversing an overlapping mixed-run pair under
682
+ these rules is refused; `--propose-plan` drafts the same order. Null/tied depth
683
+ ambiguity remains a refusal even for disjoint supports.
673
684
  Replacing a layer with hidden companion art is refused because independent
674
685
  hidden-art identity has not been declared. Active scalar depth sheets (`depth`
675
686
  opt-in or already bound sheets) are also refused: selecting donor art does not
@@ -4451,7 +4462,7 @@ those settings. Changing a config seed alone cannot change a no-config run.
4451
4462
  | <a id="ASSEMBLE_TAKE_TAG_IN_RUN"></a>`ASSEMBLE_TAKE_TAG_IN_RUN` | selected donor does not hold the tag | choose a tag present in that donor |
4452
4463
  | <a id="ASSEMBLE_TAKE_SUPPORT"></a>`ASSEMBLE_TAKE_SUPPORT` | selected layer has zero alpha > 8 pixels | choose a nonempty donor layer |
4453
4464
  | <a id="ASSEMBLE_TAKE_DEPTH_AMBIGUOUS"></a>`ASSEMBLE_TAKE_DEPTH_AMBIGUOUS` | different original runs have equal or null depths | use a run with distinct numeric depths; do not invent an inter-run tie order |
4454
- | <a id="ASSEMBLE_TAKE_PLAN_ORDER"></a>`ASSEMBLE_TAKE_PLAN_ORDER` | supplied plan reverses mixed-run depth order | use the depth order drafted by `assemble --propose-plan` |
4465
+ | <a id="ASSEMBLE_TAKE_PLAN_ORDER"></a>`ASSEMBLE_TAKE_PLAN_ORDER` | supplied plan reverses an overlapping mixed-run pair: two-run full-layer head roles first, scalar depth within a role band (head-run and single-run rows use scalar depth); names the parts and support intersection | use the order drafted by `assemble --propose-plan`; fallback headwear follows the body and precedes the face, front hair follows the face; disjoint supports impose no plan-order constraint |
4455
4466
  | <a id="ASSEMBLE_TAKE_GEOMETRY"></a>`ASSEMBLE_TAKE_GEOMETRY` | missing target head, unequal run canvases or unequal original input dimensions | supply matching geometry; the check does not resize inputs |
4456
4467
  | <a id="ASSEMBLE_TAKE_META"></a>`ASSEMBLE_TAKE_META` | donor `meta.json` lacks a readable prompt ID or seed | retain the original run metadata |
4457
4468
  | <a id="ASSEMBLE_TAKE_INPUT"></a>`ASSEMBLE_TAKE_INPUT` | declared donor input PNG cannot be read | name its original PNG relative to the config |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rig-parts",
3
- "version": "1.83.0",
3
+ "version": "1.83.1",
4
4
  "description": "AI-authored 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through rig-c before anything is written. A CLI for agents that cannot see the image.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/assemble.ts CHANGED
@@ -6048,6 +6048,18 @@ export const PROPOSE_HEAD_TAGS: readonly string[] = [
6048
6048
  'eyebrow-l',
6049
6049
  ];
6050
6050
 
6051
+ /** #869: two-run full rows are body rows or head fallbacks, regardless of part spelling.
6052
+ * Within one band the run's depth order survives; the bands encode the proposer's roles.
6053
+ */
6054
+ export function fullPlanBand(tag: string): number {
6055
+ if (tag === 'back hair') return 0;
6056
+ if (!PROPOSE_HEAD_TAGS.includes(tag)) return 1;
6057
+ if (tag === 'headwear' || tag === 'earwear') return 2;
6058
+ if (tag === 'face') return 3;
6059
+ if (tag === 'front hair') return 5;
6060
+ return 4;
6061
+ }
6062
+
6051
6063
  /** #863: prefer suitable head layers; only unusable head candidates may fall back for these tags. */
6052
6064
  const HEAD_FALLBACK_TAGS: readonly string[] = ['back hair', 'face', 'front hair'];
6053
6065
 
@@ -6106,7 +6118,7 @@ function fmt0(v: number): string {
6106
6118
  * A suitable head candidate is retained. Every fallback names the two rig
6107
6119
  * areas and the reason. A `headwear`/`earwear` the head run drops or
6108
6120
  * shrinks (its rig-pixel area below 0.8 of the full run's) is taken from the
6109
- * full run instead, drawn right after `back hair` — noted. `front hair` and
6121
+ * full run instead, drawn after the body and before the face — noted. `front hair` and
6110
6122
  * `back hair` taken from the head run are extended below the crop from the
6111
6123
  * full run when the full run's layer reaches more than 8 rig pixels below the
6112
6124
  * crop line. A lone `handwear` layer is named `sleeves`.
@@ -6232,7 +6244,7 @@ export function proposePlan(full0: LayerSet, head0: LayerSet, g: Geometry, minPx
6232
6244
  return tag !== 'nose' && !(run === 'head' && tag === 'face' && headFaceRefusal !== null) &&
6233
6245
  l !== undefined && l.opaqueRunPx >= floor && !implausible(run, tag, 'not proposed');
6234
6246
  };
6235
- const body = runs.full.filter((l) => !PROPOSE_HEAD_TAGS.includes(l.tag) && ok('full', l.tag)).map((l) => l.tag);
6247
+ const body = runs.full.filter((l) => fullPlanBand(l.tag) === 1 && ok('full', l.tag)).map((l) => l.tag);
6236
6248
  // Issue #256: an author-made layer is proposed from the run that holds it, at its place in that run's draw order,
6237
6249
  // like any layer; one both runs hold is proposed once, from the full run when the full run's is proposed, and the note says so.
6238
6250
  const authorBoth = runs.head.filter((l) => readAuthorTag(l.tag) !== null && body.includes(l.tag)).map((l) => l.tag);
@@ -6279,15 +6291,15 @@ export function proposePlan(full0: LayerSet, head0: LayerSet, g: Geometry, minPx
6279
6291
  const draw = t === 'back hair' ? 'behind the body and face' : t === 'face' ? 'before head details and front hair' : 'in front of the face';
6280
6292
  notes.push(`${t}: head run ${fmt0(hp)} rig px (${reason}); full run ${fmt0(fp)} rig px -> full-run layer, drawn ${draw}`);
6281
6293
  }
6282
- for (const t of ['headwear', 'earwear']) {
6294
+ // Keep the full run's order inside the accessory band, including mixed donors.
6295
+ for (const { tag: t } of runs.full.filter(l => fullPlanBand(l.tag) === 2)) {
6283
6296
  const hl = find('head', t);
6284
6297
  const fl = find('full', t);
6285
6298
  const hp = hl !== undefined ? hl.opaqueRunPx * headArea : 0;
6286
6299
  const fp = fl !== undefined ? fl.opaqueRunPx * fullArea : 0;
6287
6300
  if (ok('full', t) && hp < 0.8 * fp) {
6288
6301
  drop(t);
6289
- const at = plan.length > 0 && plan[0][2] === 'back hair' ? 1 : 0;
6290
- plan.splice(at, 0, [PROPOSE_ALIAS[t], 'full', t]);
6302
+ plan.push([PROPOSE_ALIAS[t], 'full', t]);
6291
6303
  notes.push(`${t}: head run ${fmt0(hp)} rig px < 0.8 x full run ${fmt0(fp)} -> full-run layer, drawn behind the face`);
6292
6304
  }
6293
6305
  }
package/src/takelayers.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { dirname, resolve } from 'node:path';
4
4
  import type { EarlyConfig, PlanEntry, Run } from './config.ts';
5
- import { CORE_KERNEL } from './assemble.ts';
5
+ import { CORE_KERNEL, fullPlanBand } from './assemble.ts';
6
6
  import { makeInputs } from './inputs.ts';
7
7
  import { OPAQUE_ALPHA_ABOVE, readLayers, type Layer, type LayerSet } from './layers.ts';
8
8
  import { PartsError, refuseIfAny, type Problem } from './errors.ts';
@@ -51,6 +51,16 @@ function identity(layer: Layer, canvas: LayerSet['canvas'], donor: Raster, targe
51
51
  export function takeLayerNote(source: TakenLayerSource): string {
52
52
  return `take ${source.run}:${source.tag} from ${source.run_dir}; prompt_id ${source.prompt_id}; seed ${source.seed}; st_input ${source.st_input}; support ${source.support_px} run px; checked ${source.checked_px} input px; differing ${source.differing_px}; alpha > ${OPAQUE_ALPHA_ABOVE}, ${CORE_KERNEL}x${CORE_KERNEL} margin`;
53
53
  }
54
+ /** Intersection of original alpha supports in the common target-run canvas; no bbox or dilation proxy. */
55
+ function supportOverlap(a: Layer, b: Layer): number {
56
+ let px = 0;
57
+ for (let y = Math.max(a.top, b.top); y < Math.min(a.bottom, b.bottom); y++)
58
+ for (let x = Math.max(a.left, b.left); x < Math.min(a.right, b.right); x++) {
59
+ if (a.pixels.data[((y - a.top) * a.pixels.width + x - a.left) * 4 + 3] > OPAQUE_ALPHA_ABOVE &&
60
+ b.pixels.data[((y - b.top) * b.pixels.width + x - b.left) * 4 + 3] > OPAQUE_ALPHA_ABOVE) px++;
61
+ }
62
+ return px;
63
+ }
54
64
  /** Apply every declaration together, so replacing all tied layers from one run can retain its known order. */
55
65
  export function applyTakenLayers(runs: { full: LayerSet; head: LayerSet; single: boolean }, painting: Raster, cfg: Pick<EarlyConfig, 'seethrough' | 'depth'> & { assemble: { take?: TakeLayers[]; plan?: PlanEntry[] } }, configPath: string): { full: LayerSet; head: LayerSet; single: boolean; notes: string[] } {
56
66
  const declarations = cfg.assemble.take ?? [];
@@ -99,7 +109,14 @@ export function applyTakenLayers(runs: { full: LayerSet; head: LayerSet; single:
99
109
  const selected = (cfg.assemble.plan ?? []).filter(p => p[1] === run).map(p => ({ part: p[0], layer: list.find(l => l.name === p[2]) }));
100
110
  for (let i = 0; i < selected.length; i++) for (let j = i + 1; j < selected.length; j++) {
101
111
  const a = selected[i], b = selected[j];
102
- if (a.layer !== undefined && b.layer !== undefined && origin(a.layer) !== origin(b.layer) && list.indexOf(a.layer) > list.indexOf(b.layer)) problems.push({ code: 'ASSEMBLE_TAKE_PLAN_ORDER', object: `${run} plan parts "${a.part}" / "${b.part}"`, detail: `plan order reverses mixed-run depths ${a.layer.depth} / ${b.layer.depth}; put the larger depth behind, as in assemble --propose-plan` });
112
+ if (a.layer === undefined || b.layer === undefined || origin(a.layer) === origin(b.layer)) continue;
113
+ const overlap = supportOverlap(a.layer, b.layer);
114
+ if (overlap === 0) continue;
115
+ // Only full rows in a two-run plan have the proposer's fallback roles.
116
+ // Head rows and one-run plans retain the scalar order, as do peers in a band.
117
+ const band = !runs.single && run === 'full' ? fullPlanBand(a.layer.name) - fullPlanBand(b.layer.name) : 0;
118
+ const reversed = band === 0 ? list.indexOf(a.layer) > list.indexOf(b.layer) : band > 0;
119
+ if (reversed) problems.push({ code: 'ASSEMBLE_TAKE_PLAN_ORDER', object: `${run} plan parts "${a.part}" / "${b.part}"`, detail: `plan order reverses ${band === 0 ? `mixed-run depths ${a.layer.depth} / ${b.layer.depth}; put the larger depth behind` : `two-run full-layer roles "${a.layer.name}" / "${b.layer.name}"; order back hair, body, headwear/earwear, face, head details, front hair`}; supports overlap at ${overlap} run px (alpha > ${OPAQUE_ALPHA_ABOVE}); use assemble --propose-plan` });
103
120
  }
104
121
  refuseIfAny(problems);
105
122
  const replaced = new Set(declarations.filter(d => d.run === run).flatMap(d => d.tags));