@rig-c/core 2.65.0 → 2.65.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 +18 -2
- package/docs/MOTION.md +5 -1
- package/package.json +1 -1
- package/src/pose.ts +134 -12
package/docs/AUTHORING.md
CHANGED
|
@@ -11365,10 +11365,23 @@ ambiguous one ends with a sentence saying which reading its figures support:
|
|
|
11365
11365
|
|
|
11366
11366
|
> loud.png: the best placement found has residual 0.5000, above --max-residual 0.25; part is 200x150 px (span 200 frame px, opaque 1) with texture 0.2473, detail 49.455; 2 candidate(s), best 0.5000, next 0.5000, spread 0.0000 — above the measured floor (AUTHORING §11.5: detail 0.5, measured from 24 px up), so size and texture do not explain this; the cut (or the search window) is the suspect
|
|
11367
11367
|
|
|
11368
|
+
- **above the floor, one size in two places** — two copies of an 18x34 checker 0.6 of its span and 6° apart, the shape a left/right pair cut from one drawing reads as:
|
|
11369
|
+
|
|
11370
|
+
> part is 18x34 px (span 36.2 frame px, opaque 1) with texture 0.0221, detail 0.752; 14 candidate(s), best 0.0160, next 0.0162, spread 0.0002 — above the measured floor (AUTHORING §11.5: detail 0.5, measured from 24 px up), so size and texture do not explain this; every placement in the margin is at the best's scale (within 0.30%, bar 2%) and in another place (the nearest 0.62 span(s) away, bar 0.45), which is the part — or a near-copy, such as a left/right pair cut from one drawing — drawn more than once: read the placements as an unordered set and order it across frames (MOTION.md §2.2 🔀); a --rotation window that excludes one place chooses the answer rather than measuring it
|
|
11371
|
+
|
|
11368
11372
|
Read the figures, not only the verdict. *Below* says plainness alone explains the
|
|
11369
11373
|
failure: the cut may be right, and `pose` cannot tell. *Above* says it does not,
|
|
11370
11374
|
so look at the cut, the `--scale`/`--rotation` window, or — for an ambiguity — at
|
|
11371
|
-
whether the frame really holds the part twice.
|
|
11375
|
+
whether the frame really holds the part twice. 🔀 When **every** placement inside
|
|
11376
|
+
the margin is at the best's scale and in another place — the two bars the sentence
|
|
11377
|
+
prints beside the figures it measured — the reading says the frame does hold it more than once and
|
|
11378
|
+
names MOTION.md §2.2's unordered set instead of a window: no `--scale` window
|
|
11379
|
+
separates two copies of one size, and a `--rotation` window that excludes one place
|
|
11380
|
+
*chooses* the answer — both files then place on the other copy, one of them off its
|
|
11381
|
+
truth and printed clean. Both bars sit at the geometric middle of the gap measured
|
|
11382
|
+
between such repeats and every other ambiguity on the floor's grid (the constants
|
|
11383
|
+
`REPEAT_SCALE_RATIO` and `REPEAT_DISTANCE_SPANS` in `src/pose.ts` carry the figures);
|
|
11384
|
+
any other ambiguity above the floor keeps the window sentence. ⚠️ Above the floor is **not** a
|
|
11372
11385
|
promise that `pose` will place the part (see the rates below).
|
|
11373
11386
|
|
|
11374
11387
|
**The texture figure.** `legibility.texture` is the mean colour step between
|
|
@@ -11513,7 +11526,10 @@ cutoff, and `PO26` holds every polish to strictly descending residuals, read thr
|
|
|
11513
11526
|
the same trace. `PO31` places §6's post at the old placement from the page's own
|
|
11514
11527
|
input bytes, and `PO32` holds it to taking the quarter-rung escape and reading the same twice.
|
|
11515
11528
|
`PO33` holds the coarse row the tool reads rotations from to the field each seed was
|
|
11516
|
-
picked from.
|
|
11529
|
+
picked from. `PO34` holds two copies of one part 0.6 span apart to both coming back — and is
|
|
11530
|
+
seen red on the quarter-span grid releases before 1.4.0 searched on, which put them on
|
|
11531
|
+
one placement — and `PO35` holds the two readings of an ambiguity above the floor to
|
|
11532
|
+
their own shapes, each with a forged alternate that moves it to the other.
|
|
11517
11533
|
|
|
11518
11534
|
## 12. Reading the half of that picture `pose` refuses — `rigc chainfit`
|
|
11519
11535
|
|
package/docs/MOTION.md
CHANGED
|
@@ -234,7 +234,11 @@ frames is your job.** `alternates` non-empty means the answer was not unique;
|
|
|
234
234
|
`ambiguous` means at least one alternate is inside the margin. Two identical limbs
|
|
235
235
|
look exactly like that, and so does a shape whose silhouette fits itself at more than
|
|
236
236
|
one angle. The instrument has run out — it sees one frame and has no notion of which
|
|
237
|
-
limb is which. A
|
|
237
|
+
limb is which. A left/right pair cut from one drawing is two files that each fit both
|
|
238
|
+
places, so each comes back with both, and narrowing `--scale` or `--rotation` until one
|
|
239
|
+
place is left picks the answer — it does not read it; the `ambiguous:` reading names
|
|
240
|
+
this route instead of a window when every placement in the margin is one size in
|
|
241
|
+
another place (AUTHORING §11.5). A method that works:
|
|
238
242
|
|
|
239
243
|
1. **Enumerate the assignments, not the placements.** With k interchangeable
|
|
240
244
|
placements of one part in frame A and k in frame B, there are k! ways to pair them.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/core",
|
|
3
|
-
"version": "2.65.
|
|
3
|
+
"version": "2.65.1",
|
|
4
4
|
"description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/pose.ts
CHANGED
|
@@ -185,7 +185,7 @@ export const COARSE_STRIDE_FRACTION = 0.125;
|
|
|
185
185
|
* trials found rather than 226, and 12 that placed on the old grid lost rather
|
|
186
186
|
* than 9 (both at three minima per rung).
|
|
187
187
|
*/
|
|
188
|
-
const COARSE_SUPPRESSION_CELLS = 4;
|
|
188
|
+
export const COARSE_SUPPRESSION_CELLS = 4;
|
|
189
189
|
|
|
190
190
|
/** How many scale rungs one octave gets in the coarse ladder. */
|
|
191
191
|
export const SCALE_STEPS_PER_OCTAVE = 3;
|
|
@@ -254,7 +254,27 @@ export const BACKGROUND_BORDER_SHARE = 0.6;
|
|
|
254
254
|
* carries it: 227 trials found against 226 at three, none lost, and a CPU cost
|
|
255
255
|
* inside the grid's run-to-run noise.
|
|
256
256
|
*/
|
|
257
|
-
const MINIMA_PER_SCALE = 5;
|
|
257
|
+
export const MINIMA_PER_SCALE = 5;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The three numbers that decide which basins the coarse pass sends down —
|
|
261
|
+
* `COARSE_STRIDE_FRACTION`, `COARSE_SUPPRESSION_CELLS`, `MINIMA_PER_SCALE` — as
|
|
262
|
+
* one value, so a control can run the search on the grid an earlier release
|
|
263
|
+
* used and show what the current one buys (issue #1519). See
|
|
264
|
+
* `PoseOptions.coarseGrid`.
|
|
265
|
+
*/
|
|
266
|
+
export interface PoseCoarseGrid {
|
|
267
|
+
strideFraction: number;
|
|
268
|
+
suppressionCells: number;
|
|
269
|
+
minimaPerScale: number;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The grid every caller gets — the three constants above, unchanged. */
|
|
273
|
+
export const COARSE_GRID: PoseCoarseGrid = {
|
|
274
|
+
strideFraction: COARSE_STRIDE_FRACTION,
|
|
275
|
+
suppressionCells: COARSE_SUPPRESSION_CELLS,
|
|
276
|
+
minimaPerScale: MINIMA_PER_SCALE,
|
|
277
|
+
};
|
|
258
278
|
|
|
259
279
|
/**
|
|
260
280
|
* How many candidates survive each refinement level.
|
|
@@ -387,6 +407,64 @@ export function floorClause(): string {
|
|
|
387
407
|
return `detail ${POSE_FLOOR.detail}, measured from ${POSE_FLOOR.span} px up`;
|
|
388
408
|
}
|
|
389
409
|
|
|
410
|
+
/**
|
|
411
|
+
* When an ambiguity is ONE part fitting several places at one size — the
|
|
412
|
+
* reading a left/right pair cut from one drawing gives, which no window
|
|
413
|
+
* resolves (issue #1519).
|
|
414
|
+
*
|
|
415
|
+
* Both bars are read off the measured gap between the two populations, and
|
|
416
|
+
* each sits at that gap's geometric middle — the same rule for both, so neither
|
|
417
|
+
* is a number chosen to make one case come out:
|
|
418
|
+
*
|
|
419
|
+
* - **Scale**, as `|alternate / best − 1|` and over EVERY alternate inside the
|
|
420
|
+
* margin. Same-size repeats measured 0.00001–0.00034 (a pair of legs and a pair
|
|
421
|
+
* of ears in a picture rendered from a rig), 0.0030 (the selftest's twin
|
|
422
|
+
* fixture, PO34) and 0.0055 (the pose fixture's mirrored arms, PO02). Every
|
|
423
|
+
* other ambiguous part above the floor on `tools/pose_floor.ts`'s 420 trials
|
|
424
|
+
* had an alternate at least 0.0719 off its best's scale — two scales at one
|
|
425
|
+
* spot — and so did a pair of eyes read at 1.000 and 0.922. √(0.0055 × 0.0719)
|
|
426
|
+
* ≈ 0.020.
|
|
427
|
+
* - **Distance**, from the best's centre to the nearest alternate's, over the
|
|
428
|
+
* part's span at the best's scale. On that grid every other alternate above
|
|
429
|
+
* the floor sat within 0.336 span of its best — two readings of one spot — or
|
|
430
|
+
* 2.26 spans or more away at a scale more than 20% off, which the scale bar
|
|
431
|
+
* already separates; the closest repeat measured was the legs at 0.603.
|
|
432
|
+
* √(0.336 × 0.603) ≈ 0.45.
|
|
433
|
+
*
|
|
434
|
+
* ⚠️ One grid trial above the floor takes this branch and is not a repeat: a
|
|
435
|
+
* blurred 48 px shin whose four placements all stopped on the scale FLOOR of the
|
|
436
|
+
* default window, at 0.5 against a truth at 1.0. Every placement on one wall has
|
|
437
|
+
* one scale by construction. Its console line marks the wall beside the value,
|
|
438
|
+
* which is the first thing to move; nothing in the alternates alone separates it
|
|
439
|
+
* from a repeat — a pair of ears in the same picture also sat on a scale floor.
|
|
440
|
+
*/
|
|
441
|
+
export const REPEAT_SCALE_RATIO = 0.02;
|
|
442
|
+
export const REPEAT_DISTANCE_SPANS = 0.45;
|
|
443
|
+
|
|
444
|
+
/** How an ambiguity's placements stand to the best one, when they are one part in several places at one size. */
|
|
445
|
+
export interface PoseRepeat {
|
|
446
|
+
/** The largest `|alternate.scale / best.scale − 1|` inside the margin. */
|
|
447
|
+
scaleRatio: number;
|
|
448
|
+
/** The nearest alternate's centre distance from the best's, in spans at the best's scale. */
|
|
449
|
+
spans: number;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Whether every alternate inside the margin is a DIFFERENT place at the SAME
|
|
454
|
+
* scale as the best, against `REPEAT_SCALE_RATIO` and `REPEAT_DISTANCE_SPANS`
|
|
455
|
+
* — `null` when any one is not, or there is none. `span` is the part's longest
|
|
456
|
+
* material side in frame pixels at the best placement's scale.
|
|
457
|
+
*
|
|
458
|
+
* ⭐ Exported so the selftest can hand it a forged alternate and watch the
|
|
459
|
+
* reading change branch; the search calls it with exactly what it reported.
|
|
460
|
+
*/
|
|
461
|
+
export function repeatedPlaces(best: PosePlacement, alternates: readonly PosePlacement[], span: number): PoseRepeat | null {
|
|
462
|
+
if (alternates.length === 0 || !(span > 0)) return null;
|
|
463
|
+
const scaleRatio = Math.max(...alternates.map((a) => Math.abs(a.scale / best.scale - 1)));
|
|
464
|
+
const spans = Math.min(...alternates.map((a) => hypot2(a.x - best.x, a.y - best.y) / span));
|
|
465
|
+
return scaleRatio <= REPEAT_SCALE_RATIO && spans >= REPEAT_DISTANCE_SPANS ? { scaleRatio, spans } : null;
|
|
466
|
+
}
|
|
467
|
+
|
|
390
468
|
/**
|
|
391
469
|
* The sentence that tells "the part is wrong" from "the part is too small or too
|
|
392
470
|
* plain for `pose`", with every figure it rests on (issue #857).
|
|
@@ -394,7 +472,11 @@ export function floorClause(): string {
|
|
|
394
472
|
* ⭐ Exported for the reason `windowEdgeNote` is: the guide quotes it, and a
|
|
395
473
|
* message and the document teaching it must be built from one place.
|
|
396
474
|
*/
|
|
397
|
-
export function legibilityReading(
|
|
475
|
+
export function legibilityReading(
|
|
476
|
+
l: Omit<PoseLegibility, 'reading'>,
|
|
477
|
+
verdict: 'refused' | 'ambiguous',
|
|
478
|
+
repeat: PoseRepeat | null = null,
|
|
479
|
+
): string {
|
|
398
480
|
const figures =
|
|
399
481
|
`part is ${l.width}x${l.height} px (span ${l.span} frame px, opaque ${l.opaqueShare}) with texture ${l.texture}, ` +
|
|
400
482
|
`detail ${l.detail}; ` +
|
|
@@ -425,12 +507,27 @@ export function legibilityReading(l: Omit<PoseLegibility, 'reading'>, verdict: '
|
|
|
425
507
|
// blurred goggles and gun from 128 px and rear shin at 48 px — five of them
|
|
426
508
|
// 2.1–2.7 px off, the rear shin 19 px. So the sentence names that reading
|
|
427
509
|
// too, with the one remedy that separates it: a window.
|
|
510
|
+
//
|
|
511
|
+
// 🔀 Except when the placements are one size in different places (issue
|
|
512
|
+
// #1519). Measured on a picture holding a left/right pair cut from one
|
|
513
|
+
// drawing: no --scale window separated them, narrower or wider, because they
|
|
514
|
+
// ARE one scale, and every --rotation window that "resolved" them did so by
|
|
515
|
+
// excluding one place — after which both files placed on the other, one of
|
|
516
|
+
// them 69 px off its truth and printed clean. There the window does not tell
|
|
517
|
+
// the two readings apart, it picks one, so the sentence names the route that
|
|
518
|
+
// does work instead.
|
|
519
|
+
const ambiguity =
|
|
520
|
+
repeat === null
|
|
521
|
+
? 'either the frame holds more than one place this part fits, or every candidate missed the true one — ' +
|
|
522
|
+
'a narrower --scale or --rotation window around what you know of the part tells the two apart'
|
|
523
|
+
: `every placement in the margin is at the best's scale (within ${(repeat.scaleRatio * 100).toFixed(2)}%, ` +
|
|
524
|
+
`bar ${REPEAT_SCALE_RATIO * 100}%) and in another place (the nearest ${repeat.spans.toFixed(2)} span(s) away, ` +
|
|
525
|
+
`bar ${REPEAT_DISTANCE_SPANS}), which is the part — or a near-copy, such as a left/right pair cut from one ` +
|
|
526
|
+
'drawing — drawn more than once: read the placements as an unordered set and order it across frames ' +
|
|
527
|
+
'(MOTION.md §2.2 🔀); a --rotation window that excludes one place chooses the answer rather than measuring it';
|
|
428
528
|
return (
|
|
429
529
|
`${figures} — above the measured floor (AUTHORING §11.5: ${floorClause()}), so size and texture do not explain this; ` +
|
|
430
|
-
(verdict === 'refused'
|
|
431
|
-
? 'the cut (or the search window) is the suspect'
|
|
432
|
-
: 'either the frame holds more than one place this part fits, or every candidate missed the true one — ' +
|
|
433
|
-
'a narrower --scale or --rotation window around what you know of the part tells the two apart')
|
|
530
|
+
(verdict === 'refused' ? 'the cut (or the search window) is the suspect' : ambiguity)
|
|
434
531
|
);
|
|
435
532
|
}
|
|
436
533
|
|
|
@@ -664,6 +761,19 @@ export interface PoseOptions {
|
|
|
664
761
|
* level of every part's refinement is appended to it. See `PoseTrace`.
|
|
665
762
|
*/
|
|
666
763
|
trace?: PoseTrace;
|
|
764
|
+
/**
|
|
765
|
+
* The coarse grid to search on — default `COARSE_GRID`, which is what the CLI
|
|
766
|
+
* always uses and has no flag to change.
|
|
767
|
+
*
|
|
768
|
+
* ⚠️ A plant, not a tuning knob. It exists so the selftest can run the search
|
|
769
|
+
* on the quarter-span grid releases before 1.4.0 used and watch a control go
|
|
770
|
+
* red there: two near-identical parts 0.6 of a span apart came back as ONE
|
|
771
|
+
* placement on that grid, printed `PLACE` with no alternate (issue #1519). A
|
|
772
|
+
* control nobody has seen fail holds nothing, and the only honest way to see
|
|
773
|
+
* this one fail is to search on the grid that failed. The report states the
|
|
774
|
+
* stride fraction in force in `search.coarse.strideFraction`.
|
|
775
|
+
*/
|
|
776
|
+
coarseGrid?: PoseCoarseGrid;
|
|
667
777
|
}
|
|
668
778
|
|
|
669
779
|
/** A search candidate as the trace states it: where the part image's centre would land, frame pixels. */
|
|
@@ -1742,7 +1852,7 @@ export function estimatePose(options: PoseOptions): PoseReport {
|
|
|
1742
1852
|
coarse: {
|
|
1743
1853
|
frameLongSide: COARSE_LONG_SIDE,
|
|
1744
1854
|
partSpan: COARSE_PART_SPAN,
|
|
1745
|
-
strideFraction:
|
|
1855
|
+
strideFraction: (options.coarseGrid ?? COARSE_GRID).strideFraction,
|
|
1746
1856
|
framePyramid: framePyramid.length,
|
|
1747
1857
|
},
|
|
1748
1858
|
maxResidual,
|
|
@@ -1787,6 +1897,7 @@ export function estimatePose(options: PoseOptions): PoseReport {
|
|
|
1787
1897
|
maxResidual,
|
|
1788
1898
|
{ min: scaleMin, max: scaleMax },
|
|
1789
1899
|
rotationBounds,
|
|
1900
|
+
options.coarseGrid ?? COARSE_GRID,
|
|
1790
1901
|
options.trace,
|
|
1791
1902
|
),
|
|
1792
1903
|
);
|
|
@@ -1804,6 +1915,7 @@ function placePart(
|
|
|
1804
1915
|
maxResidual: number,
|
|
1805
1916
|
scaleBounds: { min: number; max: number },
|
|
1806
1917
|
rotationBounds: { min: number; max: number; wraps: boolean },
|
|
1918
|
+
coarseGrid: PoseCoarseGrid,
|
|
1807
1919
|
trace?: PoseTrace,
|
|
1808
1920
|
): PosePart {
|
|
1809
1921
|
const scaleMin = scaleBounds.min;
|
|
@@ -1938,14 +2050,14 @@ function placePart(
|
|
|
1938
2050
|
}
|
|
1939
2051
|
const coarse = levels[coarseIndex];
|
|
1940
2052
|
const spanAtCoarse = (span * scaleReference) / coarse.reduction;
|
|
1941
|
-
const stride = Math.max(1, Math.round(spanAtCoarse *
|
|
2053
|
+
const stride = Math.max(1, Math.round(spanAtCoarse * coarseGrid.strideFraction));
|
|
1942
2054
|
const coarseSamples = samplesFor(coarse.reduction, COARSE_SAMPLES);
|
|
1943
2055
|
let candidates: Candidate[] = [];
|
|
1944
2056
|
let grid = { reduction: coarse.reduction, cols: 0, rows: 0, stride };
|
|
1945
2057
|
for (const scale of scales) {
|
|
1946
2058
|
const field = coarseScan(coarse, coarseSamples, scale, searchRotations, stride);
|
|
1947
2059
|
grid = { reduction: coarse.reduction, cols: field.cols, rows: field.rows, stride };
|
|
1948
|
-
candidates.push(...localMinima(field,
|
|
2060
|
+
candidates.push(...localMinima(field, coarseGrid.suppressionCells, coarseGrid.minimaPerScale));
|
|
1949
2061
|
}
|
|
1950
2062
|
base.coarse = grid;
|
|
1951
2063
|
candidates.sort((a, b) => a.residual - b.residual);
|
|
@@ -1976,7 +2088,7 @@ function placePart(
|
|
|
1976
2088
|
const plate = plates[li];
|
|
1977
2089
|
const smooth = li !== coarseIndex;
|
|
1978
2090
|
const branch = li === branchLevel;
|
|
1979
|
-
const keep = li === coarseIndex ? scales.length *
|
|
2091
|
+
const keep = li === coarseIndex ? scales.length * coarseGrid.minimaPerScale : REFINE_CANDIDATES;
|
|
1980
2092
|
const s = samplesFor(level.reduction, li === 0 ? POLISH_SAMPLES : REFINE_SAMPLES);
|
|
1981
2093
|
// The coarse pass only sampled every `stride` pixels, so entering the
|
|
1982
2094
|
// refinement the anchor can be half a stride out; the first polish gets a
|
|
@@ -2235,7 +2347,17 @@ function placePart(
|
|
|
2235
2347
|
// move, and this sentence is about what moving it cannot change.
|
|
2236
2348
|
if (base.refusal !== null || base.ambiguous) {
|
|
2237
2349
|
const verdict = base.refusal !== null ? 'refused' : 'ambiguous';
|
|
2238
|
-
|
|
2350
|
+
// The whole margin rather than the alternates kept, so a repeat is not
|
|
2351
|
+
// read off a list `MAX_ALTERNATES` truncated.
|
|
2352
|
+
const repeat =
|
|
2353
|
+
verdict === 'ambiguous'
|
|
2354
|
+
? repeatedPlaces(
|
|
2355
|
+
best,
|
|
2356
|
+
close.map((d) => d.placement),
|
|
2357
|
+
span * best.scale,
|
|
2358
|
+
)
|
|
2359
|
+
: null;
|
|
2360
|
+
const reading = legibilityReading(base.legibility, verdict, repeat);
|
|
2239
2361
|
base.legibility.reading = reading;
|
|
2240
2362
|
if (base.refusal !== null) base.refusal.detail += `; ${reading}`;
|
|
2241
2363
|
base.notes.push(`${name}: ${reading}.`);
|