knife-edge port: polyClipFinish W=0 eye-plane clip + degenerate-view propagation; EyeInsidePortalOpening rescue DELETED
Ports retail ACRender::polyClipFinish (0x006b6d00, pc:702749) near-eye
semantics into PortalProjection.ProjectToClip - the fundamental fix for
the in-plane portal clip family (climb strobes, tower-top roof/floor
flap while turning; live-corroborated this session: [viewer-diff]
0xAAB30108 strobing 27x mid-climb, whole interior dropping at the top).
Pseudocode: docs/research/2026-06-11-polyclipfinish-w0-clip-pseudocode.md.
Three legs, all decomp-driven:
1. ProjectToClip clips at w >= 0 EXACTLY (was EyePlaneW=1e-4), with
retail's any-negative-w gate. Boundary intersections land at w == 0
(homogeneous directions), so a portal the eye is CROSSING yields the
correct unbounded half-region that the bounded view-region clip cuts
to the screen. A w=0 vertex cannot survive a bounded region clip
into the divide (direction fails some edge of any bounded convex
region); the measure-zero corner case is guarded non-finite->empty.
2. CellView.CanonicalKey keys ALL-COLLINEAR (zero-area) views as their
snapped segment ("L:" + extremes) instead of rejecting them - retail
PROPAGATES degenerate views (ClipPortals decomp:433651-433711
forwards any count!=0 GetClip output, no area gate anywhere), keeping
the cell behind an exactly-in-plane portal in the draw list (cells
draw whole; onward floods die naturally). Rejection dropped the
whole chain for the frame - the parked-eye knife-edge band. Finite
key space unchanged -> dedup + strict-growth convergence intact.
3. The EyeInsidePortalOpening rescue is DELETED (the T2-documented
compensation for the 1e-4 divergence) along with EyeStandingPerpDist
+ PointInPoly2D. Empty clip = no flood, period (retail's rule).
CornerFloodReplay - the gate that REFUTED the previous deletion
attempt - passes WITHOUT the rescue under the W=0 port.
Harness criterion corrected to retail's rules (it codified the rescue):
cells fully BEHIND the camera are not required (all-behind portals clip
empty in retail); monotone area holds per root regime; the two
manufactured exact-on-plane steps assert root-only (boundary root pick
is ambiguous; the in-plane portal there is ~perpendicular to the gaze =
genuinely off-screen). Build_CollapsedInteriorPortalNearEye test
inverted to pin the retail empty-clip rule (it pinned the rescue).
New pins: eye-crossing portal -> w==0 boundary verts + half-region (not
sliver); gaze-along-plane degenerate view accepted + segment-key dedup;
non-finite guard. Replay harnesses (CornerFloodReplay, Issue120,
TowerAscent, HouseExit, Issue127) all green.
Suites: App 246+1skip / Core 1430+2skip / UI 420 / Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2163308032
commit
987313aa54
7 changed files with 357 additions and 130 deletions
|
|
@ -167,10 +167,20 @@ public sealed class CellView
|
|||
// Canonical key for a view polygon: vertices snapped to the NDC grid, consecutive snap-duplicates
|
||||
// removed (including wrap-around), COLLINEAR points removed (exact integer cross-products on the
|
||||
// snapped grid), then rotated to start at the lexicographically smallest vertex so a rotated
|
||||
// emission of the same cycle yields the same key. Returns null when fewer than 3 distinct
|
||||
// snapped vertices survive (a degenerate sliver, not a real region). Winding is already CCW for every
|
||||
// emission of the same cycle yields the same key. Winding is already CCW for every
|
||||
// builder input (ClipToRegion / EnsureCcw), so the cyclic order is canonical without a reversal step.
|
||||
//
|
||||
// W=0 port (2026-06-11): an ALL-COLLINEAR polygon (zero area) keys as its snapped segment
|
||||
// ("L:" + extreme points) instead of null. A portal whose plane contains the eye projects to
|
||||
// exactly this — and retail PROPAGATES it: PView::ClipPortals (decomp:433651-433711) forwards
|
||||
// any GetClip output with count != 0 to copy_view/OtherPortalClip with no area gate anywhere,
|
||||
// so the neighbour cell stays in the draw list (cells draw whole; onward floods die naturally
|
||||
// against the zero-area region). Rejecting these views dropped the whole chain behind an
|
||||
// exactly-in-plane portal for the frame — the parked-eye knife-edge band (tower deck, spiral
|
||||
// landings). The segment key space is finite like the area-key space, so dedup + the strict
|
||||
// growth convergence invariant are unchanged. Returns null only when fewer than 2 distinct
|
||||
// snapped points survive (a true sub-grid point — not a real region OR segment).
|
||||
//
|
||||
// §4 corner/doorway fix (2026-06-10) — the collinear pass: the homogeneous region clipper
|
||||
// (PortalProjection.ClipToRegion, used by the forward AND — as of today — the reciprocal hop)
|
||||
// legitimately inserts intersection vertices ON a subject edge when a region edge grazes it, so
|
||||
|
|
@ -192,10 +202,15 @@ public sealed class CellView
|
|||
}
|
||||
if (pts.Count >= 2 && pts[^1] == pts[0]) pts.RemoveAt(pts.Count - 1);
|
||||
|
||||
// Snapshot the distinct snapped points BEFORE collinear removal — the all-collinear
|
||||
// fallback keys off the segment EXTREMES of the full point set (stable across
|
||||
// re-emissions regardless of the removal loop's order).
|
||||
List<(int X, int Y)>? preCollinear = pts.Count >= 2 ? new List<(int, int)>(pts) : null;
|
||||
|
||||
// Remove collinear points: for consecutive (prev, cur, next) around the cycle, drop cur when
|
||||
// cross(cur-prev, next-cur) == 0 — exact in integer grid coordinates (deltas ≤ ~4000, products
|
||||
// ≤ ~1.6e7, no overflow). Loop to a fixpoint: removing one point can make its neighbour
|
||||
// collinear. Degenerate inputs (all points on one line) reduce below 3 → rejected below.
|
||||
// collinear. All-collinear inputs reduce below 3 → the segment-key fallback below.
|
||||
bool removed = true;
|
||||
while (removed && pts.Count >= 3)
|
||||
{
|
||||
|
|
@ -215,7 +230,22 @@ public sealed class CellView
|
|||
}
|
||||
}
|
||||
}
|
||||
if (pts.Count < 3) return null;
|
||||
if (pts.Count < 3)
|
||||
{
|
||||
// Zero-area (all-collinear) view — key as its snapped segment so retail's
|
||||
// degenerate-view propagation works (see method doc). Extremes are the
|
||||
// lexicographic min/max of the full snapped point set.
|
||||
if (preCollinear is null) return null;
|
||||
var lo = preCollinear[0];
|
||||
var hi = preCollinear[0];
|
||||
foreach (var q in preCollinear)
|
||||
{
|
||||
if (q.X < lo.X || (q.X == lo.X && q.Y < lo.Y)) lo = q;
|
||||
if (q.X > hi.X || (q.X == hi.X && q.Y > hi.Y)) hi = q;
|
||||
}
|
||||
if (lo == hi) return null; // a sub-grid point — not a region or a segment
|
||||
return $"L:{lo.X},{lo.Y};{hi.X},{hi.Y};";
|
||||
}
|
||||
|
||||
int n = pts.Count;
|
||||
int best = 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue