diff --git a/src/AcDream.App/Rendering/ClipPlaneSet.cs b/src/AcDream.App/Rendering/ClipPlaneSet.cs
new file mode 100644
index 0000000..a4824eb
--- /dev/null
+++ b/src/AcDream.App/Rendering/ClipPlaneSet.cs
@@ -0,0 +1,260 @@
+// ClipPlaneSet.cs
+//
+// Phase U.2c: turn a CellView (a cell's accumulated screen-space clip region,
+// in NDC) into a small set of clip-space half-space planes for the GPU's
+// gl_ClipDistance, OR a scissor AABB when the region can't be expressed as one
+// convex plane set.
+//
+// This is the bridge between PortalVisibilityBuilder's 2D NDC view polygons and
+// the per-vertex clip the mesh/terrain shaders will perform (Phase U.2c → U.2e).
+// Pure System.Numerics math; NO GL. The shader consumes each plane as
+// d = nx*clip.x + ny*clip.y + 0*clip.z + dw*clip.w (>= 0 ⇒ keep)
+// where (nx, ny, dw) = the plane's (normal.xy, offset). z is always 0 because a
+// screen-space (NDC) edge is a vertical slab in clip space — independent of depth.
+//
+// === The convexity rule (read before touching this file) =====================
+// gl_ClipDistance planes are a CONJUNCTION of half-spaces, i.e. exactly ONE
+// convex region (their intersection). A CellView with MORE THAN ONE polygon is a
+// UNION of convex regions, which is in general NOT convex and CANNOT be
+// represented by one plane set. Emitting just the first/largest polygon's planes
+// would clip away the others → a real visibility bug (under-inclusion).
+//
+// Therefore From() NEVER emits a single polygon's planes when the CellView holds
+// several. Multi-polygon (and >8-edge) regions degrade to the UNION AABB scissor:
+// the scissor is a superset of the true region, so it OVER-includes (draws a few
+// extra pixels) but never hides anything. Over-inclusion is safe; under-inclusion
+// is the bug class.
+//
+// === The three Count==0 states (how a consumer tells them apart) =============
+// Count == 0 can mean three different things; the consumer MUST distinguish:
+// (a) Empty — IsNothingVisible == true, UseScissorFallback == false.
+// The cell/region isn't visible at all → DRAW NOTHING. The
+// ScissorNdcAabb is a degenerate inverted box (min > max) so that a
+// consumer which naively scissors on it still draws nothing.
+// (b) Scissor — UseScissorFallback == true, IsNothingVisible == false.
+// The convex-plane budget was exceeded (multi-polygon or >8 edges)
+// → DRAW the ScissorNdcAabb box (a valid min<=max NDC rectangle).
+// There is no third Count==0 state produced by From(). (A separate "no-clip,
+// pass-all" slot 0 is constructed by the consumer directly, not via From().)
+// When Count > 0, Planes carries the convex gate and the scissor fields are unused.
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+
+namespace AcDream.App.Rendering;
+
+///
+/// An NDC convex view region reduced to ≤8 clip-space gl_ClipDistance planes, or a
+/// scissor AABB fallback. See the file header for the convexity rule and the three
+/// Count==0 states.
+///
+public readonly struct ClipPlaneSet
+{
+ // Max simultaneous hardware clip planes we target (GL guarantees >= 8).
+ private const int MaxPlanes = 8;
+
+ // Collinear-edge merge threshold. Two consecutive edge directions are treated as
+ // the same edge when the turn between them is below ~0.5° (retail copy_view does a
+ // ~1px screen-space dedup). |sin θ| for unit dirs = |cross|; sin(0.5°) ≈ 0.0087265.
+ private const float CollinearSinEps = 0.0087265f;
+
+ // Drop a vertex whose two incident edges are shorter than this (NDC) — a duplicate
+ // or near-duplicate point that would otherwise yield a garbage normalized normal.
+ private const float DegenerateEdgeLen = 1e-6f;
+
+ // A polygon whose absolute signed area (full area, not 2x) falls below this is a line
+ // or point — zero screen coverage ⇒ nothing visible. A real portal opening has area far
+ // above this (e.g. the sliver-clip test region is 0.4); only an edge-on projection gets here.
+ private const float MinPolygonArea = 1e-7f;
+
+ private readonly Vector4[] _planes;
+
+ private ClipPlaneSet(Vector4[] planes, bool useScissorFallback, bool isNothingVisible, Vector4 scissorNdcAabb)
+ {
+ _planes = planes ?? Array.Empty();
+ UseScissorFallback = useScissorFallback;
+ IsNothingVisible = isNothingVisible;
+ ScissorNdcAabb = scissorNdcAabb;
+ }
+
+ /// Number of active clip planes, 0..8. 0 ⇒ inspect
+ /// and to decide between "draw the AABB" and "draw nothing".
+ public int Count => _planes?.Length ?? 0;
+
+ /// The active clip-space planes (nx, ny, 0, d). Empty when is 0.
+ /// A clip-space point clip is inside iff Vector4.Dot(plane, clip) >= 0 for every plane.
+ public IReadOnlyList Planes => _planes ?? (IReadOnlyList)Array.Empty();
+
+ /// True ⇒ the convex-plane budget was exceeded; gate on
+ /// instead (draw the box). Always false when > 0 or when the region is empty.
+ public bool UseScissorFallback { get; }
+
+ /// True ⇒ the region is not visible at all; the consumer draws NOTHING.
+ /// Mutually exclusive with , and only meaningful when Count == 0.
+ public bool IsNothingVisible { get; }
+
+ /// NDC axis-aligned scissor box (minX, minY, maxX, maxY). Valid (min <= max) only when
+ /// is true. For the empty/nothing-visible case it is a degenerate
+ /// inverted box so naive scissoring still draws nothing.
+ public Vector4 ScissorNdcAabb { get; }
+
+ /// The "nothing is visible" sentinel: Count == 0, not a scissor fallback, draw nothing.
+ public static ClipPlaneSet Empty { get; } =
+ new(Array.Empty(), useScissorFallback: false, isNothingVisible: true, scissorNdcAabb: DegenerateAabb);
+
+ // Inverted box (min > max) — any sane AABB intersection against it is empty.
+ private static Vector4 DegenerateAabb => new(1f, 1f, -1f, -1f);
+
+ ///
+ /// Reduce a CellView's NDC clip region to a ClipPlaneSet. One convex polygon (≤8 edges
+ /// after collinear-merge) → per-edge planes; multi-polygon or >8 edges → union-AABB scissor;
+ /// empty/degenerate → . See the file header for the full rule.
+ ///
+ public static ClipPlaneSet From(CellView region)
+ {
+ if (region is null || region.IsEmpty || region.Polygons.Count == 0)
+ return Empty;
+
+ // MORE THAN ONE polygon ⇒ union, not convex ⇒ never emit one polygon's planes.
+ // Over-include via the union AABB (safe). region.Min/Max already track the union.
+ if (region.Polygons.Count > 1)
+ return Scissor(region.MinX, region.MinY, region.MaxX, region.MaxY);
+
+ // Exactly one polygon: normalize winding to CCW, merge collinear edges, then decide.
+ Vector2[] verts = NormalizeAndMerge(region.Polygons[0].Vertices);
+
+ // Fewer than 3 distinct edges survive ⇒ a sliver/line with no area. There is no
+ // meaningful AABB to over-include (a zero-area region), so treat it as nothing visible.
+ if (verts.Length < 3)
+ return Empty;
+
+ // A single convex polygon with too many edges to fit the hardware budget ⇒ scissor
+ // on ITS own AABB (still a superset of the polygon → over-include, safe).
+ if (verts.Length > MaxPlanes)
+ return Scissor(verts);
+
+ // 3..8 edges: emit one inward half-space plane per edge (CCW formula).
+ var planes = new Vector4[verts.Length];
+ for (int i = 0; i < verts.Length; i++)
+ {
+ Vector2 p = verts[i];
+ Vector2 q = verts[(i + 1) % verts.Length];
+ Vector2 dir = q - p;
+ // Inward normal for CCW winding: perp(dir) = (-dir.y, dir.x) points to the polygon's
+ // interior (the "left" side of the directed edge p→q).
+ Vector2 n = Vector2.Normalize(new Vector2(-dir.Y, dir.X));
+ // Plane: n·x + d >= 0 inside, with d = -(n·p). In clip space with NDC x = clip.x/clip.w:
+ // dist = n.x*clip.x + n.y*clip.y + 0*clip.z + (-(n·p))*clip.w (>= 0 ⇒ keep)
+ planes[i] = new Vector4(n.X, n.Y, 0f, -Vector2.Dot(n, p));
+ }
+ return new ClipPlaneSet(planes, useScissorFallback: false, isNothingVisible: false, scissorNdcAabb: DegenerateAabb);
+ }
+
+ private static ClipPlaneSet Scissor(float minX, float minY, float maxX, float maxY) =>
+ new(Array.Empty(), useScissorFallback: true, isNothingVisible: false,
+ scissorNdcAabb: new Vector4(minX, minY, maxX, maxY));
+
+ private static ClipPlaneSet Scissor(ReadOnlySpan verts)
+ {
+ float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue;
+ foreach (var v in verts)
+ {
+ if (v.X < minX) minX = v.X;
+ if (v.X > maxX) maxX = v.X;
+ if (v.Y < minY) minY = v.Y;
+ if (v.Y > maxY) maxY = v.Y;
+ }
+ return Scissor(minX, minY, maxX, maxY);
+ }
+
+ ///
+ /// Return the polygon wound CCW with collinear vertices removed. The PortalVisibilityBuilder
+ /// already EnsureCcw's its output, but From() is a public entry point that must be robust to
+ /// either winding (e.g. a hand-built CellView), so we normalize here too.
+ ///
+ private static Vector2[] NormalizeAndMerge(Vector2[] input)
+ {
+ if (input is null || input.Length < 3)
+ return Array.Empty();
+
+ // 1) Drop exact/near-duplicate consecutive points first so edge directions are well-defined.
+ var pts = new List(input.Length);
+ foreach (var v in input)
+ {
+ if (pts.Count == 0 || (v - pts[^1]).LengthSquared() > DegenerateEdgeLen * DegenerateEdgeLen)
+ pts.Add(v);
+ }
+ // Wrap-around duplicate (last == first).
+ if (pts.Count >= 2 && (pts[^1] - pts[0]).LengthSquared() <= DegenerateEdgeLen * DegenerateEdgeLen)
+ pts.RemoveAt(pts.Count - 1);
+ if (pts.Count < 3)
+ return Array.Empty();
+
+ // 2) Force CCW winding (positive signed area). perp(dir)=(-y,x) is the inward normal only
+ // for CCW; if the caller handed us CW, reverse so the plane signs come out inside-positive.
+ if (SignedArea2(pts) < 0f)
+ pts.Reverse();
+
+ // 3) Merge collinear edges: drop vertex i when edge (i-1→i) and edge (i→i+1) point the same
+ // way (turn angle < ~0.5°). Iterate until stable — removing one vertex can expose a new
+ // collinear triple. |cross(a,b)| of unit dirs = |sin θ|; dot>0 rules out a 180° reversal.
+ bool changed = true;
+ while (changed && pts.Count >= 3)
+ {
+ changed = false;
+ for (int i = 0; i < pts.Count; i++)
+ {
+ Vector2 prev = pts[(i - 1 + pts.Count) % pts.Count];
+ Vector2 cur = pts[i];
+ Vector2 next = pts[(i + 1) % pts.Count];
+
+ Vector2 d0 = cur - prev;
+ Vector2 d1 = next - cur;
+ float l0 = d0.Length();
+ float l1 = d1.Length();
+ if (l0 < DegenerateEdgeLen || l1 < DegenerateEdgeLen)
+ {
+ pts.RemoveAt(i);
+ changed = true;
+ break;
+ }
+
+ d0 /= l0;
+ d1 /= l1;
+ float cross = d0.X * d1.Y - d0.Y * d1.X; // sin θ
+ float dot = d0.X * d1.X + d0.Y * d1.Y; // cos θ
+ if (dot > 0f && MathF.Abs(cross) < CollinearSinEps)
+ {
+ pts.RemoveAt(i); // cur lies on the straight line prev→next
+ changed = true;
+ break;
+ }
+ }
+ }
+
+ if (pts.Count < 3)
+ return Array.Empty();
+
+ // Final degeneracy gate: a polygon with negligible area is a line/point even if it still
+ // has >= 3 distinct vertices (e.g. an edge-on portal, or a near-collinear triple the 0.5°
+ // merge didn't quite collapse). Emitting its planes would yield an empty half-space
+ // intersection that silently gates out everything; report it honestly as nothing-visible.
+ if (MathF.Abs(SignedArea2(pts)) * 0.5f < MinPolygonArea)
+ return Array.Empty();
+
+ return pts.ToArray();
+ }
+
+ // Twice the signed area (the "shoelace" sum). > 0 ⇒ CCW, < 0 ⇒ CW.
+ private static float SignedArea2(List poly)
+ {
+ float a = 0f;
+ for (int i = 0; i < poly.Count; i++)
+ {
+ Vector2 p = poly[i];
+ Vector2 q = poly[(i + 1) % poly.Count];
+ a += p.X * q.Y - q.X * p.Y;
+ }
+ return a;
+ }
+}
diff --git a/tests/AcDream.App.Tests/Rendering/ClipPlaneSetTests.cs b/tests/AcDream.App.Tests/Rendering/ClipPlaneSetTests.cs
new file mode 100644
index 0000000..1c2a82a
--- /dev/null
+++ b/tests/AcDream.App.Tests/Rendering/ClipPlaneSetTests.cs
@@ -0,0 +1,255 @@
+using System;
+using System.Numerics;
+using AcDream.App.Rendering;
+using Xunit;
+
+namespace AcDream.App.Tests.Rendering;
+
+public class ClipPlaneSetTests
+{
+ // A CellView holding one regular n-gon centred at the origin, wound CCW.
+ private static CellView RegularNgonCellView(int n, float radius)
+ {
+ var verts = new Vector2[n];
+ for (int i = 0; i < n; i++)
+ {
+ // CCW: increasing angle.
+ float a = MathF.Tau * i / n;
+ verts[i] = new Vector2(radius * MathF.Cos(a), radius * MathF.Sin(a));
+ }
+ var cv = new CellView();
+ cv.Add(new ViewPolygon(verts));
+ return cv;
+ }
+
+ private static CellView SquareCellView(float min, float max)
+ {
+ var cv = new CellView();
+ cv.Add(new ViewPolygon(new[]
+ {
+ new Vector2(min, min), new Vector2(max, min), new Vector2(max, max), new Vector2(min, max),
+ }));
+ return cv;
+ }
+
+ // --- The three required tests (verbatim intent from the plan) -------------
+
+ [Fact]
+ public void From_AxisAlignedSquare_FourPlanes_PointInsideHasPositiveDistances()
+ {
+ var sq = new CellView();
+ sq.Add(new ViewPolygon(new[]
+ {
+ new Vector2(-0.5f, -0.5f), new Vector2(0.5f, -0.5f), new Vector2(0.5f, 0.5f), new Vector2(-0.5f, 0.5f),
+ }));
+ var cps = ClipPlaneSet.From(sq);
+ Assert.Equal(4, cps.Count);
+
+ var clip = new Vector4(0, 0, 0, 1); // NDC (0,0), inside
+ foreach (var p in cps.Planes)
+ Assert.True(Vector4.Dot(p, clip) >= 0);
+
+ var outClip = new Vector4(0.9f, 0, 0, 1); // NDC (0.9,0), outside the square
+ Assert.Contains(cps.Planes, p => Vector4.Dot(p, outClip) < 0);
+ }
+
+ [Fact]
+ public void From_NineEdgePolygon_FallsBackToScissor()
+ {
+ var poly = RegularNgonCellView(n: 9, radius: 0.6f);
+ var cps = ClipPlaneSet.From(poly);
+ Assert.True(cps.UseScissorFallback || cps.Count <= 8);
+ if (cps.UseScissorFallback)
+ {
+ Assert.Equal(0, cps.Count); // AABB carries the gate
+ }
+ }
+
+ [Fact]
+ public void From_EmptyRegion_IsEmpty()
+ {
+ Assert.Equal(0, ClipPlaneSet.From(new CellView()).Count);
+ }
+
+ // --- Multi-polygon safety (the under-inclusion guard) ---------------------
+
+ [Fact]
+ public void From_MultiplePolygons_FallsBackToUnionScissor_NeverEmitsOnePolygonsPlanes()
+ {
+ // Two disjoint squares: a CONVEX plane set can never represent their union.
+ var cv = new CellView();
+ cv.Add(new ViewPolygon(new[]
+ {
+ new Vector2(-0.8f, -0.8f), new Vector2(-0.4f, -0.8f), new Vector2(-0.4f, -0.4f), new Vector2(-0.8f, -0.4f),
+ }));
+ cv.Add(new ViewPolygon(new[]
+ {
+ new Vector2(0.4f, 0.4f), new Vector2(0.8f, 0.4f), new Vector2(0.8f, 0.8f), new Vector2(0.4f, 0.8f),
+ }));
+
+ var cps = ClipPlaneSet.From(cv);
+
+ // MUST NOT emit a single polygon's convex planes (that would hide the other).
+ Assert.True(cps.UseScissorFallback);
+ Assert.Equal(0, cps.Count);
+ Assert.Empty(cps.Planes);
+
+ // Scissor AABB is the UNION of both polygons (over-include, safe).
+ Assert.Equal(-0.8f, cps.ScissorNdcAabb.X, 5); // minX
+ Assert.Equal(-0.8f, cps.ScissorNdcAabb.Y, 5); // minY
+ Assert.Equal(0.8f, cps.ScissorNdcAabb.Z, 5); // maxX
+ Assert.Equal(0.8f, cps.ScissorNdcAabb.W, 5); // maxY
+
+ // The far corner of the second square is inside the union AABB → not hidden.
+ Assert.True(0.7f >= cps.ScissorNdcAabb.X && 0.7f <= cps.ScissorNdcAabb.Z);
+ Assert.True(0.7f >= cps.ScissorNdcAabb.Y && 0.7f <= cps.ScissorNdcAabb.W);
+ }
+
+ // --- Distinguishing the three Count==0 states ----------------------------
+
+ [Fact]
+ public void Empty_IsNothingVisible_NotScissorFallback()
+ {
+ var e = ClipPlaneSet.From(new CellView());
+ Assert.Equal(0, e.Count);
+ Assert.False(e.UseScissorFallback); // NOT "draw the AABB box"
+ Assert.True(e.IsNothingVisible); // "draw nothing"
+ Assert.Empty(e.Planes);
+ }
+
+ [Fact]
+ public void Empty_StaticProperty_DrawsNothing()
+ {
+ var e = ClipPlaneSet.Empty;
+ Assert.Equal(0, e.Count);
+ Assert.False(e.UseScissorFallback);
+ Assert.True(e.IsNothingVisible);
+ // Degenerate AABB (min > max) so a consumer that naively scissors on it draws nothing.
+ Assert.True(e.ScissorNdcAabb.X > e.ScissorNdcAabb.Z);
+ Assert.True(e.ScissorNdcAabb.Y > e.ScissorNdcAabb.W);
+ }
+
+ [Fact]
+ public void ScissorFallback_IsNotNothingVisible()
+ {
+ var poly = RegularNgonCellView(n: 9, radius: 0.6f);
+ var cps = ClipPlaneSet.From(poly);
+ Assert.True(cps.UseScissorFallback);
+ Assert.False(cps.IsNothingVisible); // "draw the AABB box", not "draw nothing"
+ // The fallback AABB bounds the 9-gon (radius 0.6).
+ Assert.True(cps.ScissorNdcAabb.Z - cps.ScissorNdcAabb.X > 0.5f);
+ Assert.True(cps.ScissorNdcAabb.W - cps.ScissorNdcAabb.Y > 0.5f);
+ }
+
+ // --- Plane-sign correctness for every edge -------------------------------
+
+ [Fact]
+ public void From_Square_EveryEdgePlane_IsPositiveAtCenter_NegativeJustOutsideThatEdge()
+ {
+ var cps = ClipPlaneSet.From(SquareCellView(-0.5f, 0.5f));
+ Assert.Equal(4, cps.Count);
+
+ var center = new Vector4(0, 0, 0, 1);
+ // Inside ⇒ EVERY plane non-negative.
+ foreach (var p in cps.Planes)
+ Assert.True(Vector4.Dot(p, center) >= 0, $"plane {p} should be >=0 at center");
+
+ // For each of the four cardinal directions just outside the square, at least one
+ // plane goes negative (the edge facing that direction).
+ foreach (var pt in new[]
+ {
+ new Vector4(0.6f, 0, 0, 1), new Vector4(-0.6f, 0, 0, 1),
+ new Vector4(0, 0.6f, 0, 1), new Vector4(0, -0.6f, 0, 1),
+ })
+ {
+ Assert.Contains(cps.Planes, p => Vector4.Dot(p, pt) < 0);
+ }
+ }
+
+ [Fact]
+ public void From_Triangle_ThreePlanes_AllZeroZComponent()
+ {
+ var cv = new CellView();
+ cv.Add(new ViewPolygon(new[]
+ {
+ new Vector2(0f, 0.5f), new Vector2(-0.5f, -0.5f), new Vector2(0.5f, -0.5f), // CCW
+ }));
+ var cps = ClipPlaneSet.From(cv);
+ Assert.Equal(3, cps.Count);
+ foreach (var p in cps.Planes)
+ {
+ Assert.Equal(0f, p.Z, 6); // clip-space planes are (nx, ny, 0, d)
+ // Normal is unit length in xy.
+ Assert.Equal(1f, MathF.Sqrt(p.X * p.X + p.Y * p.Y), 4);
+ }
+ }
+
+ // --- Winding normalization: a CW square must still yield inside-positive planes.
+
+ [Fact]
+ public void From_ClockwiseSquare_NormalizesWinding_InsideStillPositive()
+ {
+ var cv = new CellView();
+ // Same square but wound CW (reverse order). Builder normally EnsureCcw's,
+ // but From must be robust to either winding.
+ cv.Add(new ViewPolygon(new[]
+ {
+ new Vector2(-0.5f, -0.5f), new Vector2(-0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, -0.5f),
+ }));
+ var cps = ClipPlaneSet.From(cv);
+ Assert.Equal(4, cps.Count);
+ var center = new Vector4(0, 0, 0, 1);
+ foreach (var p in cps.Planes)
+ Assert.True(Vector4.Dot(p, center) >= 0, $"plane {p} must be >=0 at center even for CW input");
+ }
+
+ // --- Collinear-edge merge: a square with a midpoint added on one edge → 4 planes.
+
+ [Fact]
+ public void From_SquareWithCollinearMidpoint_MergesToFourPlanes()
+ {
+ var cv = new CellView();
+ cv.Add(new ViewPolygon(new[]
+ {
+ new Vector2(-0.5f, -0.5f),
+ new Vector2(0f, -0.5f), // collinear midpoint on the bottom edge
+ new Vector2(0.5f, -0.5f),
+ new Vector2(0.5f, 0.5f),
+ new Vector2(-0.5f, 0.5f),
+ }));
+ var cps = ClipPlaneSet.From(cv);
+ Assert.Equal(4, cps.Count); // the redundant collinear edge is merged away
+ }
+
+ // --- Exactly 8 edges fit (octagon); 9 spills to scissor (after merge fails to help).
+
+ [Fact]
+ public void From_RegularOctagon_FitsInEightPlanes()
+ {
+ var cps = ClipPlaneSet.From(RegularNgonCellView(n: 8, radius: 0.6f));
+ Assert.False(cps.UseScissorFallback);
+ Assert.Equal(8, cps.Count);
+ var center = new Vector4(0, 0, 0, 1);
+ foreach (var p in cps.Planes)
+ Assert.True(Vector4.Dot(p, center) >= 0);
+ }
+
+ // --- Degenerate single polygon (all collinear after merge) → nothing convex → empty.
+
+ [Fact]
+ public void From_DegenerateSinglePolygon_IsNothingVisible()
+ {
+ // A ViewPolygon needs >=3 verts to be added, but they can be (nearly) collinear,
+ // leaving <3 distinct edges after merge → no convex region → treat as nothing visible.
+ var cv = new CellView();
+ cv.Add(new ViewPolygon(new[]
+ {
+ new Vector2(-0.5f, -0.5f), new Vector2(0f, 0f), new Vector2(0.5f, 0.5f),
+ }));
+ var cps = ClipPlaneSet.From(cv);
+ Assert.Equal(0, cps.Count);
+ // Degenerate sliver carries no area → safest is "nothing visible" (over-include would
+ // need a real AABB; a zero-area line has none).
+ Assert.True(cps.IsNothingVisible);
+ }
+}