rig-c 2.31.0 → 2.31.2
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/package.json +1 -1
- package/src/meshreduce.ts +379 -86
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-c",
|
|
3
|
-
"version": "2.31.
|
|
3
|
+
"version": "2.31.2",
|
|
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
|
// ---------------------------------------------------------------------------
|
|
@@ -467,18 +559,7 @@ interface Canonical {
|
|
|
467
559
|
* 5. bindings strongest first, ties by the bone's position in `boneOrder`.
|
|
468
560
|
*/
|
|
469
561
|
function canonicalise(work: Work, sourceTurn: number, boneRank: Map<string, number>): Canonical {
|
|
470
|
-
const ids
|
|
471
|
-
for (let id = 0; id < work.alive.length; id++) if (work.alive[id]) ids.push(id);
|
|
472
|
-
const compact = new Map<number, number>();
|
|
473
|
-
ids.forEach((id, i) => compact.set(id, i));
|
|
474
|
-
const flat: number[] = [];
|
|
475
|
-
for (const tri of work.triangles) for (const id of tri) flat.push(compact.get(id)!);
|
|
476
|
-
const outline = traceOutline(ids.length, flat);
|
|
477
|
-
let walk = outline.walk.map((i) => ids[i]);
|
|
478
|
-
const start = walk.indexOf(Math.min(...walk));
|
|
479
|
-
walk = [...walk.slice(start), ...walk.slice(0, start)];
|
|
480
|
-
const turn = Math.sign(signedArea(walk.map((id) => [work.pos[id][0], work.pos[id][1]])));
|
|
481
|
-
if (turn !== sourceTurn) walk = [walk[0], ...walk.slice(1).reverse()];
|
|
562
|
+
const { ids, walk } = canonicalWalk(work, sourceTurn);
|
|
482
563
|
const onHull = new Set(walk);
|
|
483
564
|
const interiorSource = ids.filter((id) => !onHull.has(id) && id < work.nSource);
|
|
484
565
|
const interiorInserted = ids
|
|
@@ -504,6 +585,34 @@ function canonicalise(work: Work, sourceTurn: number, boneRank: Map<string, numb
|
|
|
504
585
|
return { mesh, order, indexOf };
|
|
505
586
|
}
|
|
506
587
|
|
|
588
|
+
/**
|
|
589
|
+
* Step 1 of the canonical order alone: the surviving ids, and the outline walked from its smallest id the way the
|
|
590
|
+
* source's own hull listing turns — `canonicalise`'s hull, and the walk the early deviation floor (issue #1309) reads
|
|
591
|
+
* the current outline as. A `MeshError` when the triangles are not one closed outline.
|
|
592
|
+
*/
|
|
593
|
+
function canonicalWalk(work: Work, sourceTurn: number): { ids: number[]; walk: number[] } {
|
|
594
|
+
const ids: number[] = [];
|
|
595
|
+
for (let id = 0; id < work.alive.length; id++) if (work.alive[id]) ids.push(id);
|
|
596
|
+
const compact = new Map<number, number>();
|
|
597
|
+
ids.forEach((id, i) => compact.set(id, i));
|
|
598
|
+
const flat: number[] = [];
|
|
599
|
+
for (const tri of work.triangles) for (const id of tri) flat.push(compact.get(id)!);
|
|
600
|
+
const outline = traceOutline(ids.length, flat);
|
|
601
|
+
return { ids, walk: turnedFromSmallest(work, outline.walk.map((i) => ids[i]), sourceTurn) };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* A closed walk of working ids rotated to start at its smallest id and, when its signed area does not turn the way
|
|
606
|
+
* `sourceTurn` does, walked the other way from that id — the one rule `canonicalise` orders its hull by, and the one
|
|
607
|
+
* the early deviation floor turns its predicted outline by (issue #1309), so the two read one polygon.
|
|
608
|
+
*/
|
|
609
|
+
function turnedFromSmallest(work: Work, loop: readonly number[], sourceTurn: number): number[] {
|
|
610
|
+
const start = loop.indexOf(Math.min(...loop));
|
|
611
|
+
const walk = [...loop.slice(start), ...loop.slice(0, start)];
|
|
612
|
+
const turn = Math.sign(signedArea(walk.map((id) => [work.pos[id][0], work.pos[id][1]])));
|
|
613
|
+
return turn !== sourceTurn ? [walk[0], ...walk.slice(1).reverse()] : walk;
|
|
614
|
+
}
|
|
615
|
+
|
|
507
616
|
/** Strongest first, ties by the bone's place in `boneOrder` — the values themselves untouched. */
|
|
508
617
|
function sortBindings(list: readonly Binding[], boneRank: Map<string, number>): Binding[] {
|
|
509
618
|
return list.map((b) => ({ bone: b.bone, weight: b.weight })).sort((p, q) => q.weight - p.weight || (boneRank.get(p.bone) ?? 0) - (boneRank.get(q.bone) ?? 0));
|
|
@@ -823,7 +932,14 @@ interface Run {
|
|
|
823
932
|
keyed: Map<number, { animation: string; ref: AttachmentRef; key: number; time: number }>;
|
|
824
933
|
protectedVertices: Set<number>;
|
|
825
934
|
protectedEdges: Array<[number, number]>;
|
|
826
|
-
|
|
935
|
+
/** The source's edges, `pairKey` over `work.nSource` (issue #1307). */
|
|
936
|
+
sourceEdges: Set<number>;
|
|
937
|
+
/** Issue #1307's audit of the edge sets, or null. */
|
|
938
|
+
edgeAudit: EdgeSetAudit | null;
|
|
939
|
+
/** Issue #1309's audit of the early deviation floor against the late one, or null. */
|
|
940
|
+
floorAudit: FloorAudit | null;
|
|
941
|
+
/** The current outline's canonical walk, for the working triangles it was read off (issue #1309); null until read. */
|
|
942
|
+
walk: { triangles: Array<[number, number, number]>; walk: number[] } | null;
|
|
827
943
|
/** The art's rasters, taken once for the call and read by every measurement in it (issue #1240). */
|
|
828
944
|
rasters: ArtRasters;
|
|
829
945
|
/** 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 +1472,17 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
1356
1472
|
// reduction (§1, §6)
|
|
1357
1473
|
// ---------------------------------------------------------------------------
|
|
1358
1474
|
|
|
1359
|
-
/**
|
|
1360
|
-
|
|
1475
|
+
/**
|
|
1476
|
+
* One removal tried: the working triangles after it and the edges it added — read through `added()`, built only when
|
|
1477
|
+
* read (issue #1307) — or the structural reason it cannot be made.
|
|
1478
|
+
*/
|
|
1479
|
+
type Removal = { triangles: Array<[number, number, number]>; added: () => Array<[number, number]> } | { blocked: string };
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* One removal made on `work`'s triangles: the triangles after it — every triangle not touching `v` as it was, the
|
|
1483
|
+
* same tuple, then the hole's new ones, `fresh` — or the structural reason it cannot be made.
|
|
1484
|
+
*/
|
|
1485
|
+
function removalStep(work: Work, v: number): { triangles: Array<[number, number, number]>; fresh: Array<[number, number, number]> } | { blocked: string } {
|
|
1361
1486
|
const star = work.triangles.filter((t) => t.includes(v));
|
|
1362
1487
|
const next = new Map<number, number>();
|
|
1363
1488
|
const incoming = new Set<number>();
|
|
@@ -1398,19 +1523,16 @@ function removalOf(work: Work, v: number): { triangles: Array<[number, number, n
|
|
|
1398
1523
|
} else if (!boundary) {
|
|
1399
1524
|
return { blocked: `retriangulation: vertex ${v} has fewer than three neighbours` };
|
|
1400
1525
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
return { triangles: [...rest, ...fresh], added };
|
|
1526
|
+
return { triangles: [...rest, ...fresh], fresh };
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/** `removalStep` with the edges it adds: those of the hole's new triangles no triangle before it holds, in their order. */
|
|
1530
|
+
function removalOf(work: Work, v: number, audit: EdgeSetAudit | null = null): Removal {
|
|
1531
|
+
const step = removalStep(work, v);
|
|
1532
|
+
if ('blocked' in step) return step;
|
|
1533
|
+
const old = work.triangles;
|
|
1534
|
+
const n = work.alive.length;
|
|
1535
|
+
return { triangles: step.triangles, added: () => auditedAdded(audit, `removing ${v}`, addedEdges(old, step.fresh, n), () => addedEdgesByString(old, step.fresh)) };
|
|
1414
1536
|
}
|
|
1415
1537
|
|
|
1416
1538
|
/** L1 difference of two weight vectors over the bones they name. */
|
|
@@ -1447,10 +1569,10 @@ function loadOrder(run: Run): { order: number[]; load: Map<number, number> } {
|
|
|
1447
1569
|
const order: number[] = [];
|
|
1448
1570
|
for (let v = 0; v < work.nSource; v++) {
|
|
1449
1571
|
if (!work.alive[v] || run.protectedVertices.has(v)) continue;
|
|
1450
|
-
const step = removalOf(work, v);
|
|
1572
|
+
const step = removalOf(work, v, run.edgeAudit);
|
|
1451
1573
|
let most = 0;
|
|
1452
1574
|
if ('blocked' in step) most = Infinity;
|
|
1453
|
-
else for (const [a, b] of step.added) most = Math.max(most, edgeLoad(work, a, b));
|
|
1575
|
+
else for (const [a, b] of step.added()) most = Math.max(most, edgeLoad(work, a, b));
|
|
1454
1576
|
load.set(v, most);
|
|
1455
1577
|
order.push(v);
|
|
1456
1578
|
}
|
|
@@ -1470,32 +1592,27 @@ function loadOrder(run: Run): { order: number[]; load: Map<number, number> } {
|
|
|
1470
1592
|
* structural reason one of the removals cannot be made. The outline loses the run and gains the chord from the
|
|
1471
1593
|
* vertex before it to the vertex after it; nothing is measured in between.
|
|
1472
1594
|
*/
|
|
1473
|
-
function runRemovalOf(work: Work, vertices: readonly number[]
|
|
1595
|
+
function runRemovalOf(work: Work, vertices: readonly number[], audit: EdgeSetAudit | null = null): Removal {
|
|
1474
1596
|
const was = work.triangles;
|
|
1597
|
+
// Issue #1307: each removal's own added edges are never read here, so none is built; what the run adds is read off
|
|
1598
|
+
// the triangles its removals made. A triangle of `after` that no removal made is the same tuple as one of `was`
|
|
1599
|
+
// (`removalStep` keeps every untouched triangle as it was), so every edge it holds is old and it adds nothing.
|
|
1600
|
+
const made = new Set<readonly number[]>();
|
|
1475
1601
|
let after: Array<[number, number, number]>;
|
|
1476
1602
|
try {
|
|
1477
1603
|
for (const v of vertices) {
|
|
1478
|
-
const step =
|
|
1604
|
+
const step = removalStep(work, v);
|
|
1479
1605
|
if ('blocked' in step) return step;
|
|
1606
|
+
for (const t of step.fresh) made.add(t);
|
|
1480
1607
|
work.triangles = step.triangles;
|
|
1481
1608
|
}
|
|
1482
1609
|
after = work.triangles;
|
|
1483
1610
|
} finally {
|
|
1484
1611
|
work.triangles = was;
|
|
1485
1612
|
}
|
|
1486
|
-
const
|
|
1487
|
-
|
|
1488
|
-
|
|
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 };
|
|
1613
|
+
const n = work.alive.length;
|
|
1614
|
+
const label = `removing ${vertices.join(', ')} as one boundary run`;
|
|
1615
|
+
return { triangles: after, added: () => auditedAdded(audit, label, addedEdges(was, after.filter((t) => made.has(t)), n), () => addedEdgesByString(was, after)) };
|
|
1499
1616
|
}
|
|
1500
1617
|
|
|
1501
1618
|
/**
|
|
@@ -1503,7 +1620,20 @@ function runRemovalOf(work: Work, vertices: readonly number[]): { triangles: Arr
|
|
|
1503
1620
|
* measurement — a thunk that measures the attempt in full and returns the name the measurement gives, so whatever
|
|
1504
1621
|
* reads it reads what an unfloored run would have written.
|
|
1505
1622
|
*/
|
|
1506
|
-
type Refusal = string |
|
|
1623
|
+
type Refusal = string | FloorRefusal;
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* A refusal the deviation floor decided (issue #1279), read since issue #1309 before any structure is built for the
|
|
1627
|
+
* attempt. Reading it names the attempt as the full path does — its structural reason when the structure refuses
|
|
1628
|
+
* it, else the rows' — in the state it was tried in. `decidedByFloor` is what the observer reads: whether the path
|
|
1629
|
+
* before #1309, which read the floor after every structural check, would have been decided by it — so an attempt
|
|
1630
|
+
* the structure refuses reads false, as it did. It is computed only when an observer or the floor audit is attached;
|
|
1631
|
+
* with neither it reads true and nothing reads it.
|
|
1632
|
+
*/
|
|
1633
|
+
interface FloorRefusal {
|
|
1634
|
+
(): string;
|
|
1635
|
+
decidedByFloor: boolean;
|
|
1636
|
+
}
|
|
1507
1637
|
|
|
1508
1638
|
function refusalText(refusal: Refusal): string {
|
|
1509
1639
|
return typeof refusal === 'string' ? refusal : refusal();
|
|
@@ -1563,7 +1693,7 @@ function removeVertices(run: Run): PhaseEnd {
|
|
|
1563
1693
|
run.steps++;
|
|
1564
1694
|
tried++;
|
|
1565
1695
|
const block = tryOperation(run, members, false);
|
|
1566
|
-
if (run.observe !== null) run.observe({ step: run.steps, kind: 'boundary-run', sourceVertices: [...members], refusedBy: block === null ? null : () => refusalText(block), decidedByFloor: typeof block === 'function', pass, predictedLoad: null });
|
|
1696
|
+
if (run.observe !== null) run.observe({ step: run.steps, kind: 'boundary-run', sourceVertices: [...members], refusedBy: block === null ? null : () => refusalText(block), decidedByFloor: typeof block === 'function' && block.decidedByFloor, pass, predictedLoad: null });
|
|
1567
1697
|
if (block === null) {
|
|
1568
1698
|
taken++;
|
|
1569
1699
|
walk = null;
|
|
@@ -1585,7 +1715,7 @@ function removeVertices(run: Run): PhaseEnd {
|
|
|
1585
1715
|
run.steps++;
|
|
1586
1716
|
tried++;
|
|
1587
1717
|
const block = tryOperation(run, [v], false);
|
|
1588
|
-
if (run.observe !== null) run.observe({ step: run.steps, kind: 'removal', sourceVertices: [v], refusedBy: block === null ? null : () => refusalText(block), decidedByFloor: typeof block === 'function', pass, predictedLoad: ranked === null ? null : ranked.load.get(v)! });
|
|
1718
|
+
if (run.observe !== null) run.observe({ step: run.steps, kind: 'removal', sourceVertices: [v], refusedBy: block === null ? null : () => refusalText(block), decidedByFloor: typeof block === 'function' && block.decidedByFloor, pass, predictedLoad: ranked === null ? null : ranked.load.get(v)! });
|
|
1589
1719
|
if (block === null) {
|
|
1590
1720
|
taken++;
|
|
1591
1721
|
if (accept(run, 'removal', [v])) return { kind: 'replayed' };
|
|
@@ -1603,6 +1733,34 @@ function removeVertices(run: Run): PhaseEnd {
|
|
|
1603
1733
|
}
|
|
1604
1734
|
}
|
|
1605
1735
|
|
|
1736
|
+
/**
|
|
1737
|
+
* Issue #1309: the current outline's canonical walk (`canonicalWalk`), read once per working mesh — the triangles are
|
|
1738
|
+
* replaced, never edited in place, whenever the mesh changes, so the array read off is what the cache is keyed by.
|
|
1739
|
+
*/
|
|
1740
|
+
function currentWalk(run: Run): number[] {
|
|
1741
|
+
if (run.walk === null || run.walk.triangles !== run.work.triangles) run.walk = { triangles: run.work.triangles, walk: canonicalWalk(run.work, run.sourceTurn).walk };
|
|
1742
|
+
return run.walk.walk;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
/**
|
|
1746
|
+
* The deviation floor read before any structure is built for the attempt (issue #1309): `deviationFloor` over the
|
|
1747
|
+
* outline the candidate would have — the current walk less the removed vertices, turned as `canonicalise` turns it
|
|
1748
|
+
* (`turnedFromSmallest`) — rather than over the canonical candidate. Whenever the structure passes, the candidate's
|
|
1749
|
+
* outline is that walk, so the two are the same polygon in the same order and the same number; when it does not, the
|
|
1750
|
+
* attempt is refused whatever the floor reads, and the reader is shown the structure's reason (`floorRefusal`). 0, the
|
|
1751
|
+
* floor of nothing, when no removed vertex is a source-hull vertex (the walk is then not read) or when fewer than three
|
|
1752
|
+
* would remain, which no passing structure leaves. `turned` reports whether the walk was walked the other way.
|
|
1753
|
+
*/
|
|
1754
|
+
function earlyFloor(run: Run, vertices: readonly number[]): { floor: number; turned: boolean } {
|
|
1755
|
+
const hull = run.input.source.hull;
|
|
1756
|
+
if (!vertices.some((v) => v < hull)) return { floor: 0, turned: false };
|
|
1757
|
+
const rest = currentWalk(run).filter((id) => !vertices.includes(id));
|
|
1758
|
+
if (rest.length < 3) return { floor: 0, turned: false };
|
|
1759
|
+
const outline = turnedFromSmallest(run.work, rest, run.sourceTurn);
|
|
1760
|
+
const turned = outline.length > 2 && outline[1] !== rest[(rest.indexOf(outline[0]) + 1) % rest.length];
|
|
1761
|
+
return { floor: floorOver(run, outline.map((id) => run.work.pos[id]), vertices), turned };
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1606
1764
|
/**
|
|
1607
1765
|
* The deviation floor (issue #1279): the furthest any removed source-hull vertex lies from the candidate's outline.
|
|
1608
1766
|
* `MQ_BOUNDARY_DEVIATION` is the symmetric Hausdorff distance between that outline and the source hull, and its
|
|
@@ -1611,7 +1769,11 @@ function removeVertices(run: Run): PhaseEnd {
|
|
|
1611
1769
|
* `distanceToSegment` over the candidate's hull edges, the function and polygon the row reads.
|
|
1612
1770
|
*/
|
|
1613
1771
|
function deviationFloor(run: Run, mesh: SourceMesh, vertices: readonly number[]): number {
|
|
1614
|
-
|
|
1772
|
+
return floorOver(run, mesh.points.slice(0, mesh.hull), vertices);
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
/** `deviationFloor` over an outline given as its points in walk order — the one loop both floors read. */
|
|
1776
|
+
function floorOver(run: Run, poly: readonly Pt[], vertices: readonly number[]): number {
|
|
1615
1777
|
let floor = 0;
|
|
1616
1778
|
for (const v of vertices) {
|
|
1617
1779
|
if (v >= run.input.source.hull) continue;
|
|
@@ -1630,19 +1792,66 @@ function deviationFloor(run: Run, mesh: SourceMesh, vertices: readonly number[])
|
|
|
1630
1792
|
const FLOOR_MARGIN = 1e-5;
|
|
1631
1793
|
|
|
1632
1794
|
/**
|
|
1633
|
-
*
|
|
1634
|
-
*
|
|
1635
|
-
* reaches the rows; otherwise a candidate whose deviation floor is over the bound by `FLOOR_MARGIN` is refused
|
|
1636
|
-
* without the measurement, by a thunk that measures it when its name is read.
|
|
1795
|
+
* Issue #1307's audit of the triangulation's edge set (`EdgeSetAudit`): the plant applied to `edges` in place — so
|
|
1796
|
+
* the decision reads the planted set — then the set held to the string-keyed one, member for member.
|
|
1637
1797
|
*/
|
|
1638
|
-
function
|
|
1798
|
+
function auditEdgeSet(audit: EdgeSetAudit, edges: Set<number>, triangles: ReadonlyArray<readonly number[]>, n: number, vertices: readonly number[]): void {
|
|
1799
|
+
if (audit.plant === 'drop-one') {
|
|
1800
|
+
const first = edges.values().next();
|
|
1801
|
+
if (first.done !== true) edges.delete(first.value);
|
|
1802
|
+
}
|
|
1803
|
+
const want = new Set<string>();
|
|
1804
|
+
for (const t of triangles) for (let k = 0; k < 3; k++) want.add(edgeKey(t[k], t[(k + 1) % 3]));
|
|
1805
|
+
audit.edgeSets++;
|
|
1806
|
+
audit.edgeSetEdges += want.size;
|
|
1807
|
+
const missing = [...want].filter((key) => {
|
|
1808
|
+
const [a, b] = key.split(',').map(Number);
|
|
1809
|
+
return !edges.has(pairKey(a, b, n));
|
|
1810
|
+
});
|
|
1811
|
+
if (missing.length > 0 || edges.size !== want.size) {
|
|
1812
|
+
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(' ')}` : ''}`);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
/**
|
|
1817
|
+
* Issue #1309's audit, for the `mesh-compare` suite's control and the population run: on every attempt the floor is
|
|
1818
|
+
* read on, the path before the issue is run beside the early floor — every structural check, then the floor read off
|
|
1819
|
+
* the canonical candidate, decided at `FLOOR_MARGIN` — and compared. Where the structure passes, the two floors are
|
|
1820
|
+
* held equal bit for bit and their decisions equal; where the early floor refuses and the structure would have refused
|
|
1821
|
+
* first, the name a reader of the refusal is shown is held to the structure's, and the observer's `decidedByFloor` to
|
|
1822
|
+
* false. A difference is recorded, not thrown. `plant` is the fault applied to the early refusal: its name read off
|
|
1823
|
+
* the floor (`MQ_BOUNDARY_DEVIATION`), never reaching the structural check that would have refused first. With the
|
|
1824
|
+
* audit and no plant, the call's result is the call's without it.
|
|
1825
|
+
*/
|
|
1826
|
+
export interface FloorAudit {
|
|
1827
|
+
plant?: 'refusal-named-by-the-floor';
|
|
1828
|
+
/** Attempts the floor was read on. */
|
|
1829
|
+
attempts: number;
|
|
1830
|
+
/** Among them, those whose structure passes — where the early floor and the late one are compared. */
|
|
1831
|
+
compared: number;
|
|
1832
|
+
/** Among those, the ones whose predicted outline `canonicalise` walks the other way from the current walk. */
|
|
1833
|
+
turned: number;
|
|
1834
|
+
/** Attempts the early floor refused, and among them those whose structure refuses them first. */
|
|
1835
|
+
refused: number;
|
|
1836
|
+
refusedStructurally: number;
|
|
1837
|
+
/** Each difference found, named. */
|
|
1838
|
+
differences: string[];
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
/**
|
|
1842
|
+
* The structure of one operation, as every attempt builds it: the removal or run made (or its structural reason),
|
|
1843
|
+
* condition (b) over the edges it adds, the working mesh set to it, the protected edges looked up, and the canonical
|
|
1844
|
+
* candidate. Null-free: either the reason the structure refuses the operation — the working mesh as it was — or the
|
|
1845
|
+
* canonical candidate with the working mesh holding it and the `undo` that restores it.
|
|
1846
|
+
*/
|
|
1847
|
+
function structureOf(run: Run, vertices: readonly number[]): string | { canon: Canonical; undo: () => void } {
|
|
1639
1848
|
const { work, input } = run;
|
|
1640
|
-
const step = vertices.length === 1 ? removalOf(work, vertices[0]) : runRemovalOf(work, vertices);
|
|
1849
|
+
const step = vertices.length === 1 ? removalOf(work, vertices[0], run.edgeAudit) : runRemovalOf(work, vertices, run.edgeAudit);
|
|
1641
1850
|
if ('blocked' in step) return step.blocked;
|
|
1642
1851
|
const jump = input.protect.weightJump;
|
|
1643
1852
|
if (jump !== null) {
|
|
1644
|
-
for (const [a, b] of step.added) {
|
|
1645
|
-
if (a < work.nSource && b < work.nSource && run.sourceEdges.has(
|
|
1853
|
+
for (const [a, b] of step.added()) {
|
|
1854
|
+
if (a < work.nSource && b < work.nSource && run.sourceEdges.has(pairKey(a, b, work.nSource))) continue;
|
|
1646
1855
|
const d = weightJump(work, a, b);
|
|
1647
1856
|
if (d > jump) return `weightJump (b): the new edge ${a}–${b} joins weight vectors ${r6(d)} apart, over ${jump}`;
|
|
1648
1857
|
}
|
|
@@ -1654,43 +1863,116 @@ function tryOperation(run: Run, vertices: readonly number[], full: boolean): Ref
|
|
|
1654
1863
|
work.triangles = was.triangles;
|
|
1655
1864
|
for (const v of vertices) work.alive[v] = true;
|
|
1656
1865
|
};
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1866
|
+
// Issue #1307: the triangulation's edge set is built only when a protected edge is looked up in it, keyed by number.
|
|
1867
|
+
if (run.protectedEdges.length > 0) {
|
|
1868
|
+
const n = work.alive.length;
|
|
1869
|
+
const edges = new Set<number>();
|
|
1870
|
+
for (const t of work.triangles) for (let k = 0; k < 3; k++) edges.add(pairKey(t[k], t[(k + 1) % 3], n));
|
|
1871
|
+
if (run.edgeAudit !== null) auditEdgeSet(run.edgeAudit, edges, work.triangles, n, vertices);
|
|
1872
|
+
for (const [a, b] of run.protectedEdges) {
|
|
1873
|
+
if (!edges.has(pairKey(a, b, n))) {
|
|
1874
|
+
undo();
|
|
1875
|
+
return `protect (a): the protected source edge ${a}–${b} is no longer an edge`;
|
|
1876
|
+
}
|
|
1663
1877
|
}
|
|
1664
1878
|
}
|
|
1665
|
-
let canon: Canonical;
|
|
1666
1879
|
try {
|
|
1667
|
-
canon
|
|
1880
|
+
return { canon: canonicalise(work, run.sourceTurn, run.boneRank), undo };
|
|
1668
1881
|
} catch (err) {
|
|
1669
1882
|
if (!(err instanceof MeshError)) throw err;
|
|
1670
1883
|
undo();
|
|
1671
1884
|
return `outline: ${err.message}`;
|
|
1672
1885
|
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
/**
|
|
1889
|
+
* The refusal the early floor makes (issue #1309), through #1279's thunk: read, it measures the attempt in full in the
|
|
1890
|
+
* state it was tried in and returns that path's name — the structure's reason when the structure refuses it, else the
|
|
1891
|
+
* rows'. `late`, when an observer or the floor audit asked for it, is what the structure and the floor read off the
|
|
1892
|
+
* canonical candidate gave: it decides `decidedByFloor` as the path before the issue decided it, and — with an
|
|
1893
|
+
* observer and no floor audit — its structural reason is the name returned, built once rather than twice (the edge
|
|
1894
|
+
* audit counts every list it builds). Under the floor audit the thunk always runs the full path, which is what the
|
|
1895
|
+
* audit holds equal to the structure's reason.
|
|
1896
|
+
*/
|
|
1897
|
+
function floorRefusal(run: Run, vertices: readonly number[], floor: number, limit: number, late: { structural: string } | { floor: number } | null): FloorRefusal {
|
|
1898
|
+
const members = [...vertices];
|
|
1899
|
+
const said = late !== null && 'structural' in late && run.floorAudit === null ? late.structural : null;
|
|
1900
|
+
const refusal = (() => {
|
|
1901
|
+
if (run.floorAudit?.plant === 'refusal-named-by-the-floor') return `MQ_BOUNDARY_DEVIATION: the deviation floor ${r6(floor)} is over ${run.input.targets.maxBoundaryDeviation}`;
|
|
1902
|
+
if (said !== null) return said;
|
|
1903
|
+
const measured = tryOperation(run, members, true);
|
|
1904
|
+
if (measured === null) {
|
|
1905
|
+
// Unreachable while the floor is a floor: the step was refused on a bound its own rows then pass. Loud,
|
|
1906
|
+
// because the run already went on as if it had been refused.
|
|
1907
|
+
throw new Error(`the deviation floor refused removing ${members.join(', ')}, which the rows accept`);
|
|
1908
|
+
}
|
|
1909
|
+
return refusalText(measured);
|
|
1910
|
+
}) as FloorRefusal;
|
|
1911
|
+
refusal.decidedByFloor = late === null || ('floor' in late && late.floor > limit);
|
|
1912
|
+
return refusal;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
/** The path before issue #1309 for one attempt, read beside the early floor: the structure's reason, or the floor read off the canonical candidate. The working mesh is left as it was. */
|
|
1916
|
+
function lateOf(run: Run, vertices: readonly number[]): { structural: string } | { floor: number } {
|
|
1917
|
+
const built = structureOf(run, vertices);
|
|
1918
|
+
if (typeof built === 'string') return { structural: built };
|
|
1919
|
+
const floor = deviationFloor(run, built.canon.mesh, vertices);
|
|
1920
|
+
built.undo();
|
|
1921
|
+
return { floor };
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
/** The floor audit's comparison where the structure passes: the two floors bit for bit, and the decision at `FLOOR_MARGIN`. */
|
|
1925
|
+
function auditFloors(audit: FloorAudit, run: Run, vertices: readonly number[], early: { floor: number; turned: boolean }, late: number, refusedEarly: boolean): void {
|
|
1926
|
+
audit.compared++;
|
|
1927
|
+
if (early.turned) audit.turned++;
|
|
1928
|
+
const lateRefuses = late > run.input.targets.maxBoundaryDeviation + FLOOR_MARGIN;
|
|
1929
|
+
if (!Object.is(early.floor, late)) audit.differences.push(`removing ${vertices.join(', ')}: the early floor reads ${early.floor} and the floor off the canonical candidate ${late}`);
|
|
1930
|
+
else if (refusedEarly !== lateRefuses) audit.differences.push(`removing ${vertices.join(', ')}: the early floor ${refusedEarly ? 'refused' : 'passed'} at ${early.floor}, which the floor off the canonical candidate ${lateRefuses ? 'refuses' : 'passes'} at the bound ${run.input.targets.maxBoundaryDeviation} + FLOOR_MARGIN`);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
/**
|
|
1934
|
+
* One operation — a single removal, or a boundary run of two or more — tried: null when it was taken (the working
|
|
1935
|
+
* mesh then holds it), else what refused it (the working mesh as it was). `full` measures every candidate that
|
|
1936
|
+
* reaches the rows; otherwise a candidate whose deviation floor is over the bound by `FLOOR_MARGIN` is refused
|
|
1937
|
+
* without the measurement, by a thunk that measures it when its name is read. Since issue #1309 the floor is read
|
|
1938
|
+
* before the structure is built (`earlyFloor`); an attempt it refuses is not built at all unless an observer or the
|
|
1939
|
+
* floor audit asks what the path before would have decided.
|
|
1940
|
+
*/
|
|
1941
|
+
function tryOperation(run: Run, vertices: readonly number[], full: boolean): Refusal | null {
|
|
1942
|
+
const { input } = run;
|
|
1943
|
+
const floored = !full && run.plant !== 'measure-every-candidate' && !(vertices.length > 1 && run.plant === 'run-skips-rows');
|
|
1944
|
+
const audit = run.floorAudit;
|
|
1945
|
+
let early: { floor: number; turned: boolean } | null = null;
|
|
1946
|
+
if (floored) {
|
|
1947
|
+
const limit = input.targets.maxBoundaryDeviation + (run.plant === 'floor-half-a-pixel-short' ? -0.5 : FLOOR_MARGIN);
|
|
1948
|
+
early = earlyFloor(run, vertices);
|
|
1949
|
+
if (audit !== null) audit.attempts++;
|
|
1950
|
+
if (early.floor > limit) {
|
|
1951
|
+
const late = run.observe !== null || audit !== null ? lateOf(run, vertices) : null;
|
|
1952
|
+
const refusal = floorRefusal(run, vertices, early.floor, limit, late);
|
|
1953
|
+
if (audit !== null && late !== null) {
|
|
1954
|
+
audit.refused++;
|
|
1955
|
+
if ('structural' in late) {
|
|
1956
|
+
audit.refusedStructurally++;
|
|
1957
|
+
const said = refusal();
|
|
1958
|
+
if (said !== late.structural) audit.differences.push(`removing ${vertices.join(', ')}: the early refusal reads "${said.slice(0, 80)}" where the structure refuses first with "${late.structural.slice(0, 80)}"`);
|
|
1959
|
+
if (refusal.decidedByFloor) audit.differences.push(`removing ${vertices.join(', ')}: refused by the structure first, yet the observer would read decidedByFloor`);
|
|
1960
|
+
} else {
|
|
1961
|
+
auditFloors(audit, run, vertices, early, late.floor, true);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
return refusal;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
const built = structureOf(run, vertices);
|
|
1968
|
+
if (typeof built === 'string') return built;
|
|
1969
|
+
const { canon, undo } = built;
|
|
1970
|
+
if (audit !== null && early !== null) auditFloors(audit, run, vertices, early, deviationFloor(run, canon.mesh, vertices), false);
|
|
1673
1971
|
const phase: SkinningInspection['phase'] = vertices.length > 1 ? 'boundary-run' : 'removal';
|
|
1674
1972
|
if (vertices.length > 1 && run.plant === 'run-skips-rows') {
|
|
1675
1973
|
followSkinning(run, phase);
|
|
1676
1974
|
return null;
|
|
1677
1975
|
}
|
|
1678
|
-
if (!full && run.plant !== 'measure-every-candidate') {
|
|
1679
|
-
const margin = run.plant === 'floor-half-a-pixel-short' ? -0.5 : FLOOR_MARGIN;
|
|
1680
|
-
if (deviationFloor(run, canon.mesh, vertices) > input.targets.maxBoundaryDeviation + margin) {
|
|
1681
|
-
undo();
|
|
1682
|
-
const members = [...vertices];
|
|
1683
|
-
return () => {
|
|
1684
|
-
const measured = tryOperation(run, members, true);
|
|
1685
|
-
if (measured === null) {
|
|
1686
|
-
// Unreachable while the floor is a floor: the step was refused on a bound its own rows then pass. Loud,
|
|
1687
|
-
// because the run already went on as if it had been refused.
|
|
1688
|
-
throw new Error(`the deviation floor refused removing ${members.join(', ')}, which the rows accept`);
|
|
1689
|
-
}
|
|
1690
|
-
return refusalText(measured);
|
|
1691
|
-
};
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
1976
|
const measured = measureAgainstTargets(run, canon.mesh, 'candidate');
|
|
1695
1977
|
const blocking =
|
|
1696
1978
|
firstBlockingRow(measured, run.input.targets.artFit) ??
|
|
@@ -1841,7 +2123,7 @@ function delaunayFlips(run: Run, plant: ReductionPlant | null): FlipResult {
|
|
|
1841
2123
|
const sine = oppositeAngleSine(P[a], P[b], P[c], P[d]);
|
|
1842
2124
|
if (against ? !(sine > FLIP_MARGIN) : !(sine < -FLIP_MARGIN)) continue;
|
|
1843
2125
|
if (held(a, b) || held(c, d)) continue;
|
|
1844
|
-
if (jump !== null && !(c < work.nSource && d < work.nSource && run.sourceEdges.has(
|
|
2126
|
+
if (jump !== null && !(c < work.nSource && d < work.nSource && run.sourceEdges.has(pairKey(c, d, work.nSource))) && weightJump(work, c, d) > jump) continue;
|
|
1845
2127
|
if (flips >= maxFlips) return { bound: `the flip bound n(n - 1)/2 = ${maxFlips} over ${alive} vertices was reached`, flips, sweeps };
|
|
1846
2128
|
tris[i] = [a, d, c];
|
|
1847
2129
|
tris[j] = [d, b, c];
|
|
@@ -1961,12 +2243,14 @@ export function reduceMeshWith(
|
|
|
1961
2243
|
plant: ReductionPlant | null = null,
|
|
1962
2244
|
observe: AttemptObserver | null = null,
|
|
1963
2245
|
skinning: SkinningHooks | null = null,
|
|
2246
|
+
edgeAudit: EdgeSetAudit | null = null,
|
|
2247
|
+
floorAudit: FloorAudit | null = null,
|
|
1964
2248
|
): MeshReductionResult {
|
|
1965
2249
|
validateReduction(input, plant);
|
|
1966
2250
|
if (steps !== null && steps.rasters !== rasters) {
|
|
1967
2251
|
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
2252
|
}
|
|
1969
|
-
return reduceValidated(input, rasters, steps, plant, observe, skinning);
|
|
2253
|
+
return reduceValidated(input, rasters, steps, plant, observe, skinning, edgeAudit, floorAudit);
|
|
1970
2254
|
}
|
|
1971
2255
|
|
|
1972
2256
|
/**
|
|
@@ -1980,6 +2264,8 @@ function reduceValidated(
|
|
|
1980
2264
|
plant: ReductionPlant | null = null,
|
|
1981
2265
|
observe: AttemptObserver | null = null,
|
|
1982
2266
|
hooks: SkinningHooks | null = null,
|
|
2267
|
+
edgeAudit: EdgeSetAudit | null = null,
|
|
2268
|
+
floorAudit: FloorAudit | null = null,
|
|
1983
2269
|
): MeshReductionResult {
|
|
1984
2270
|
const who = `attachment ${nameOf(input.attachment)}`;
|
|
1985
2271
|
const src = input.source;
|
|
@@ -2047,8 +2333,12 @@ function reduceValidated(
|
|
|
2047
2333
|
sourceTriangle: new Map(),
|
|
2048
2334
|
};
|
|
2049
2335
|
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
|
-
|
|
2051
|
-
|
|
2336
|
+
// The string-keyed set is read once, by `protectionOf`, whose weightJump edges follow its sorted keys; every step
|
|
2337
|
+
// reads the numeric one (issue #1307).
|
|
2338
|
+
const sourceEdgeNames = new Set<string>();
|
|
2339
|
+
for (const t of work.triangles) for (let k = 0; k < 3; k++) sourceEdgeNames.add(edgeKey(t[k], t[(k + 1) % 3]));
|
|
2340
|
+
const sourceEdges = new Set<number>();
|
|
2341
|
+
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
2342
|
const run: Run = {
|
|
2053
2343
|
input,
|
|
2054
2344
|
who,
|
|
@@ -2062,8 +2352,11 @@ function reduceValidated(
|
|
|
2062
2352
|
observe,
|
|
2063
2353
|
tally: { sharesDroppedOnGrid: 0, sharesPruned: 0 },
|
|
2064
2354
|
keyed: keyedSourceVertices(input),
|
|
2065
|
-
...protectionOf(input, work,
|
|
2355
|
+
...protectionOf(input, work, sourceEdgeNames),
|
|
2066
2356
|
sourceEdges,
|
|
2357
|
+
edgeAudit,
|
|
2358
|
+
floorAudit,
|
|
2359
|
+
walk: null,
|
|
2067
2360
|
rasters,
|
|
2068
2361
|
stepRasters: steps,
|
|
2069
2362
|
skinning: null,
|