Skip to content

Commit e6298cd

Browse files
author
DavidQ
committed
Add overlay visual priority and readability rules.
PR Details: - Ensures clear overlay hierarchy - Improves readability under multi-overlay conditions
1 parent 228f20a commit e6298cd

8 files changed

Lines changed: 245 additions & 36 deletions

File tree

docs/dev/CODEX_COMMANDS.md

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

44
COMMAND:
5-
Implement overlay layout constraints:
6-
- Define safe zones
7-
- Prevent overlays from covering critical gameplay areas
8-
- Validate across multi-layer overlays
5+
Implement overlay visual priority:
6+
- Define hierarchy rules
7+
- Ensure readability under multi-layer conditions
8+
- Prevent visual clutter
99
- Update roadmap status only
1010

1111
Package ZIP to <project folder>/tmp/

docs/dev/COMMIT_COMMENT.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
Add overlay layout constraints and safe zones.
1+
Add overlay visual priority and readability rules.
22

33
PR Details:
4-
- Prevents overlays from blocking gameplay
5-
- Improves usability and readability
4+
- Ensures clear overlay hierarchy
5+
- Improves readability under multi-overlay conditions
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
Roadmap Status Update Instruction:
2-
- Update the roadmap entry for BUILD_PR_LEVEL_19_6_OVERLAY_MULTI_LAYER_COMPOSITION using status markers only
3-
- Allowed changes: [ ] -> [.] -> [x]
4-
- Do not rewrite, move, or delete roadmap text
5-
- Do not perform cleanup edits outside the status change needed for this PR
1+
Roadmap Status Update:
2+
- Update status markers only ([ ] [.] [x])
3+
- Do not modify roadmap text
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[ ] Overlays respect safe zones
2-
[ ] No gameplay obstruction
3-
[ ] Layout stable with multiple overlays
1+
[ ] Visual hierarchy consistent
2+
[ ] Text/UI readable
3+
[ ] No clutter issues
44
[ ] 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
@@ -799,7 +799,7 @@
799799

800800
### Track D — Debug & Observability Maturity
801801
- [ ] ensure all systems expose debug data
802-
- [ ] ensure providers are complete and consistent
802+
- [.] ensure providers are complete and consistent
803803
- [x] validate debug panels across systems
804804
- [ ] confirm production-safe debug toggling
805805

docs/pr/BUILD_PR.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
1-
# BUILD_PR_LEVEL_19_7_OVERLAY_LAYOUT_CONSTRAINTS_AND_SAFE_ZONES
1+
# BUILD_PR_LEVEL_19_8_OVERLAY_VISUAL_PRIORITY_AND_READABILITY
22

33
## Purpose
4-
Introduce layout constraints and safe zones to ensure overlays never block critical gameplay areas.
4+
Ensure overlays maintain visual priority and readability when multiple overlays are active.
55

66
## Roadmap Improvement
7-
Advances Level 19 by ensuring visual safety and usability of multi-layer overlays.
7+
Advances Level 19 by guaranteeing readable, non-conflicting overlay presentation.
88

99
## Scope
10-
- Define safe zones for gameplay-critical regions
11-
- Constrain overlay placement to avoid conflicts
12-
- Validate layout across multiple overlays
10+
- Define visual priority rules between overlays
11+
- Ensure text and UI elements remain readable
12+
- Prevent clutter from multi-overlay stacking
1313

1414
## Test Steps
15-
1. Load gameplay sample
16-
2. Activate multiple overlays
17-
3. Verify overlays stay within safe zones
18-
4. Confirm gameplay visibility preserved
15+
1. Activate multiple overlays
16+
2. Verify priority ordering is respected
17+
3. Confirm readability (text/UI clarity)
18+
4. Validate no visual clutter blocks usability
1919

2020
## Expected
21-
- No overlay blocks critical gameplay
22-
- Layout remains stable and readable
21+
- Clear visual hierarchy
22+
- Readable overlays
23+
- No clutter interference

samples/phase-17/shared/overlayGameplayRuntime.js

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ function normalizeRuntimeExtensionEntry(entry) {
2424

2525
const layerOrderRaw = Number(entry.layerOrder);
2626
const layerOrder = Number.isFinite(layerOrderRaw) ? layerOrderRaw : 0;
27+
const visualPriorityRaw = Number(entry.visualPriority);
28+
const visualPriority = Number.isFinite(visualPriorityRaw) ? visualPriorityRaw : layerOrder;
2729
const compose = entry.compose === true;
2830
const panelWidthRaw = Number(entry.panelWidth);
2931
const panelHeightRaw = Number(entry.panelHeight);
@@ -36,6 +38,7 @@ function normalizeRuntimeExtensionEntry(entry) {
3638
onRender,
3739
compose,
3840
layerOrder,
41+
visualPriority,
3942
panelWidth,
4043
panelHeight,
4144
});
@@ -171,6 +174,87 @@ function getComposedRuntimeFrames(runtime, activeOverlayId) {
171174
return frames;
172175
}
173176

177+
function deriveRenderHierarchy(frames) {
178+
if (!Array.isArray(frames) || frames.length === 0) {
179+
return [];
180+
}
181+
182+
const ordered = [...frames];
183+
ordered.sort((left, right) => {
184+
const leftIsActive = left.isActive === true;
185+
const rightIsActive = right.isActive === true;
186+
if (leftIsActive !== rightIsActive) {
187+
return leftIsActive ? 1 : -1;
188+
}
189+
if (left.extension.visualPriority !== right.extension.visualPriority) {
190+
return left.extension.visualPriority - right.extension.visualPriority;
191+
}
192+
if (left.extension.layerOrder !== right.extension.layerOrder) {
193+
return left.extension.layerOrder - right.extension.layerOrder;
194+
}
195+
return left.registrationIndex - right.registrationIndex;
196+
});
197+
198+
for (let i = 0; i < ordered.length; i += 1) {
199+
const frame = ordered[i];
200+
frame.visualPriorityRank = i;
201+
frame.visualTier = frame.isActive === true ? 'primary' : 'secondary';
202+
frame.readabilityOpacity = frame.isActive === true ? 1 : 0.84;
203+
frame.hiddenByClutter = false;
204+
}
205+
206+
return ordered;
207+
}
208+
209+
function resolveMaxVisibleCompositionLayers(renderer) {
210+
const canvasSize = renderer?.getCanvasSize?.() || { width: 960, height: 540 };
211+
const height = Math.max(180, Number(canvasSize.height) || 540);
212+
if (height <= 360) {
213+
return 2;
214+
}
215+
if (height <= 540) {
216+
return 3;
217+
}
218+
return 4;
219+
}
220+
221+
function applyCompositionReadabilityLimits(frames, renderer) {
222+
if (!Array.isArray(frames) || frames.length === 0) {
223+
return [];
224+
}
225+
226+
const maxVisibleLayers = Math.max(2, resolveMaxVisibleCompositionLayers(renderer));
227+
for (let i = 0; i < frames.length; i += 1) {
228+
frames[i].hiddenByClutter = false;
229+
}
230+
if (frames.length <= maxVisibleLayers) {
231+
return frames;
232+
}
233+
234+
const activeFrame = frames.find((frame) => frame.isActive === true) || null;
235+
const selected = [];
236+
if (activeFrame) {
237+
const supportFrames = frames.filter((frame) => frame !== activeFrame);
238+
const supportLimit = Math.max(0, maxVisibleLayers - 1);
239+
const start = Math.max(0, supportFrames.length - supportLimit);
240+
for (let i = start; i < supportFrames.length; i += 1) {
241+
selected.push(supportFrames[i]);
242+
}
243+
selected.push(activeFrame);
244+
} else {
245+
const start = Math.max(0, frames.length - maxVisibleLayers);
246+
for (let i = start; i < frames.length; i += 1) {
247+
selected.push(frames[i]);
248+
}
249+
}
250+
251+
const selectedSet = new Set(selected);
252+
for (let i = 0; i < frames.length; i += 1) {
253+
frames[i].hiddenByClutter = !selectedSet.has(frames[i]);
254+
}
255+
return frames.filter((frame) => selectedSet.has(frame));
256+
}
257+
174258
function attachCompositionSlots(frames, renderer, safeZones = []) {
175259
if (!Array.isArray(frames) || frames.length === 0) {
176260
return frames || [];
@@ -370,16 +454,24 @@ export function getOverlayGameplayRuntimeInteractionSnapshot(runtime) {
370454
export function getOverlayGameplayRuntimeCompositionSnapshot(runtime, context = {}) {
371455
const activeOverlayId = String(context?.activeOverlayId || '').trim();
372456
const safeZones = resolveLayoutSafeZones(context);
373-
const frames = attachCompositionSlots(
457+
const frames = deriveRenderHierarchy(attachCompositionSlots(
374458
getComposedRuntimeFrames(runtime, activeOverlayId),
375459
context?.renderer,
376460
safeZones
377-
);
461+
));
462+
const visibleFrames = applyCompositionReadabilityLimits(frames, context?.renderer);
463+
const visibleSet = new Set(visibleFrames);
378464
return frames.map((frame, index) => ({
379465
index,
380466
count: frames.length,
467+
visibleCount: visibleFrames.length,
381468
registrationIndex: frame.registrationIndex,
382469
layerOrder: frame.extension.layerOrder,
470+
visualPriority: frame.extension.visualPriority,
471+
visualPriorityRank: frame.visualPriorityRank,
472+
visualTier: frame.visualTier,
473+
readabilityOpacity: frame.readabilityOpacity,
474+
hiddenByClutter: !visibleSet.has(frame),
383475
compose: frame.extension.compose === true,
384476
isActive: frame.isActive === true,
385477
overlayId: frame.extension.overlayId,
@@ -521,10 +613,15 @@ export function renderOverlayGameplayRuntime(runtime, context = {}) {
521613

522614
const activeOverlayId = String(context.activeOverlayId || '').trim();
523615
const safeZones = resolveLayoutSafeZones(context);
524-
const frames = attachCompositionSlots(
525-
getComposedRuntimeFrames(runtime, activeOverlayId),
526-
context.renderer,
527-
safeZones
616+
const frames = applyCompositionReadabilityLimits(
617+
deriveRenderHierarchy(
618+
attachCompositionSlots(
619+
getComposedRuntimeFrames(runtime, activeOverlayId),
620+
context.renderer,
621+
safeZones
622+
)
623+
),
624+
context.renderer
528625
);
529626
if (frames.length === 0) {
530627
return 0;
@@ -544,6 +641,11 @@ export function renderOverlayGameplayRuntime(runtime, context = {}) {
544641
count: frames.length,
545642
registrationIndex: frame.registrationIndex,
546643
layerOrder: frame.extension.layerOrder,
644+
visualPriority: frame.extension.visualPriority,
645+
visualPriorityRank: frame.visualPriorityRank,
646+
visualTier: frame.visualTier,
647+
readabilityOpacity: frame.readabilityOpacity,
648+
hiddenByClutter: frame.hiddenByClutter === true,
547649
compose: frame.extension.compose === true,
548650
isActive: frame.isActive === true,
549651
slot: frame.slot,

tests/runtime/Phase17OverlayMultiLayerComposition.test.mjs

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ function assertDeterministicCompositionOrderingAndSlots() {
131131
);
132132
assert.deepEqual(
133133
renderOrder.map((token) => token.split(':')[0]),
134-
['mission', 'base', 'telemetry'],
135-
'Composed runtime render ordering should be deterministic by layer order.'
134+
['mission', 'telemetry', 'base'],
135+
'Composed runtime render ordering should prioritize active readability while preserving deterministic secondary ordering.'
136136
);
137137

138138
const snapshot = getOverlayGameplayRuntimeCompositionSnapshot(runtime, {
@@ -141,6 +141,16 @@ function assertDeterministicCompositionOrderingAndSlots() {
141141
safeZones,
142142
});
143143
assert.equal(snapshot.length, 3, 'Composition snapshot should include all composed layers.');
144+
assert.equal(
145+
snapshot.filter((entry) => entry.hiddenByClutter === true).length,
146+
0,
147+
'Composition snapshot should keep all layers visible when layer count is within readability limits.'
148+
);
149+
assert.equal(
150+
snapshot[snapshot.length - 1].isActive,
151+
true,
152+
'Active overlay should render with highest visual readability priority.'
153+
);
144154
for (let i = 1; i < snapshot.length; i += 1) {
145155
const prev = snapshot[i - 1].slot;
146156
const curr = snapshot[i].slot;
@@ -188,6 +198,103 @@ function assertComposedRuntimeDoesNotInterfereWithGameplayInput() {
188198
assert.equal(counters.composed > 0, true, 'Composed overlay runtime step should execute alongside active layer.');
189199
}
190200

201+
function assertVisualPriorityReadabilityAndClutterControl() {
202+
const renderOrder = [];
203+
const runtime = createOverlayGameplayRuntime({
204+
runtimeExtensions: [
205+
{
206+
overlayId: '',
207+
compose: true,
208+
layerOrder: 5,
209+
panelWidth: 220,
210+
panelHeight: 86,
211+
onStep() {},
212+
onRender() {
213+
renderOrder.push('low');
214+
},
215+
},
216+
{
217+
overlayId: '',
218+
compose: true,
219+
layerOrder: 10,
220+
panelWidth: 220,
221+
panelHeight: 86,
222+
onStep() {},
223+
onRender() {
224+
renderOrder.push('mid');
225+
},
226+
},
227+
{
228+
overlayId: '',
229+
compose: true,
230+
layerOrder: 15,
231+
panelWidth: 220,
232+
panelHeight: 86,
233+
onStep() {},
234+
onRender() {
235+
renderOrder.push('upper');
236+
},
237+
},
238+
{
239+
overlayId: '',
240+
compose: true,
241+
layerOrder: 20,
242+
panelWidth: 220,
243+
panelHeight: 86,
244+
onStep() {},
245+
onRender() {
246+
renderOrder.push('top-support');
247+
},
248+
},
249+
{
250+
overlayId: '',
251+
layerOrder: 12,
252+
panelWidth: 220,
253+
panelHeight: 86,
254+
onStep() {},
255+
onRender(context) {
256+
renderOrder.push('active');
257+
assert.equal(
258+
context.overlayComposition.visualTier,
259+
'primary',
260+
'Active overlay should receive primary visual tier.'
261+
);
262+
assert.equal(
263+
context.overlayComposition.readabilityOpacity,
264+
1,
265+
'Active overlay should keep full readability opacity.'
266+
);
267+
},
268+
},
269+
],
270+
});
271+
runtime.interactionIndex = 4;
272+
273+
const renderer = createRendererProbe(960, 320);
274+
const renderInvoked = renderOverlayGameplayRuntime(runtime, {
275+
activeOverlayId: 'ui-layer',
276+
renderer,
277+
});
278+
assert.equal(renderInvoked, 2, 'Readability limits should prevent visual clutter by capping visible layers on compact canvases.');
279+
assert.deepEqual(
280+
renderOrder,
281+
['top-support', 'active'],
282+
'Render hierarchy should keep only top-priority support layer plus active layer when clutter limits apply.'
283+
);
284+
285+
const snapshot = getOverlayGameplayRuntimeCompositionSnapshot(runtime, {
286+
activeOverlayId: 'ui-layer',
287+
renderer,
288+
});
289+
const hiddenLayers = snapshot.filter((entry) => entry.hiddenByClutter === true);
290+
assert.equal(hiddenLayers.length, 3, 'Snapshot should identify lower-priority overlays hidden by clutter control.');
291+
assert.equal(
292+
snapshot.filter((entry) => entry.hiddenByClutter !== true).length,
293+
2,
294+
'Snapshot should expose the exact number of visible layers after readability limiting.'
295+
);
296+
}
297+
191298
function assertSceneSafeZonesProtectGameplayViewport() {
192299
const renderer = createRendererProbe();
193300
const runtime = createOverlayGameplayRuntime({
@@ -242,5 +349,6 @@ function assertSceneSafeZonesProtectGameplayViewport() {
242349
export function run() {
243350
assertDeterministicCompositionOrderingAndSlots();
244351
assertComposedRuntimeDoesNotInterfereWithGameplayInput();
352+
assertVisualPriorityReadabilityAndClutterControl();
245353
assertSceneSafeZonesProtectGameplayViewport();
246354
}

0 commit comments

Comments
 (0)