#120: CellView containment rejection - the reciprocal ping-pong converges; tripwire firings reproduced + killed
The armed tripwire self-attributed on the re-gate launch (regate-118-119-launch.log): a pure TWO-CELL reciprocal ping-pong, 64 laps each - chain root=0xA9B4015C eye=(109.995,37.158,96.249) cells 0162x64 015Cx64, and root=0xA9B3010F eye=(175.771,-107.310,118.814) cells 0103x64 010Fx64 (A9B3 = the hill cottage the user reports going all-transparent on entry - likely the same mechanism, verify at the next gate). Mechanism: with the eye within PortalSideEpsilon (+-1 cm; the T2 refuted-to-tighten root-lag tolerance - retail's is 0.0002) of a portal plane, the in-plane case counts as interior for BOTH cells, so views lap A->B->A...; each lap re-clips through two near-edge-on apertures whose intersection numerics wobble by more than CellView's 1e-3 dedup grid, so every lap keys as NEW and the in-place growth recurses to the depth-128 failsafe. The prior convergence sweeps could not reproduce because they only load the corner building 0x016F-0x0175 - both firing pairs are outside that set. Issue120ReciprocalPingPongTests loads the full landblock's interior cells and drives the +-epsilon window directly: deterministic firings + 65-polygon CellView piles pre-fix. Fix (the handoff's own predicted class - dedup admitting near-duplicates per lap, NOT a limit tune): CellView.Add rejects a polygon CONTAINED in one already stored (convex edge test, DedupGridNdc slack). A round-trip re-emission is, in exact arithmetic, a SUBSET of the polygon that originated it - containment rejection makes union growth strictly area-increasing, so no new visible area means no propagation. Bonus: back-emission into a full-screen view (the root cell) now always rejects. The corner-flood completeness pins stay green - no real region is dropped. Suites: App 236 (232+4), Core 1419+2skip, UI 420, Net 294. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8d93665053
commit
dede7e491c
2 changed files with 263 additions and 0 deletions
|
|
@ -83,6 +83,21 @@ public sealed class CellView
|
|||
if (key is null) return false; // degenerate after snap (< 3 distinct vertices)
|
||||
if (!_polygonKeys.Add(key)) return false; // duplicate region (drift / rotation / count tolerant)
|
||||
|
||||
// #120 convergence (2026-06-11): reject a polygon CONTAINED in one already
|
||||
// stored. The reciprocal ping-pong (eye within PortalSideEpsilon of a
|
||||
// portal plane → BOTH side tests pass → views lap A→B→A…) re-emits, each
|
||||
// lap, a region that is — in exact arithmetic — a SUBSET of the polygon
|
||||
// that originated it; near-edge-on apertures make the re-clip wobble by
|
||||
// more than the 1e-3 key grid, so every lap keyed as "new" and the
|
||||
// in-place growth recursed to the depth-128 tripwire (chain dumps:
|
||||
// 0xA9B4015C↔0x0162, 0xA9B30103↔0x010F; Issue120ReciprocalPingPongTests
|
||||
// reproduces deterministically). Containment rejection makes growth
|
||||
// strictly area-increasing — no new visible area, no propagation. The
|
||||
// key stays recorded so the exact emission also short-circuits later.
|
||||
// Bonus: back-emission into a full-screen view (the root cell) is now
|
||||
// always rejected outright.
|
||||
if (ContainedInExisting(p)) return false;
|
||||
|
||||
Polygons.Add(p);
|
||||
if (p.MinX < MinX) MinX = p.MinX;
|
||||
if (p.MinY < MinY) MinY = p.MinY;
|
||||
|
|
@ -91,6 +106,59 @@ public sealed class CellView
|
|||
return true;
|
||||
}
|
||||
|
||||
// #120: is polygon p entirely inside ONE stored polygon (with DedupGridNdc
|
||||
// slack)? Single-polygon containment is sufficient for the ping-pong class —
|
||||
// a round-trip re-emission descends from exactly one originator. Stored
|
||||
// polygons are convex (Sutherland-Hodgman / full-screen seed outputs); the
|
||||
// edge test adapts to either winding via the polygon's signed area.
|
||||
private bool ContainedInExisting(in ViewPolygon p)
|
||||
{
|
||||
const float eps = DedupGridNdc;
|
||||
for (int i = 0; i < Polygons.Count; i++)
|
||||
{
|
||||
var e = Polygons[i];
|
||||
// bounding-rect quick reject (with slack)
|
||||
if (p.MinX < e.MinX - eps || p.MaxX > e.MaxX + eps
|
||||
|| p.MinY < e.MinY - eps || p.MaxY > e.MaxY + eps)
|
||||
continue;
|
||||
if (ContainsAllVertices(e.Vertices, p.Vertices, eps))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ContainsAllVertices(Vector2[] convex, Vector2[] pts, float eps)
|
||||
{
|
||||
if (convex.Length < 3) return false;
|
||||
|
||||
// signed area → winding (CCW positive); inside = left of every CCW edge.
|
||||
float area2 = 0f;
|
||||
for (int i = 0; i < convex.Length; i++)
|
||||
{
|
||||
var a = convex[i];
|
||||
var b = convex[(i + 1) % convex.Length];
|
||||
area2 += a.X * b.Y - b.X * a.Y;
|
||||
}
|
||||
float sign = area2 >= 0f ? 1f : -1f;
|
||||
|
||||
for (int i = 0; i < convex.Length; i++)
|
||||
{
|
||||
var a = convex[i];
|
||||
var b = convex[(i + 1) % convex.Length];
|
||||
var ab = b - a;
|
||||
float len = ab.Length();
|
||||
if (len < 1e-9f) continue; // degenerate edge — no constraint
|
||||
foreach (var pt in pts)
|
||||
{
|
||||
// signed perpendicular distance of pt from edge a→b (positive = inside for CCW)
|
||||
float cross = sign * (ab.X * (pt.Y - a.Y) - ab.Y * (pt.X - a.X));
|
||||
if (cross < -eps * len)
|
||||
return false; // a vertex lies outside this edge by more than eps
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// NDC dedup grid. 1e-3 is ~0.5 px at 1080p — finer than the gap between distinct portal openings
|
||||
// (so real regions stay distinct) yet far coarser than the per-round float drift of a re-clipped
|
||||
// region (so a drifted duplicate snaps onto its predecessor). The finite grid is what bounds growth.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue