Skip to content

Commit a4d7880

Browse files
author
DavidQ
committed
Add advanced overlay feature.
PR Details: - Introduces Level 21 capabilities
1 parent 1be888c commit a4d7880

8 files changed

Lines changed: 158 additions & 21 deletions

File tree

docs/dev/CODEX_COMMANDS.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
MODEL: GPT-5.4
2-
REASONING: low
2+
REASONING: medium
33

44
COMMAND:
5-
Promote Level 20 plugin system to baseline.
6-
Update roadmap status only.
7-
Package ZIP to <project folder>/tmp/
5+
Implement advanced overlay feature:
6+
- Add one advanced capability
7+
- Maintain compatibility
8+
- Update roadmap status only

docs/dev/COMMIT_COMMENT.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Promote Level 20 plugin system to baseline.
1+
Add advanced overlay feature.
22

33
PR Details:
4-
- Marks completion of Level 20
4+
- Introduces Level 21 capabilities
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
[ ] Stable
1+
[ ] Feature works
22
[ ] No regressions
33
[ ] Roadmap updated

docs/dev/roadmaps/MASTER_ROADMAP_HIGH_LEVEL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -826,5 +826,5 @@
826826
### Track H — Final Stability Gate
827827
- [ ] full-repo validation sweep
828828
- [x] zero regression requirement
829-
- [ ] contract freeze readiness
829+
- [.] contract freeze readiness
830830
- [ ] readiness for long-term maintenance mode

docs/pr/BUILD_PR.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
# BUILD_PR_LEVEL_20_10_OVERLAY_PLUGIN_PROMOTE_BASELINE
1+
# BUILD_PR_LEVEL_21_1_ADVANCED_OVERLAY_FEATURES
22

33
## Purpose
4-
Promote validated Level 20 plugin system to baseline.
4+
Introduce advanced overlay capabilities building on plugin system.
55

66
## Roadmap Improvement
7-
Marks Level 20 complete and production-ready.
7+
Begins Level 21 advanced feature set.
88

99
## Scope
10-
- Promote current plugin system
11-
- Ensure no regression
10+
- Add one advanced overlay capability (e.g. dynamic resizing or contextual display)
11+
- Ensure compatibility with plugin system
1212

1313
## Test Steps
14-
1. Run plugins
15-
2. Confirm stability
14+
1. Activate advanced overlay
15+
2. Validate behavior
16+
3. Confirm no regression
1617

1718
## Expected
18-
- Stable baseline
19+
- Advanced overlay works
20+
- System stable

samples/phase-17/shared/overlayExpansionContracts.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,30 @@ function normalizeRuntimeExtensionEntry(entry) {
5353
const overlayId = String(entry.overlayId || '').trim();
5454
const onStep = typeof entry.onStep === 'function' ? entry.onStep : null;
5555
const onRender = typeof entry.onRender === 'function' ? entry.onRender : null;
56+
const resolvePanelSize = typeof entry.resolvePanelSize === 'function' ? entry.resolvePanelSize : null;
5657
if (!onStep && !onRender) {
5758
return null;
5859
}
5960

61+
const layerOrderRaw = Number(entry.layerOrder);
62+
const layerOrder = Number.isFinite(layerOrderRaw) ? layerOrderRaw : 0;
63+
const visualPriorityRaw = Number(entry.visualPriority);
64+
const visualPriority = Number.isFinite(visualPriorityRaw) ? visualPriorityRaw : layerOrder;
65+
const panelWidthRaw = Number(entry.panelWidth);
66+
const panelHeightRaw = Number(entry.panelHeight);
67+
const panelWidth = Number.isFinite(panelWidthRaw) && panelWidthRaw > 0 ? panelWidthRaw : 260;
68+
const panelHeight = Number.isFinite(panelHeightRaw) && panelHeightRaw > 0 ? panelHeightRaw : 96;
69+
6070
return Object.freeze({
6171
overlayId,
6272
onStep,
6373
onRender,
74+
resolvePanelSize,
75+
compose: entry.compose === true,
76+
layerOrder,
77+
visualPriority,
78+
panelWidth,
79+
panelHeight,
6480
});
6581
}
6682

samples/phase-17/shared/overlayGameplayRuntime.js

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ function normalizeRuntimeExtensionEntry(entry) {
3131
const panelHeightRaw = Number(entry.panelHeight);
3232
const panelWidth = Number.isFinite(panelWidthRaw) && panelWidthRaw > 0 ? panelWidthRaw : 260;
3333
const panelHeight = Number.isFinite(panelHeightRaw) && panelHeightRaw > 0 ? panelHeightRaw : 96;
34+
const resolvePanelSize = typeof entry.resolvePanelSize === 'function' ? entry.resolvePanelSize : null;
3435

3536
return Object.freeze({
3637
overlayId,
3738
onStep,
3839
onRender,
40+
resolvePanelSize,
3941
compose,
4042
layerOrder,
4143
visualPriority,
@@ -255,7 +257,37 @@ function applyCompositionReadabilityLimits(frames, renderer) {
255257
return frames.filter((frame) => selectedSet.has(frame));
256258
}
257259

258-
function attachCompositionSlots(frames, renderer, safeZones = []) {
260+
function normalizePanelDimension(value, fallback, minimum) {
261+
const numeric = Number(value);
262+
if (!Number.isFinite(numeric) || numeric <= 0) {
263+
return Math.max(minimum, fallback);
264+
}
265+
return Math.max(minimum, numeric);
266+
}
267+
268+
function resolveDynamicPanelSize(extension, layoutContext, fallbackWidth, fallbackHeight) {
269+
if (!extension || typeof extension.resolvePanelSize !== 'function') {
270+
return {
271+
width: fallbackWidth,
272+
height: fallbackHeight,
273+
};
274+
}
275+
276+
try {
277+
const computed = extension.resolvePanelSize(layoutContext || {});
278+
return {
279+
width: normalizePanelDimension(computed?.width, fallbackWidth, 120),
280+
height: normalizePanelDimension(computed?.height, fallbackHeight, 32),
281+
};
282+
} catch {
283+
return {
284+
width: fallbackWidth,
285+
height: fallbackHeight,
286+
};
287+
}
288+
}
289+
290+
function attachCompositionSlots(frames, renderer, safeZones = [], layoutContext = {}) {
259291
if (!Array.isArray(frames) || frames.length === 0) {
260292
return frames || [];
261293
}
@@ -330,8 +362,20 @@ function attachCompositionSlots(frames, renderer, safeZones = []) {
330362

331363
for (let i = 0; i < frames.length; i += 1) {
332364
const frame = frames[i];
333-
const slotWidth = Math.max(120, Number(frame.extension.panelWidth) || 260);
334-
const slotHeight = Math.max(32, Number(frame.extension.panelHeight) || 96);
365+
const fallbackWidth = Math.max(120, Number(frame.extension.panelWidth) || 260);
366+
const fallbackHeight = Math.max(32, Number(frame.extension.panelHeight) || 96);
367+
const dynamicPanelSize = resolveDynamicPanelSize(frame.extension, {
368+
...layoutContext,
369+
renderer,
370+
canvasSize: { width, height },
371+
frameIndex: i,
372+
frameCount: frames.length,
373+
safeZones,
374+
}, fallbackWidth, fallbackHeight);
375+
const maxPanelWidth = Math.max(120, width - margin * 2);
376+
const maxPanelHeight = Math.max(32, height - margin * 2);
377+
const slotWidth = Math.min(maxPanelWidth, Math.max(120, Number(dynamicPanelSize.width) || fallbackWidth));
378+
const slotHeight = Math.min(maxPanelHeight, Math.max(32, Number(dynamicPanelSize.height) || fallbackHeight));
335379
const anchorOrder = ['bottom-right', 'top-right', 'bottom-left', 'top-left'];
336380

337381
let slot = null;
@@ -457,7 +501,8 @@ export function getOverlayGameplayRuntimeCompositionSnapshot(runtime, context =
457501
const frames = deriveRenderHierarchy(attachCompositionSlots(
458502
getComposedRuntimeFrames(runtime, activeOverlayId),
459503
context?.renderer,
460-
safeZones
504+
safeZones,
505+
context
461506
));
462507
const visibleFrames = applyCompositionReadabilityLimits(frames, context?.renderer);
463508
const visibleSet = new Set(visibleFrames);
@@ -618,7 +663,8 @@ export function renderOverlayGameplayRuntime(runtime, context = {}) {
618663
attachCompositionSlots(
619664
getComposedRuntimeFrames(runtime, activeOverlayId),
620665
context.renderer,
621-
safeZones
666+
safeZones,
667+
context
622668
)
623669
),
624670
context.renderer

tests/runtime/Phase19OverlayExpansionFramework.test.mjs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Phase19OverlayExpansionFramework.test.mjs
77
import assert from 'node:assert/strict';
88
import { LEVEL17_OVERLAY_CYCLE_KEY } from '../../samples/phase-17/shared/overlayCycleInput.js';
99
import {
10+
getOverlayGameplayRuntimeCompositionSnapshot,
1011
renderOverlayGameplayRuntime,
1112
stepOverlayGameplayRuntime,
1213
} from '../../samples/phase-17/shared/overlayGameplayRuntime.js';
@@ -111,7 +112,78 @@ function assertExtensionLifecycleMutations() {
111112
assert.equal(framework.createRuntimeForExtension('phase19-overlay-seed'), null, 'Removed extension should no longer create runtime.');
112113
}
113114

115+
function assertDynamicPanelSizingCapability() {
116+
const framework = createPhase19OverlayExpansionFramework();
117+
framework.registerExtension(definePhase19OverlayExtension({
118+
id: 'phase19-overlay-dynamic-size',
119+
overlays: [
120+
{ id: 'ui', label: 'UI' },
121+
{ id: 'runtime', label: 'Runtime' },
122+
],
123+
initialOverlayId: 'ui',
124+
runtimeExtensions: [
125+
{
126+
overlayId: 'runtime',
127+
panelWidth: 220,
128+
panelHeight: 90,
129+
onRender() {},
130+
resolvePanelSize(layoutContext) {
131+
const canvasWidth = Number(layoutContext?.canvasSize?.width) || 0;
132+
const canvasHeight = Number(layoutContext?.canvasSize?.height) || 0;
133+
return {
134+
width: Math.round(canvasWidth * 0.5),
135+
height: Math.round(canvasHeight * 0.2),
136+
};
137+
},
138+
},
139+
],
140+
}));
141+
142+
const runtime = framework.createRuntimeForExtension('phase19-overlay-dynamic-size');
143+
const wideSnapshot = getOverlayGameplayRuntimeCompositionSnapshot(runtime, {
144+
activeOverlayId: 'runtime',
145+
renderer: createRendererProbe(1000, 600),
146+
});
147+
assert.equal(wideSnapshot.length, 1, 'Dynamic panel sizing runtime should emit a composed frame snapshot.');
148+
assert.equal(wideSnapshot[0].slot.width, 500, 'Dynamic sizing should scale panel width from canvas size.');
149+
assert.equal(wideSnapshot[0].slot.height, 120, 'Dynamic sizing should scale panel height from canvas size.');
150+
151+
const compactSnapshot = getOverlayGameplayRuntimeCompositionSnapshot(runtime, {
152+
activeOverlayId: 'runtime',
153+
renderer: createRendererProbe(600, 400),
154+
});
155+
assert.equal(compactSnapshot[0].slot.width, 300, 'Dynamic sizing should recompute width for smaller canvases.');
156+
assert.equal(compactSnapshot[0].slot.height, 80, 'Dynamic sizing should recompute height for smaller canvases.');
157+
158+
const fallbackFramework = createPhase19OverlayExpansionFramework();
159+
fallbackFramework.registerExtension(definePhase19OverlayExtension({
160+
id: 'phase19-overlay-dynamic-size-fallback',
161+
overlays: [{ id: 'runtime', label: 'Runtime' }],
162+
initialOverlayId: 'runtime',
163+
runtimeExtensions: [
164+
{
165+
overlayId: 'runtime',
166+
panelWidth: 234,
167+
panelHeight: 98,
168+
onRender() {},
169+
resolvePanelSize() {
170+
throw new Error('resolver failed');
171+
},
172+
},
173+
],
174+
}));
175+
176+
const fallbackRuntime = fallbackFramework.createRuntimeForExtension('phase19-overlay-dynamic-size-fallback');
177+
const fallbackSnapshot = getOverlayGameplayRuntimeCompositionSnapshot(fallbackRuntime, {
178+
activeOverlayId: 'runtime',
179+
renderer: createRendererProbe(960, 540),
180+
});
181+
assert.equal(fallbackSnapshot[0].slot.width, 234, 'Resolver failures should preserve compatibility via configured width fallback.');
182+
assert.equal(fallbackSnapshot[0].slot.height, 98, 'Resolver failures should preserve compatibility via configured height fallback.');
183+
}
184+
114185
export function run() {
115186
assertExpansionRegistrationAndCompatibility();
116187
assertExtensionLifecycleMutations();
188+
assertDynamicPanelSizingCapability();
117189
}

0 commit comments

Comments
 (0)