Rect Architecture: Memory Layout, Coordinate Boundaries, and Collision Logic
A technical guide to Rect primitives in UI and graphics engines, covering memory layout, coordinate-space boundaries, AABB collision logic, and degenerate-case handling.
03 Sept 2025, 11:10 UTC

Why Rect Boundaries Break Layout Engines
A rectangle is the default unit of layout in most 2D UI frameworks and graphics engines. It looks trivial—four numbers—but the same primitive is used for positioning, hit-testing, clipping, and spatial culling. When those uses share one representation without clear rules, the result is flickering borders, missed clicks, or recursive culling loops. The useful takeaway is that a Rect is not just a data holder; it is a boundary contract that must define coordinate space, edge inclusivity, and degenerate-case behavior before it enters a scene tree.
Requirements for a Production Rect Primitive
Before choosing a layout, define what the Rect must support:
- Layout positioning: Storing an origin and size relative to a parent.
- Hit-testing: Determining whether a point or another Rect overlaps this one.
- Culling: Quickly discarding off-screen subtrees during traversal.
- Transformation: Mapping between local and global coordinate spaces without accumulating error.
If rotation or non-uniform scaling is required, an axis-aligned Rect is insufficient and the design must change (see the final section).
Smallest Suitable Design: Origin vs. Edge Representation
Two representations cover nearly all 2D engines:
| Representation | Fields | Best for | Trade-off |
|---|---|---|---|
| Origin-based | x, y, width, height | Layout and resizing | Right/bottom edges require addition during collision checks |
| Edge-based | minX, minY, maxX, maxY | Collision and culling | Less intuitive for width/height math; invariant minX ≤ maxX must be maintained |
For engines that process large Rect arrays—such as hit-test buffers or culling lists—storing edges as float32 pairs allows SIMD (Single Instruction, Multiple Data) loads and comparisons. The layout choice should follow the dominant operation: layout-heavy trees favor origin-based, while render/culling-heavy systems favor edge-based.
Trust and Data Boundaries: Local vs. Global Space
The most common Rect bug is mixing coordinate spaces. A child Rect is usually stored in local space, where (0,0) is its parent's top-left corner. Mouse input and screen culling operate in global space. Without a strict boundary, a nested component might test against the wrong rectangle.
Establish a policy: a Rect is either local or global, never both. When a parent transform changes, mark children dirty and recompute their global bounds during traversal rather than mutating the stored local values. If a Rect is scaled or rotated, its axis-aligned bounds no longer describe the shape accurately; in that case the global bounds must be treated as an approximation or replaced with an oriented bounding box.
Operational Checks: Hit-Testing and Intersection
AABB (Axis-Aligned Bounding Box) intersection is O(1) and requires four comparisons. The subtle part is edge inclusivity. Decide whether a point exactly on maxX or maxY is inside. Most engines use an exclusive rule for right and bottom edges to avoid double-counting adjacent pixels or elements.
The following pseudocode shows an intersection test with an epsilon guard for floating-point gaps. Run this in your engine's geometry utility module or a unit-test harness; it requires no special permissions.
// Pseudocode: run in a geometry utility or test harness
const EPSILON = 0.00001;
function intersects(a, b) {
// a and b are {x, y, width, height}
return (
a.x < b.x + b.width - EPSILON &&
a.x + a.width > b.x + EPSILON &&
a.y < b.y + b.height - EPSILON &&
a.y + a.height > b.y + EPSILON
);
}
function contains(rect, px, py) {
// Exclusive right/bottom edges
return px >= rect.x &&
px < rect.x + rect.width &&
py >= rect.y &&
py < rect.y + rect.height;
}
Expected check: Two Rects that share an edge should return false for intersection. A point exactly on the right edge should return false from contains. A zero-width Rect should not crash either function.
Failure Modes and Degenerate Cases
Rects with zero or negative dimensions are the most common source of crashes in recursive algorithms:
- Zero-area Rects: If culling logic divides by area or recursively splits space, a zero-width or zero-height Rect can cause division-by-zero or infinite recursion.
- Negative dimensions: Often produced by inverted scroll offsets or bad resize math. They break the
minX ≤ maxXinvariant and can make hit-tests return inverted results. - Floating-point gaps: Adjacent Rects computed independently may not share an exact edge. Without an epsilon comparison, a point on the seam can fall through both.
Guard against these by normalizing Rects at construction: clamp negative sizes to zero, or reject them according to engine policy. For culling, treat zero-area Rects as invisible and skip them before any division or recursion.
Conditions That Would Change the Design
The origin/edge choice and AABB logic hold only under specific constraints:
- Rotation: Once a Rect rotates, AABB is no longer sufficient. Switch to an Oriented Bounding Box (OBB) or transform the point into the Rect's local space before testing.
- Non-uniform scaling: Scaling width and height by different factors can turn a square into a non-square AABB, but the axis-aligned assumption remains valid only if the transform has no rotation.
- 3D or perspective: Screen-space Rects become approximations. Depth testing and perspective-correct interpolation require volume or plane primitives instead.
Practical Verification
To validate your implementation without a full engine, write a small test harness that constructs Rects at known coordinates and asserts the boundary behavior described above. Check inclusive/exclusive edges, zero-area handling, and nested local-to-global transforms. If the tests pass, the primitive is ready for layout and hit-testing; if rotation enters the roadmap, revisit the bounding-volume choice before it reaches the renderer.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.