rig-c 2.31.0 → 2.31.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/meshreduce.ts +177 -47
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rig-c",
3
- "version": "2.31.0",
3
+ "version": "2.31.1",
4
4
  "description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/meshreduce.ts CHANGED
@@ -430,6 +430,98 @@ function edgeKey(a: number, b: number): string {
430
430
  return a < b ? `${a},${b}` : `${b},${a}`;
431
431
  }
432
432
 
433
+ /**
434
+ * Issue #1307: an undirected edge as one number, `min * n + max`, for ids below `n` — the key every edge set on the
435
+ * removal and boundary-run paths is held in, where `edgeKey` built a string per edge. Exact below 2^53, so for any
436
+ * `n` up to 94,906,265.
437
+ */
438
+ function pairKey(a: number, b: number, n: number): number {
439
+ return a < b ? a * n + b : b * n + a;
440
+ }
441
+
442
+ /**
443
+ * Issue #1307: the edges `fresh`'s triangles hold that no triangle of `old` holds, each once, in the order `fresh`
444
+ * holds them (triangle by triangle, corner k to k + 1) — the order condition (b) reads them in and names the first
445
+ * over `weightJump`. Keyed by number over ids below `n`, and only the keys `fresh` holds are looked for in `old`.
446
+ * `addedEdgesByString` is the construction it replaced, kept as the one it is held equal to.
447
+ */
448
+ function addedEdges(old: ReadonlyArray<readonly number[]>, fresh: ReadonlyArray<readonly number[]>, n: number): Array<[number, number]> {
449
+ const wanted = new Set<number>();
450
+ for (const t of fresh) for (let k = 0; k < 3; k++) wanted.add(pairKey(t[k], t[(k + 1) % 3], n));
451
+ const held = new Set<number>();
452
+ for (const t of old) {
453
+ for (let k = 0; k < 3; k++) {
454
+ const key = pairKey(t[k], t[(k + 1) % 3], n);
455
+ if (wanted.has(key)) held.add(key);
456
+ }
457
+ }
458
+ const added: Array<[number, number]> = [];
459
+ const seen = new Set<number>();
460
+ for (const t of fresh) {
461
+ for (let k = 0; k < 3; k++) {
462
+ const key = pairKey(t[k], t[(k + 1) % 3], n);
463
+ if (held.has(key) || seen.has(key)) continue;
464
+ seen.add(key);
465
+ added.push([t[k], t[(k + 1) % 3]]);
466
+ }
467
+ }
468
+ return added;
469
+ }
470
+
471
+ /** The string-keyed construction `addedEdges` replaced (issue #1307), run only by the edge-set audit. */
472
+ function addedEdgesByString(old: ReadonlyArray<readonly number[]>, fresh: ReadonlyArray<readonly number[]>): Array<[number, number]> {
473
+ const had = new Set<string>();
474
+ for (const t of old) for (let k = 0; k < 3; k++) had.add(edgeKey(t[k], t[(k + 1) % 3]));
475
+ const added: Array<[number, number]> = [];
476
+ const seen = new Set<string>();
477
+ for (const t of fresh) {
478
+ for (let k = 0; k < 3; k++) {
479
+ const key = edgeKey(t[k], t[(k + 1) % 3]);
480
+ if (had.has(key) || seen.has(key)) continue;
481
+ seen.add(key);
482
+ added.push([t[k], t[(k + 1) % 3]]);
483
+ }
484
+ }
485
+ return added;
486
+ }
487
+
488
+ /**
489
+ * Issue #1307's audit, for the `mesh-compare` suite's control: every edge set the removal and boundary-run paths
490
+ * build is built a second time the string-keyed way it was before the issue and compared — the added edges as a list
491
+ * (membership and order, the order condition (b) reads), the triangulation's edge set the protected edges are looked
492
+ * up in as a set (its order is never read). A difference is recorded, not thrown, so the control reads every one.
493
+ * `plant` is the fault applied to the numeric construction before the comparison: one edge dropped from each list
494
+ * and each set, or each added list of two or more reversed. With the audit and no plant, the call's result is the
495
+ * call's without it.
496
+ */
497
+ export interface EdgeSetAudit {
498
+ plant?: 'drop-one' | 'reorder';
499
+ /** Added-edge lists compared, and the edges in them. */
500
+ addedLists: number;
501
+ addedEdges: number;
502
+ /** Lists among them whose order a difference could move — two edges or more. */
503
+ orderedLists: number;
504
+ /** Triangulation edge sets compared (one per operation reaching the protected-edge check), and the edges in them. */
505
+ edgeSets: number;
506
+ edgeSetEdges: number;
507
+ /** Each difference found, named; the first few in full. */
508
+ differences: string[];
509
+ }
510
+
511
+ /** One added-edge list read, through the audit when there is one: the plant applied, then the string construction compared. */
512
+ function auditedAdded(audit: EdgeSetAudit | null, where: string, added: Array<[number, number]>, byString: () => Array<[number, number]>): Array<[number, number]> {
513
+ if (audit === null) return added;
514
+ let read = added;
515
+ if (audit.plant === 'drop-one' && read.length > 0) read = read.slice(0, -1);
516
+ if (audit.plant === 'reorder' && read.length > 1) read = [...read].reverse();
517
+ const want = byString();
518
+ audit.addedLists++;
519
+ audit.addedEdges += want.length;
520
+ if (want.length > 1) audit.orderedLists++;
521
+ if (JSON.stringify(read) !== JSON.stringify(want)) audit.differences.push(`${where}: added edges ${JSON.stringify(read)} against the string construction's ${JSON.stringify(want)}`);
522
+ return read;
523
+ }
524
+
433
525
  // ---------------------------------------------------------------------------
434
526
  // the working mesh — vertices by stable id: source index, then insertion order
435
527
  // ---------------------------------------------------------------------------
@@ -823,7 +915,10 @@ interface Run {
823
915
  keyed: Map<number, { animation: string; ref: AttachmentRef; key: number; time: number }>;
824
916
  protectedVertices: Set<number>;
825
917
  protectedEdges: Array<[number, number]>;
826
- sourceEdges: Set<string>;
918
+ /** The source's edges, `pairKey` over `work.nSource` (issue #1307). */
919
+ sourceEdges: Set<number>;
920
+ /** Issue #1307's audit of the edge sets, or null. */
921
+ edgeAudit: EdgeSetAudit | null;
827
922
  /** The art's rasters, taken once for the call and read by every measurement in it (issue #1240). */
828
923
  rasters: ArtRasters;
829
924
  /** The step state each removal and insertion is measured through, carried from the last measurement (issue #1246); null measures every step in full. */
@@ -1356,8 +1451,17 @@ function refineRegions(run: Run): PhaseEnd {
1356
1451
  // reduction (§1, §6)
1357
1452
  // ---------------------------------------------------------------------------
1358
1453
 
1359
- /** One removal tried: the working triangles after it and the edges it added, or the structural reason it cannot be made. */
1360
- function removalOf(work: Work, v: number): { triangles: Array<[number, number, number]>; added: Array<[number, number]> } | { blocked: string } {
1454
+ /**
1455
+ * One removal tried: the working triangles after it and the edges it added — read through `added()`, built only when
1456
+ * read (issue #1307) — or the structural reason it cannot be made.
1457
+ */
1458
+ type Removal = { triangles: Array<[number, number, number]>; added: () => Array<[number, number]> } | { blocked: string };
1459
+
1460
+ /**
1461
+ * One removal made on `work`'s triangles: the triangles after it — every triangle not touching `v` as it was, the
1462
+ * same tuple, then the hole's new ones, `fresh` — or the structural reason it cannot be made.
1463
+ */
1464
+ function removalStep(work: Work, v: number): { triangles: Array<[number, number, number]>; fresh: Array<[number, number, number]> } | { blocked: string } {
1361
1465
  const star = work.triangles.filter((t) => t.includes(v));
1362
1466
  const next = new Map<number, number>();
1363
1467
  const incoming = new Set<number>();
@@ -1398,19 +1502,16 @@ function removalOf(work: Work, v: number): { triangles: Array<[number, number, n
1398
1502
  } else if (!boundary) {
1399
1503
  return { blocked: `retriangulation: vertex ${v} has fewer than three neighbours` };
1400
1504
  }
1401
- const old = new Set<string>();
1402
- for (const t of work.triangles) for (let k = 0; k < 3; k++) old.add(edgeKey(t[k], t[(k + 1) % 3]));
1403
- const added: Array<[number, number]> = [];
1404
- const seen = new Set<string>();
1405
- for (const t of fresh) {
1406
- for (let k = 0; k < 3; k++) {
1407
- const key = edgeKey(t[k], t[(k + 1) % 3]);
1408
- if (old.has(key) || seen.has(key)) continue;
1409
- seen.add(key);
1410
- added.push([t[k], t[(k + 1) % 3]]);
1411
- }
1412
- }
1413
- return { triangles: [...rest, ...fresh], added };
1505
+ return { triangles: [...rest, ...fresh], fresh };
1506
+ }
1507
+
1508
+ /** `removalStep` with the edges it adds: those of the hole's new triangles no triangle before it holds, in their order. */
1509
+ function removalOf(work: Work, v: number, audit: EdgeSetAudit | null = null): Removal {
1510
+ const step = removalStep(work, v);
1511
+ if ('blocked' in step) return step;
1512
+ const old = work.triangles;
1513
+ const n = work.alive.length;
1514
+ return { triangles: step.triangles, added: () => auditedAdded(audit, `removing ${v}`, addedEdges(old, step.fresh, n), () => addedEdgesByString(old, step.fresh)) };
1414
1515
  }
1415
1516
 
1416
1517
  /** L1 difference of two weight vectors over the bones they name. */
@@ -1447,10 +1548,10 @@ function loadOrder(run: Run): { order: number[]; load: Map<number, number> } {
1447
1548
  const order: number[] = [];
1448
1549
  for (let v = 0; v < work.nSource; v++) {
1449
1550
  if (!work.alive[v] || run.protectedVertices.has(v)) continue;
1450
- const step = removalOf(work, v);
1551
+ const step = removalOf(work, v, run.edgeAudit);
1451
1552
  let most = 0;
1452
1553
  if ('blocked' in step) most = Infinity;
1453
- else for (const [a, b] of step.added) most = Math.max(most, edgeLoad(work, a, b));
1554
+ else for (const [a, b] of step.added()) most = Math.max(most, edgeLoad(work, a, b));
1454
1555
  load.set(v, most);
1455
1556
  order.push(v);
1456
1557
  }
@@ -1470,32 +1571,27 @@ function loadOrder(run: Run): { order: number[]; load: Map<number, number> } {
1470
1571
  * structural reason one of the removals cannot be made. The outline loses the run and gains the chord from the
1471
1572
  * vertex before it to the vertex after it; nothing is measured in between.
1472
1573
  */
1473
- function runRemovalOf(work: Work, vertices: readonly number[]): { triangles: Array<[number, number, number]>; added: Array<[number, number]> } | { blocked: string } {
1574
+ function runRemovalOf(work: Work, vertices: readonly number[], audit: EdgeSetAudit | null = null): Removal {
1474
1575
  const was = work.triangles;
1576
+ // Issue #1307: each removal's own added edges are never read here, so none is built; what the run adds is read off
1577
+ // the triangles its removals made. A triangle of `after` that no removal made is the same tuple as one of `was`
1578
+ // (`removalStep` keeps every untouched triangle as it was), so every edge it holds is old and it adds nothing.
1579
+ const made = new Set<readonly number[]>();
1475
1580
  let after: Array<[number, number, number]>;
1476
1581
  try {
1477
1582
  for (const v of vertices) {
1478
- const step = removalOf(work, v);
1583
+ const step = removalStep(work, v);
1479
1584
  if ('blocked' in step) return step;
1585
+ for (const t of step.fresh) made.add(t);
1480
1586
  work.triangles = step.triangles;
1481
1587
  }
1482
1588
  after = work.triangles;
1483
1589
  } finally {
1484
1590
  work.triangles = was;
1485
1591
  }
1486
- const old = new Set<string>();
1487
- for (const t of was) for (let k = 0; k < 3; k++) old.add(edgeKey(t[k], t[(k + 1) % 3]));
1488
- const added: Array<[number, number]> = [];
1489
- const seen = new Set<string>();
1490
- for (const t of after) {
1491
- for (let k = 0; k < 3; k++) {
1492
- const key = edgeKey(t[k], t[(k + 1) % 3]);
1493
- if (old.has(key) || seen.has(key)) continue;
1494
- seen.add(key);
1495
- added.push([t[k], t[(k + 1) % 3]]);
1496
- }
1497
- }
1498
- return { triangles: after, added };
1592
+ const n = work.alive.length;
1593
+ const label = `removing ${vertices.join(', ')} as one boundary run`;
1594
+ return { triangles: after, added: () => auditedAdded(audit, label, addedEdges(was, after.filter((t) => made.has(t)), n), () => addedEdgesByString(was, after)) };
1499
1595
  }
1500
1596
 
1501
1597
  /**
@@ -1629,6 +1725,28 @@ function deviationFloor(run: Run, mesh: SourceMesh, vertices: readonly number[])
1629
1725
  */
1630
1726
  const FLOOR_MARGIN = 1e-5;
1631
1727
 
1728
+ /**
1729
+ * Issue #1307's audit of the triangulation's edge set (`EdgeSetAudit`): the plant applied to `edges` in place — so
1730
+ * the decision reads the planted set — then the set held to the string-keyed one, member for member.
1731
+ */
1732
+ function auditEdgeSet(audit: EdgeSetAudit, edges: Set<number>, triangles: ReadonlyArray<readonly number[]>, n: number, vertices: readonly number[]): void {
1733
+ if (audit.plant === 'drop-one') {
1734
+ const first = edges.values().next();
1735
+ if (first.done !== true) edges.delete(first.value);
1736
+ }
1737
+ const want = new Set<string>();
1738
+ for (const t of triangles) for (let k = 0; k < 3; k++) want.add(edgeKey(t[k], t[(k + 1) % 3]));
1739
+ audit.edgeSets++;
1740
+ audit.edgeSetEdges += want.size;
1741
+ const missing = [...want].filter((key) => {
1742
+ const [a, b] = key.split(',').map(Number);
1743
+ return !edges.has(pairKey(a, b, n));
1744
+ });
1745
+ if (missing.length > 0 || edges.size !== want.size) {
1746
+ audit.differences.push(`removing ${vertices.join(', ')}: the triangulation's edge set holds ${edges.size} edge(s) against the string construction's ${want.size}${missing.length > 0 ? `, missing ${missing.slice(0, 3).join(' ')}` : ''}`);
1747
+ }
1748
+ }
1749
+
1632
1750
  /**
1633
1751
  * One operation — a single removal, or a boundary run of two or more — tried: null when it was taken (the working
1634
1752
  * mesh then holds it), else what refused it (the working mesh as it was). `full` measures every candidate that
@@ -1637,12 +1755,12 @@ const FLOOR_MARGIN = 1e-5;
1637
1755
  */
1638
1756
  function tryOperation(run: Run, vertices: readonly number[], full: boolean): Refusal | null {
1639
1757
  const { work, input } = run;
1640
- const step = vertices.length === 1 ? removalOf(work, vertices[0]) : runRemovalOf(work, vertices);
1758
+ const step = vertices.length === 1 ? removalOf(work, vertices[0], run.edgeAudit) : runRemovalOf(work, vertices, run.edgeAudit);
1641
1759
  if ('blocked' in step) return step.blocked;
1642
1760
  const jump = input.protect.weightJump;
1643
1761
  if (jump !== null) {
1644
- for (const [a, b] of step.added) {
1645
- if (a < work.nSource && b < work.nSource && run.sourceEdges.has(edgeKey(a, b))) continue;
1762
+ for (const [a, b] of step.added()) {
1763
+ if (a < work.nSource && b < work.nSource && run.sourceEdges.has(pairKey(a, b, work.nSource))) continue;
1646
1764
  const d = weightJump(work, a, b);
1647
1765
  if (d > jump) return `weightJump (b): the new edge ${a}–${b} joins weight vectors ${r6(d)} apart, over ${jump}`;
1648
1766
  }
@@ -1654,12 +1772,17 @@ function tryOperation(run: Run, vertices: readonly number[], full: boolean): Ref
1654
1772
  work.triangles = was.triangles;
1655
1773
  for (const v of vertices) work.alive[v] = true;
1656
1774
  };
1657
- const edges = new Set<string>();
1658
- for (const t of work.triangles) for (let k = 0; k < 3; k++) edges.add(edgeKey(t[k], t[(k + 1) % 3]));
1659
- for (const [a, b] of run.protectedEdges) {
1660
- if (!edges.has(edgeKey(a, b))) {
1661
- undo();
1662
- return `protect (a): the protected source edge ${a}–${b} is no longer an edge`;
1775
+ // Issue #1307: the triangulation's edge set is built only when a protected edge is looked up in it, keyed by number.
1776
+ if (run.protectedEdges.length > 0) {
1777
+ const n = work.alive.length;
1778
+ const edges = new Set<number>();
1779
+ for (const t of work.triangles) for (let k = 0; k < 3; k++) edges.add(pairKey(t[k], t[(k + 1) % 3], n));
1780
+ if (run.edgeAudit !== null) auditEdgeSet(run.edgeAudit, edges, work.triangles, n, vertices);
1781
+ for (const [a, b] of run.protectedEdges) {
1782
+ if (!edges.has(pairKey(a, b, n))) {
1783
+ undo();
1784
+ return `protect (a): the protected source edge ${a}–${b} is no longer an edge`;
1785
+ }
1663
1786
  }
1664
1787
  }
1665
1788
  let canon: Canonical;
@@ -1841,7 +1964,7 @@ function delaunayFlips(run: Run, plant: ReductionPlant | null): FlipResult {
1841
1964
  const sine = oppositeAngleSine(P[a], P[b], P[c], P[d]);
1842
1965
  if (against ? !(sine > FLIP_MARGIN) : !(sine < -FLIP_MARGIN)) continue;
1843
1966
  if (held(a, b) || held(c, d)) continue;
1844
- if (jump !== null && !(c < work.nSource && d < work.nSource && run.sourceEdges.has(edgeKey(c, d))) && weightJump(work, c, d) > jump) continue;
1967
+ if (jump !== null && !(c < work.nSource && d < work.nSource && run.sourceEdges.has(pairKey(c, d, work.nSource))) && weightJump(work, c, d) > jump) continue;
1845
1968
  if (flips >= maxFlips) return { bound: `the flip bound n(n - 1)/2 = ${maxFlips} over ${alive} vertices was reached`, flips, sweeps };
1846
1969
  tris[i] = [a, d, c];
1847
1970
  tris[j] = [d, b, c];
@@ -1961,12 +2084,13 @@ export function reduceMeshWith(
1961
2084
  plant: ReductionPlant | null = null,
1962
2085
  observe: AttemptObserver | null = null,
1963
2086
  skinning: SkinningHooks | null = null,
2087
+ edgeAudit: EdgeSetAudit | null = null,
1964
2088
  ): MeshReductionResult {
1965
2089
  validateReduction(input, plant);
1966
2090
  if (steps !== null && steps.rasters !== rasters) {
1967
2091
  refuse('REDUCE_ART_RASTERS_MISMATCH', `attachment ${nameOf(input.attachment)}: the step rasters were made over another rasters object; required step rasters made over the rasters passed beside them (stepRastersOf(rasters))`);
1968
2092
  }
1969
- return reduceValidated(input, rasters, steps, plant, observe, skinning);
2093
+ return reduceValidated(input, rasters, steps, plant, observe, skinning, edgeAudit);
1970
2094
  }
1971
2095
 
1972
2096
  /**
@@ -1980,6 +2104,7 @@ function reduceValidated(
1980
2104
  plant: ReductionPlant | null = null,
1981
2105
  observe: AttemptObserver | null = null,
1982
2106
  hooks: SkinningHooks | null = null,
2107
+ edgeAudit: EdgeSetAudit | null = null,
1983
2108
  ): MeshReductionResult {
1984
2109
  const who = `attachment ${nameOf(input.attachment)}`;
1985
2110
  const src = input.source;
@@ -2047,8 +2172,12 @@ function reduceValidated(
2047
2172
  sourceTriangle: new Map(),
2048
2173
  };
2049
2174
  for (let i = 0; i + 2 < src.triangles.length; i += 3) work.triangles.push([src.triangles[i], src.triangles[i + 1], src.triangles[i + 2]]);
2050
- const sourceEdges = new Set<string>();
2051
- for (const t of work.triangles) for (let k = 0; k < 3; k++) sourceEdges.add(edgeKey(t[k], t[(k + 1) % 3]));
2175
+ // The string-keyed set is read once, by `protectionOf`, whose weightJump edges follow its sorted keys; every step
2176
+ // reads the numeric one (issue #1307).
2177
+ const sourceEdgeNames = new Set<string>();
2178
+ for (const t of work.triangles) for (let k = 0; k < 3; k++) sourceEdgeNames.add(edgeKey(t[k], t[(k + 1) % 3]));
2179
+ const sourceEdges = new Set<number>();
2180
+ for (const t of work.triangles) for (let k = 0; k < 3; k++) sourceEdges.add(pairKey(t[k], t[(k + 1) % 3], work.nSource));
2052
2181
  const run: Run = {
2053
2182
  input,
2054
2183
  who,
@@ -2062,8 +2191,9 @@ function reduceValidated(
2062
2191
  observe,
2063
2192
  tally: { sharesDroppedOnGrid: 0, sharesPruned: 0 },
2064
2193
  keyed: keyedSourceVertices(input),
2065
- ...protectionOf(input, work, sourceEdges),
2194
+ ...protectionOf(input, work, sourceEdgeNames),
2066
2195
  sourceEdges,
2196
+ edgeAudit,
2067
2197
  rasters,
2068
2198
  stepRasters: steps,
2069
2199
  skinning: null,