Skip to content

Commit 228f20a

Browse files
author
DavidQ
committed
Add overlay layout constraints and safe zones.
PR Details: - Prevents overlays from blocking gameplay - Improves usability and readability
1 parent 3c4967c commit 228f20a

9 files changed

Lines changed: 312 additions & 74 deletions

docs/dev/CODEX_COMMANDS.md

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,10 @@ MODEL: GPT-5.4
22
REASONING: medium
33

44
COMMAND:
5-
Create BUILD_PR_LEVEL_19_6_OVERLAY_MULTI_LAYER_COMPOSITION as the next smallest executable/testable PR.
5+
Implement overlay layout constraints:
6+
- Define safe zones
7+
- Prevent overlays from covering critical gameplay areas
8+
- Validate across multi-layer overlays
9+
- Update roadmap status only
610

7-
Requirements:
8-
- Add multi-layer overlay composition for gameplay-safe overlays
9-
- Define and enforce deterministic render ordering for multiple active overlays
10-
- Prevent overlap/occlusion regressions in the composed state
11-
- Preserve existing shared non-Tab input mapping
12-
- Preserve gameplay-first input priority
13-
- Add or update focused tests validating composition order and non-interference
14-
- Update roadmap status for this PR using status markers only ([ ] [.] [x]); do not rewrite roadmap text
15-
- Do not modify start_of_day folders
16-
- Package the repo-structured ZIP to <project folder>/tmp/BUILD_PR_LEVEL_19_6_OVERLAY_MULTI_LAYER_COMPOSITION.zip
11+
Package ZIP to <project folder>/tmp/

docs/dev/COMMIT_COMMENT.txt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
Add multi-layer overlay composition for gameplay-safe overlays.
1+
Add overlay layout constraints and safe zones.
22

33
PR Details:
4-
- Enables deterministic rendering of multiple active overlays
5-
- Preserves gameplay-first input and shared non-Tab input mapping
6-
- Improves roadmap progress with a testable composition milestone
7-
- Roadmap update must be status-only
4+
- Prevents overlays from blocking gameplay
5+
- Improves usability and readability
Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
[ ] Gameplay sample with overlay support loads
2-
[ ] Multiple overlays can be active together
3-
[ ] Composition/render order is deterministic
4-
[ ] No overlay occludes required gameplay information unexpectedly
5-
[ ] Gameplay controls remain responsive with multiple overlays active
6-
[ ] No Tab-based interaction is introduced
7-
[ ] Roadmap status was updated using status markers only
1+
[ ] Overlays respect safe zones
2+
[ ] No gameplay obstruction
3+
[ ] Layout stable with multiple overlays
4+
[ ] Roadmap status updated

docs/dev/roadmaps/MASTER_ROADMAP_HIGH_LEVEL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -800,7 +800,7 @@
800800
### Track D — Debug & Observability Maturity
801801
- [ ] ensure all systems expose debug data
802802
- [ ] ensure providers are complete and consistent
803-
- [.] validate debug panels across systems
803+
- [x] validate debug panels across systems
804804
- [ ] confirm production-safe debug toggling
805805

806806
### Track E — Toolchain Validation

docs/pr/BUILD_PR.md

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,22 @@
1-
# BUILD_PR_LEVEL_19_6_OVERLAY_MULTI_LAYER_COMPOSITION
1+
# BUILD_PR_LEVEL_19_7_OVERLAY_LAYOUT_CONSTRAINTS_AND_SAFE_ZONES
22

33
## Purpose
4-
Add testable multi-layer overlay composition so gameplay-safe overlays can render together in a predictable stack without visual conflicts.
4+
Introduce layout constraints and safe zones to ensure overlays never block critical gameplay areas.
55

66
## Roadmap Improvement
7-
This PR advances Level 19 from stable overlay input handling to stable multi-overlay composition during gameplay.
7+
Advances Level 19 by ensuring visual safety and usability of multi-layer overlays.
88

99
## Scope
10-
- Support composition of multiple active overlays in the same gameplay session
11-
- Define deterministic layer ordering for composed overlays
12-
- Prevent overlap and occlusion regressions caused by composed overlay rendering
13-
- Validate composition in at least one gameplay-active sample
14-
15-
## Included
16-
- Multi-layer composition rules for gameplay overlays
17-
- Deterministic render ordering for composed overlays
18-
- Focused validation for overlap, occlusion, and ordering behavior
19-
- Status-only roadmap update instruction for this PR
20-
21-
## Excluded
22-
- New overlay feature families
23-
- Mission-system expansion
24-
- Telemetry-system expansion
25-
- Visual redesign of existing overlays
26-
- Repo-wide render pipeline changes
27-
28-
## Execution Notes
29-
- Preserve existing non-Tab overlay input behavior
30-
- Preserve gameplay-first input priority
31-
- Keep scope to the smallest executable/testable change
32-
- Do not modify start_of_day folders
33-
- Update roadmap status only; do not rewrite roadmap text
10+
- Define safe zones for gameplay-critical regions
11+
- Constrain overlay placement to avoid conflicts
12+
- Validate layout across multiple overlays
3413

3514
## Test Steps
36-
1. Load a gameplay-active sample with overlay support
37-
2. Activate multiple overlays in the same session
38-
3. Verify render order matches the composition contract
39-
4. Verify overlays remain readable and do not hide required gameplay information
40-
5. Verify gameplay controls continue to work while multiple overlays are active
41-
42-
## Expected Result
43-
- Multiple overlays can render together predictably
44-
- Layer order remains stable
45-
- No visual conflict or gameplay-input regression is introduced
15+
1. Load gameplay sample
16+
2. Activate multiple overlays
17+
3. Verify overlays stay within safe zones
18+
4. Confirm gameplay visibility preserved
19+
20+
## Expected
21+
- No overlay blocks critical gameplay
22+
- Layout remains stable and readable

samples/phase-17/1708/RealGameplayMiniGameScene.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,20 @@ export default class RealGameplayMiniGameScene extends Scene {
248248
};
249249
}
250250

251+
getOverlayLayoutSafeZones() {
252+
const insetX = 96;
253+
const insetY = 66;
254+
return [
255+
{
256+
id: 'critical-gameplay-area',
257+
x: this.viewport.x + insetX,
258+
y: this.viewport.y + insetY,
259+
width: Math.max(240, this.viewport.width - insetX * 2),
260+
height: Math.max(160, this.viewport.height - insetY * 2),
261+
},
262+
];
263+
}
264+
251265
pushCollisionRow(overlayId, kind, state, enabled = true) {
252266
this.debugCollisionRows.push({
253267
overlayId,

samples/phase-17/1710/RealGameplayMiniGameScene.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,20 @@ export default class RealGameplayMiniGameScene extends Scene {
248248
};
249249
}
250250

251+
getOverlayLayoutSafeZones() {
252+
const insetX = 96;
253+
const insetY = 66;
254+
return [
255+
{
256+
id: 'critical-gameplay-area',
257+
x: this.viewport.x + insetX,
258+
y: this.viewport.y + insetY,
259+
width: Math.max(240, this.viewport.width - insetX * 2),
260+
height: Math.max(160, this.viewport.height - insetY * 2),
261+
},
262+
];
263+
}
264+
251265
pushCollisionRow(overlayId, kind, state, enabled = true) {
252266
this.debugCollisionRows.push({
253267
overlayId,

samples/phase-17/shared/overlayGameplayRuntime.js

Lines changed: 182 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,59 @@ function normalizeInteractionIndex(runtime) {
8282
return normalized;
8383
}
8484

85+
function normalizeSafeZoneEntry(entry) {
86+
if (!entry || typeof entry !== 'object') {
87+
return null;
88+
}
89+
const x = Number(entry.x);
90+
const y = Number(entry.y);
91+
const width = Number(entry.width);
92+
const height = Number(entry.height);
93+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(width) || !Number.isFinite(height)) {
94+
return null;
95+
}
96+
if (width <= 0 || height <= 0) {
97+
return null;
98+
}
99+
return Object.freeze({
100+
id: String(entry.id || '').trim(),
101+
x,
102+
y,
103+
width,
104+
height,
105+
});
106+
}
107+
108+
function normalizeSafeZones(safeZones) {
109+
if (!Array.isArray(safeZones) || safeZones.length === 0) {
110+
return Object.freeze([]);
111+
}
112+
const normalized = [];
113+
for (let i = 0; i < safeZones.length; i += 1) {
114+
const candidate = normalizeSafeZoneEntry(safeZones[i]);
115+
if (!candidate) {
116+
continue;
117+
}
118+
normalized.push(candidate);
119+
}
120+
return Object.freeze(normalized);
121+
}
122+
123+
function resolveLayoutSafeZones(context = {}) {
124+
const fromContext = normalizeSafeZones(context?.safeZones);
125+
if (fromContext.length > 0) {
126+
return fromContext;
127+
}
128+
if (typeof context?.scene?.getOverlayLayoutSafeZones === 'function') {
129+
return normalizeSafeZones(context.scene.getOverlayLayoutSafeZones());
130+
}
131+
return Object.freeze([]);
132+
}
133+
134+
function rectsOverlap(a, b) {
135+
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
136+
}
137+
85138
function getComposedRuntimeFrames(runtime, activeOverlayId) {
86139
if (!runtime || !Array.isArray(runtime.runtimeExtensions) || runtime.runtimeExtensions.length === 0) {
87140
return [];
@@ -118,7 +171,7 @@ function getComposedRuntimeFrames(runtime, activeOverlayId) {
118171
return frames;
119172
}
120173

121-
function attachCompositionSlots(frames, renderer) {
174+
function attachCompositionSlots(frames, renderer, safeZones = []) {
122175
if (!Array.isArray(frames) || frames.length === 0) {
123176
return frames || [];
124177
}
@@ -128,22 +181,135 @@ function attachCompositionSlots(frames, renderer) {
128181
const height = Math.max(180, Number(canvasSize.height) || 540);
129182
const margin = 16;
130183
const gap = 10;
131-
let cursorY = height - margin;
184+
const anchorCounts = {
185+
'bottom-right': 0,
186+
'top-right': 0,
187+
'bottom-left': 0,
188+
'top-left': 0,
189+
};
190+
const placedSlots = [];
191+
192+
function createAnchorSlot(anchor, slotWidth, slotHeight, index = 0) {
193+
const stackOffset = index * (slotHeight + gap);
194+
const x = anchor.endsWith('right')
195+
? Math.round(width - margin - slotWidth)
196+
: Math.round(margin);
197+
const y = anchor.startsWith('bottom')
198+
? Math.round(height - margin - slotHeight - stackOffset)
199+
: Math.round(margin + stackOffset);
200+
return {
201+
x,
202+
y,
203+
width: slotWidth,
204+
height: slotHeight,
205+
anchor,
206+
};
207+
}
208+
209+
function isSlotWithinBounds(slot) {
210+
if (!slot) {
211+
return false;
212+
}
213+
return !(slot.x < 0 || slot.y < 0 || slot.x + slot.width > width || slot.y + slot.height > height);
214+
}
215+
216+
function slotOverlapsPlaced(slot) {
217+
if (!slot) {
218+
return false;
219+
}
220+
for (let i = 0; i < placedSlots.length; i += 1) {
221+
if (rectsOverlap(slot, placedSlots[i])) {
222+
return true;
223+
}
224+
}
225+
return false;
226+
}
227+
228+
function slotOverlapsSafeZones(slot) {
229+
if (!slot) {
230+
return false;
231+
}
232+
for (let i = 0; i < safeZones.length; i += 1) {
233+
if (rectsOverlap(slot, safeZones[i])) {
234+
return true;
235+
}
236+
}
237+
return false;
238+
}
239+
240+
function isSlotUsable(slot) {
241+
if (!isSlotWithinBounds(slot)) {
242+
return false;
243+
}
244+
return !slotOverlapsPlaced(slot) && !slotOverlapsSafeZones(slot);
245+
}
132246

133247
for (let i = 0; i < frames.length; i += 1) {
134248
const frame = frames[i];
135249
const slotWidth = Math.max(120, Number(frame.extension.panelWidth) || 260);
136250
const slotHeight = Math.max(32, Number(frame.extension.panelHeight) || 96);
137-
const slotX = Math.round(width - margin - slotWidth);
138-
const slotY = Math.round(cursorY - slotHeight);
251+
const anchorOrder = ['bottom-right', 'top-right', 'bottom-left', 'top-left'];
252+
253+
let slot = null;
254+
for (let j = 0; j < anchorOrder.length; j += 1) {
255+
const anchor = anchorOrder[j];
256+
const startStackIndex = anchorCounts[anchor] || 0;
257+
const maxStackIndexExclusive = Math.max(
258+
startStackIndex + 1,
259+
startStackIndex + frames.length + safeZones.length + 2
260+
);
261+
for (let stackIndex = startStackIndex; stackIndex < maxStackIndexExclusive; stackIndex += 1) {
262+
const candidate = createAnchorSlot(anchor, slotWidth, slotHeight, stackIndex);
263+
if (!isSlotUsable(candidate)) {
264+
continue;
265+
}
266+
slot = candidate;
267+
anchorCounts[anchor] = stackIndex + 1;
268+
break;
269+
}
270+
if (slot) {
271+
break;
272+
}
273+
}
274+
275+
if (!slot) {
276+
for (let j = 0; j < anchorOrder.length; j += 1) {
277+
const anchor = anchorOrder[j];
278+
const startStackIndex = anchorCounts[anchor] || 0;
279+
const maxStackIndexExclusive = Math.max(
280+
startStackIndex + 1,
281+
startStackIndex + frames.length + safeZones.length + 2
282+
);
283+
for (let stackIndex = startStackIndex; stackIndex < maxStackIndexExclusive; stackIndex += 1) {
284+
const candidate = createAnchorSlot(anchor, slotWidth, slotHeight, stackIndex);
285+
if (!isSlotWithinBounds(candidate) || slotOverlapsPlaced(candidate)) {
286+
continue;
287+
}
288+
slot = candidate;
289+
anchorCounts[anchor] = stackIndex + 1;
290+
break;
291+
}
292+
if (slot) {
293+
break;
294+
}
295+
}
296+
}
297+
298+
if (!slot) {
299+
const fallbackAnchor = 'bottom-right';
300+
const fallbackStackIndex = anchorCounts[fallbackAnchor] || 0;
301+
slot = createAnchorSlot(fallbackAnchor, slotWidth, slotHeight, fallbackStackIndex);
302+
anchorCounts[fallbackAnchor] = fallbackStackIndex + 1;
303+
}
304+
139305
frame.slot = Object.freeze({
140-
x: slotX,
141-
y: slotY,
142-
width: slotWidth,
143-
height: slotHeight,
144-
anchor: 'bottom-right',
306+
x: slot.x,
307+
y: slot.y,
308+
width: slot.width,
309+
height: slot.height,
310+
anchor: slot.anchor,
145311
});
146-
cursorY = slotY - gap;
312+
placedSlots.push(slot);
147313
}
148314

149315
return frames;
@@ -203,9 +369,11 @@ export function getOverlayGameplayRuntimeInteractionSnapshot(runtime) {
203369

204370
export function getOverlayGameplayRuntimeCompositionSnapshot(runtime, context = {}) {
205371
const activeOverlayId = String(context?.activeOverlayId || '').trim();
372+
const safeZones = resolveLayoutSafeZones(context);
206373
const frames = attachCompositionSlots(
207374
getComposedRuntimeFrames(runtime, activeOverlayId),
208-
context?.renderer
375+
context?.renderer,
376+
safeZones
209377
);
210378
return frames.map((frame, index) => ({
211379
index,
@@ -352,9 +520,11 @@ export function renderOverlayGameplayRuntime(runtime, context = {}) {
352520
}
353521

354522
const activeOverlayId = String(context.activeOverlayId || '').trim();
523+
const safeZones = resolveLayoutSafeZones(context);
355524
const frames = attachCompositionSlots(
356525
getComposedRuntimeFrames(runtime, activeOverlayId),
357-
context.renderer
526+
context.renderer,
527+
safeZones
358528
);
359529
if (frames.length === 0) {
360530
return 0;

0 commit comments

Comments
 (0)