Skip to content

Commit 3af43ae

Browse files
author
DavidQ
committed
Fix bezel path and gameplay background draw order, add bezel extra-stretch override
BUILD_PR_LEVEL_10_20_FIX_BEZEL_PATH_AND_BACKGROUND_DRAW_ORDER
1 parent ecaba39 commit 3af43ae

8 files changed

Lines changed: 398 additions & 22 deletions

File tree

docs/dev/CODEX_COMMANDS.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,28 @@ Implement the confirmed Asteroids runtime fixes:
4444
- if left/right does not fill, use that result for resize
4545
- if no valid transparency window is found, fall back to existing centered-canvas behavior
4646

47-
6. Validate
47+
6. Add one shared extra-stretch setting
48+
- create one single developer-facing variable that applies equally to all four sides
49+
- use it to push the displayed canvas slightly farther toward the bezel opening edges when the default fit is just short
50+
- place this in a small easy-to-find config/override file, not hidden in code constants
51+
- when bezel is detected, check whether that file exists
52+
- if it does not exist, create it automatically for the developer with a safe default value
53+
- keep filename/location obvious near the relevant game asset/config area
54+
55+
7. Validate
4856
- bezel URL/path not duplicated
4957
- bezel visible in fullscreen
5058
- canvas internal size unchanged
5159
- canvas centered
5260
- exact four-direction transparency rule used
5361
- displayed canvas fills transparency window as fully as possible while preserving aspect ratio
62+
- shared extra-stretch variable affects all four sides equally
63+
- override/config file is auto-created when missing and bezel is detected
5464
- background visible during gameplay
5565
- background absent in non-gameplay states
5666
- starfield no longer hides background incorrectly
5767

58-
7. Final packaging step is REQUIRED
68+
8. Final packaging step is REQUIRED
5969
- package ALL changed files into this exact repo-structured ZIP:
6070
`<project folder>/tmp/BUILD_PR_LEVEL_10_20_FIX_BEZEL_PATH_AND_BACKGROUND_DRAW_ORDER.zip`
6171

docs/dev/COMMIT_COMMENT.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
Fix bezel path and gameplay background draw order, add exact transparency-window fitting rule
1+
Fix bezel path and gameplay background draw order, add bezel extra-stretch override
22
BUILD_PR_LEVEL_10_20_FIX_BEZEL_PATH_AND_BACKGROUND_DRAW_ORDER
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
- Reissued as strict docs-only BUILD_PR bundle
2-
- Codex instructed to fix bezel path duplication
3-
- Canvas internal size must remain unchanged and centered
4-
- backgroundImage must render after clear and before starfield/world
5-
- backgroundImage limited to gameplay states only
6-
- Updated bezel fitting requirement to use the exact four-direction first-transparent-pixel rule
2+
- Canvas internal size remains unchanged and centered
3+
- backgroundImage remains gameplay-only and draws before starfield/world
4+
- Bezel fit still uses the exact four-direction first-transparent-pixel rule
5+
- Added one shared extra-stretch developer setting for all four sides
6+
- Added auto-create behavior for the bezel-fit override/config file when missing

docs/dev/reports/validation_checklist.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
- Canvas must remain centered
66
- Transparency window must use the exact four-direction first-transparent-pixel rule
77
- Canvas display box must fill the transparency window as fully as possible while preserving aspect ratio
8+
- One shared extra-stretch variable must affect all four sides equally
9+
- Bezel-fit override/config file must be auto-created when missing and bezel is detected
810
- Background must draw only during gameplay
911
- Background must draw before starfield/world content
1012
- Codex output ZIP path must be:

docs/pr/BUILD_PR_LEVEL_10_20_FIX_BEZEL_PATH_AND_BACKGROUND_DRAW_ORDER.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Prepare a docs-only BUILD_PR bundle for Codex to implement the confirmed Asteroi
1414

1515
4. Background should render only during gameplay, not attract, title, select-player, menu, or other non-gameplay states.
1616

17-
5. When a bezel image is loaded, the displayed canvas should be fit to the bezel transparency using the exact edge-detection rule below.
17+
5. Current bezel fit is almost correct, but the displayed canvas is still slightly short of the bezel opening edges.
1818

1919
## Required implementation for Codex
2020

@@ -65,10 +65,25 @@ Selection rule:
6565
- If top/bottom does not fill, use that result for resize.
6666
- If left/right does not fill, use that result for resize.
6767

68-
Interpretation:
69-
- the display box should be driven by the transparency bounds
70-
- the goal is to fill the transparent gameplay area as fully as possible while keeping aspect ratio
71-
- fallback to existing centered-canvas behavior only if a valid transparency window cannot be determined
68+
### F. Add one shared extra-stretch developer setting
69+
Add one single variable that applies equally to all four sides of the detected bezel opening.
70+
71+
Intent:
72+
- this is a small outward adjustment for cases where the fitted canvas is just short of the transparent edges
73+
- one variable only
74+
- same adjustment behavior for top, bottom, left, and right
75+
- used after the transparency window is detected and before final display-box placement is applied
76+
77+
Decision:
78+
- this should live in a small developer-facing config/override file, not buried in rules/code constants
79+
- the location should be easy to find and edit
80+
81+
Required behavior:
82+
- when a bezel is detected, check whether the bezel-fit override file exists
83+
- if it does not exist, create it automatically for the developer
84+
- include the shared extra-stretch variable in that file with a safe default
85+
- keep the filename/location obvious and easy to discover near the game assets/config area
86+
- use that single value to allow the developer to push the fitted canvas slightly farther toward all four opening edges
7287

7388
## Validation targets
7489
Codex must validate:
@@ -78,6 +93,8 @@ Codex must validate:
7893
- canvas remains centered
7994
- transparency bounds are determined by the exact four-direction first-transparent-pixel rule
8095
- displayed canvas fills the transparency window as fully as possible while preserving aspect ratio
96+
- shared extra-stretch variable affects all four sides equally
97+
- override/config file is auto-created when bezel is detected and the file is missing
8198
- background is visible during gameplay
8299
- background does not render in non-gameplay states
83100
- starfield no longer hides background by draw order mistake
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"uniformEdgeStretchPx": 5
3+
}

src/engine/runtime/fullscreenBezel.js

Lines changed: 219 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { resolveGameImageConventionPaths, resolveRuntimeAssetUrl } from "./gameImageConvention.js";
22

33
const TRANSPARENT_ALPHA_THRESHOLD = 8;
4+
const DEFAULT_BEZEL_STRETCH_OVERRIDE_FILENAME = "bezel.stretch.override.json";
5+
const DEFAULT_BEZEL_STRETCH_CONFIG = Object.freeze({
6+
uniformEdgeStretchPx: 0
7+
});
48

59
function toDisplayValue(visible) {
610
return visible ? "block" : "none";
@@ -96,6 +100,103 @@ function toPixel(value) {
96100
return `${rounded}px`;
97101
}
98102

103+
function normalizePath(value) {
104+
return typeof value === "string" ? value.replace(/\\/g, "/") : "";
105+
}
106+
107+
function safeNumber(value, fallback = 0) {
108+
const parsed = Number(value);
109+
return Number.isFinite(parsed) ? parsed : fallback;
110+
}
111+
112+
export function sanitizeUniformEdgeStretchPx(value) {
113+
return Math.max(0, safeNumber(value, 0));
114+
}
115+
116+
export function resolveBezelStretchConfigPath(bezelPath, fileName = DEFAULT_BEZEL_STRETCH_OVERRIDE_FILENAME) {
117+
const normalized = normalizePath(bezelPath).trim();
118+
if (!normalized) {
119+
return "";
120+
}
121+
const slashIndex = normalized.lastIndexOf("/");
122+
if (slashIndex < 0) {
123+
return fileName;
124+
}
125+
return `${normalized.slice(0, slashIndex + 1)}${fileName}`;
126+
}
127+
128+
function parseStretchConfigObject(candidate) {
129+
const source = candidate && typeof candidate === "object" ? candidate : {};
130+
return {
131+
uniformEdgeStretchPx: sanitizeUniformEdgeStretchPx(source.uniformEdgeStretchPx)
132+
};
133+
}
134+
135+
function isNodeRuntime() {
136+
return typeof process !== "undefined"
137+
&& !!process?.versions?.node;
138+
}
139+
140+
export async function ensureBezelStretchConfigFile(configPath, options = {}) {
141+
const normalizedPath = normalizePath(configPath).trim();
142+
if (!normalizedPath || !isNodeRuntime()) {
143+
return { ...DEFAULT_BEZEL_STRETCH_CONFIG };
144+
}
145+
146+
const cwd = typeof options.cwd === "string" && options.cwd.trim()
147+
? options.cwd
148+
: process.cwd();
149+
const fsModule = options.fsModule || await import("node:fs/promises");
150+
const pathModule = options.pathModule || await import("node:path");
151+
const absolutePath = pathModule.resolve(cwd, normalizedPath);
152+
const directoryPath = pathModule.dirname(absolutePath);
153+
const defaultContent = `${JSON.stringify(DEFAULT_BEZEL_STRETCH_CONFIG, null, 2)}\n`;
154+
155+
try {
156+
await fsModule.access(absolutePath);
157+
} catch {
158+
await fsModule.mkdir(directoryPath, { recursive: true });
159+
await fsModule.writeFile(absolutePath, defaultContent, "utf8");
160+
return { ...DEFAULT_BEZEL_STRETCH_CONFIG };
161+
}
162+
163+
try {
164+
const content = await fsModule.readFile(absolutePath, "utf8");
165+
const parsed = JSON.parse(content);
166+
return parseStretchConfigObject(parsed);
167+
} catch {
168+
return { ...DEFAULT_BEZEL_STRETCH_CONFIG };
169+
}
170+
}
171+
172+
export async function loadBezelStretchConfig(configPath, options = {}) {
173+
const normalizedPath = normalizePath(configPath).trim();
174+
if (!normalizedPath) {
175+
return { ...DEFAULT_BEZEL_STRETCH_CONFIG };
176+
}
177+
178+
if (isNodeRuntime()) {
179+
return ensureBezelStretchConfigFile(normalizedPath, options);
180+
}
181+
182+
const fetchImpl = options.fetchImpl || globalThis.fetch;
183+
if (typeof fetchImpl !== "function") {
184+
return { ...DEFAULT_BEZEL_STRETCH_CONFIG };
185+
}
186+
187+
try {
188+
const requestPath = normalizedPath.startsWith("/") ? normalizedPath : `/${normalizedPath}`;
189+
const response = await fetchImpl(requestPath, { cache: "no-store" });
190+
if (!response || response.ok !== true) {
191+
return { ...DEFAULT_BEZEL_STRETCH_CONFIG };
192+
}
193+
const parsed = await response.json();
194+
return parseStretchConfigObject(parsed);
195+
} catch {
196+
return { ...DEFAULT_BEZEL_STRETCH_CONFIG };
197+
}
198+
}
199+
99200
function sanitizeRect(candidate) {
100201
if (!candidate || typeof candidate !== "object") {
101202
return null;
@@ -112,6 +213,45 @@ function sanitizeRect(candidate) {
112213
return { x, y, width, height };
113214
}
114215

216+
function expandRectWithinBounds(rect, expandPx, bounds) {
217+
const source = sanitizeRect(rect);
218+
if (!source || !bounds || typeof bounds !== "object") {
219+
return source;
220+
}
221+
222+
const stretch = sanitizeUniformEdgeStretchPx(expandPx);
223+
if (stretch <= 0) {
224+
return source;
225+
}
226+
227+
const boundX = safeNumber(bounds.x, 0);
228+
const boundY = safeNumber(bounds.y, 0);
229+
const boundWidth = toPositiveNumber(bounds.width);
230+
const boundHeight = toPositiveNumber(bounds.height);
231+
if (boundWidth <= 0 || boundHeight <= 0) {
232+
return source;
233+
}
234+
235+
const boundRight = boundX + boundWidth;
236+
const boundBottom = boundY + boundHeight;
237+
const expandedLeft = Math.max(boundX, source.x - stretch);
238+
const expandedTop = Math.max(boundY, source.y - stretch);
239+
const expandedRight = Math.min(boundRight, source.x + source.width + stretch);
240+
const expandedBottom = Math.min(boundBottom, source.y + source.height + stretch);
241+
const expandedWidth = Math.max(0, expandedRight - expandedLeft);
242+
const expandedHeight = Math.max(0, expandedBottom - expandedTop);
243+
if (expandedWidth <= 0 || expandedHeight <= 0) {
244+
return source;
245+
}
246+
247+
return {
248+
x: expandedLeft,
249+
y: expandedTop,
250+
width: expandedWidth,
251+
height: expandedHeight
252+
};
253+
}
254+
115255
function getHostBounds(host) {
116256
if (!host) {
117257
return null;
@@ -399,11 +539,17 @@ export default class fullscreenBezel {
399539
this.defaultHost = options.host || null;
400540
this.gameId = resolved.gameId;
401541
this.path = resolved.bezelPath;
542+
this.stretchConfigPath = resolveBezelStretchConfigPath(this.path);
402543
this.host = null;
403544
this.element = null;
404545
this.ready = false;
405546
this.missing = !this.path;
406547
this.alphaInspector = typeof options.alphaInspector === "function" ? options.alphaInspector : null;
548+
this.stretchConfigProvider = typeof options.stretchConfigProvider === "function"
549+
? options.stretchConfigProvider
550+
: loadBezelStretchConfig;
551+
this.stretchConfigPromise = null;
552+
this.uniformEdgeStretchPx = 0;
407553
this.transparentWindowRect = null;
408554
this.imageSize = null;
409555
this.canvasLayoutMode = "fallback";
@@ -417,7 +563,9 @@ export default class fullscreenBezel {
417563
visible: this.element?.style?.display === "block",
418564
hostTagName: this.host?.tagName || "",
419565
canvasLayoutMode: this.canvasLayoutMode,
420-
transparentWindowRect: this.transparentWindowRect
566+
transparentWindowRect: this.transparentWindowRect,
567+
stretchConfigPath: this.stretchConfigPath,
568+
uniformEdgeStretchPx: this.uniformEdgeStretchPx
421569
};
422570
}
423571

@@ -454,6 +602,51 @@ export default class fullscreenBezel {
454602
return true;
455603
}
456604

605+
applyStretchConfig(config) {
606+
const parsed = parseStretchConfigObject(config);
607+
this.uniformEdgeStretchPx = parsed.uniformEdgeStretchPx;
608+
return parsed;
609+
}
610+
611+
refreshStretchConfig() {
612+
if (!this.stretchConfigPath || typeof this.stretchConfigProvider !== "function") {
613+
this.applyStretchConfig(DEFAULT_BEZEL_STRETCH_CONFIG);
614+
return null;
615+
}
616+
617+
try {
618+
const result = this.stretchConfigProvider(this.stretchConfigPath, {
619+
bezelPath: this.path,
620+
gameId: this.gameId,
621+
defaultConfig: { ...DEFAULT_BEZEL_STRETCH_CONFIG }
622+
});
623+
if (result && typeof result.then === "function") {
624+
const pending = Promise.resolve(result)
625+
.then((config) => {
626+
const parsed = this.applyStretchConfig(config);
627+
if (this.element?.style?.display === "block") {
628+
const fitted = this.applyCanvasWindowFitLayout();
629+
if (!fitted) {
630+
this.applyCanvasFallbackLayout();
631+
}
632+
}
633+
return parsed;
634+
})
635+
.catch(() => this.applyStretchConfig(DEFAULT_BEZEL_STRETCH_CONFIG));
636+
this.stretchConfigPromise = pending;
637+
return pending;
638+
}
639+
640+
this.applyStretchConfig(result);
641+
this.stretchConfigPromise = Promise.resolve({ uniformEdgeStretchPx: this.uniformEdgeStretchPx });
642+
return this.stretchConfigPromise;
643+
} catch {
644+
this.applyStretchConfig(DEFAULT_BEZEL_STRETCH_CONFIG);
645+
this.stretchConfigPromise = Promise.resolve({ uniformEdgeStretchPx: this.uniformEdgeStretchPx });
646+
return this.stretchConfigPromise;
647+
}
648+
}
649+
457650
attach() {
458651
if (!this.documentRef || !this.path || this.element) {
459652
return;
@@ -475,6 +668,7 @@ export default class fullscreenBezel {
475668
this.transparentWindowRect = this.imageSize
476669
? inspectTransparentWindowRect(element, this.documentRef, this.alphaInspector, this.imageSize)
477670
: null;
671+
this.refreshStretchConfig();
478672
this.ready = true;
479673
this.missing = false;
480674
};
@@ -553,17 +747,34 @@ export default class fullscreenBezel {
553747
return false;
554748
}
555749

556-
const mappedWindowWidth = this.transparentWindowRect.width * containPlacement.scale;
557-
const mappedWindowHeight = this.transparentWindowRect.height * containPlacement.scale;
558-
const fittedCanvas = chooseBestOpeningFit(sourceWidth, sourceHeight, mappedWindowWidth, mappedWindowHeight);
750+
const mappedWindow = {
751+
x: containPlacement.x + (this.transparentWindowRect.x * containPlacement.scale),
752+
y: containPlacement.y + (this.transparentWindowRect.y * containPlacement.scale),
753+
width: this.transparentWindowRect.width * containPlacement.scale,
754+
height: this.transparentWindowRect.height * containPlacement.scale
755+
};
756+
const stretchedWindow = expandRectWithinBounds(mappedWindow, this.uniformEdgeStretchPx, {
757+
x: containPlacement.x,
758+
y: containPlacement.y,
759+
width: containPlacement.width,
760+
height: containPlacement.height
761+
});
762+
const fittedCanvas = chooseBestOpeningFit(
763+
sourceWidth,
764+
sourceHeight,
765+
stretchedWindow?.width || 0,
766+
stretchedWindow?.height || 0
767+
);
559768
if (!fittedCanvas) {
560769
return false;
561770
}
562771

563-
const mappedWindowX = containPlacement.x + (this.transparentWindowRect.x * containPlacement.scale);
564-
const mappedWindowY = containPlacement.y + (this.transparentWindowRect.y * containPlacement.scale);
565-
const left = mappedWindowX + ((mappedWindowWidth - fittedCanvas.width) * 0.5);
566-
const top = mappedWindowY + ((mappedWindowHeight - fittedCanvas.height) * 0.5);
772+
const windowX = stretchedWindow?.x || 0;
773+
const windowY = stretchedWindow?.y || 0;
774+
const windowWidth = stretchedWindow?.width || 0;
775+
const windowHeight = stretchedWindow?.height || 0;
776+
const left = windowX + ((windowWidth - fittedCanvas.width) * 0.5);
777+
const top = windowY + ((windowHeight - fittedCanvas.height) * 0.5);
567778

568779
this.canvas.style.position = "absolute";
569780
this.canvas.style.left = toPixel(left);

0 commit comments

Comments
 (0)