Skip to content

Commit d8abada

Browse files
Add DOM polyfills (TextEncoder, PointerEvent, AbortController, document.createEvent)
Playground tests that use modern browser APIs hit ReferenceErrors on the Chakra-based BN runtime because TextEncoder and PointerEvent are not part of older Chakra's built-ins. Several serializer tests also exercise document.createEvent + element.dispatchEvent shapes that the existing minimal document polyfill in validation_native.js did not cover. - Apps/Playground/Scripts/dom_polyfill.js: new self-detecting JS polyfills for TextEncoder (UTF-8 encode + encodeInto) and PointerEvent (constructor with the MouseEvent + PointerEvent surface). - Apps/Playground/Shared/AppContext.cpp + CMakeLists.txt: wire in the native AbortController polyfill from JsRuntimeHost and load dom_polyfill.js before ammo.js. - Apps/Playground/Scripts/validation_native.js: extend the existing document shim with createEvent, dispatchEvent, addEventListener and a generic-element fallback for createElement so click-event-based serializer tests can run. - Apps/Playground/Scripts/config.json: re-enable "Serialize scene without materials" (idx 342), which now validates with 248 px diff. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ff9d4db commit d8abada

5 files changed

Lines changed: 165 additions & 4 deletions

File tree

Apps/Playground/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ set(DEPENDENCIES
1313
"../Dependencies/recast.js")
1414

1515
set(SCRIPTS
16+
"Scripts/dom_polyfill.js"
1617
"Scripts/experience.js"
1718
"Scripts/playground_runner.js"
1819
"Scripts/validation_native.js"
@@ -134,6 +135,7 @@ endif()
134135
target_include_directories(Playground PRIVATE ".")
135136

136137
target_link_libraries(Playground
138+
PRIVATE AbortController
137139
PRIVATE AppRuntime
138140
PRIVATE Blob
139141
PRIVATE bx

Apps/Playground/Scripts/config.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2258,8 +2258,6 @@
22582258
{
22592259
"title": "Serialize scene without materials",
22602260
"playgroundId": "#PH4DEZ#1",
2261-
"excludeFromAutomaticTesting": true,
2262-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
22632261
"referenceImage": "serializeWithoutMaterials.png"
22642262
},
22652263
{
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// dom_polyfill.js
2+
//
3+
// Minimal browser/DOM globals for the Babylon Native Playground JS host.
4+
// Provides TextEncoder and PointerEvent on runtimes (older Chakra) that do
5+
// not ship them natively. AbortController is provided by a native polyfill.
6+
// Each shim is self-detecting and no-ops if the symbol already exists.
7+
//
8+
// `document` is shimmed by validation_native.js once it has loaded the
9+
// config, since the runner needs to inject test-specific behaviour into
10+
// createElement.
11+
12+
(function () {
13+
'use strict';
14+
15+
// Chakra has no `globalThis`; use the Function-constructor trick.
16+
var g = (new Function('return this'))();
17+
18+
if (typeof g.TextEncoder === 'undefined') {
19+
function TextEncoder() {}
20+
Object.defineProperty(TextEncoder.prototype, 'encoding', {
21+
get: function () { return 'utf-8'; },
22+
configurable: true,
23+
});
24+
TextEncoder.prototype.encode = function (input) {
25+
var str = input === undefined ? '' : String(input);
26+
var bytes = [];
27+
for (var i = 0; i < str.length; i++) {
28+
var code = str.charCodeAt(i);
29+
if (code >= 0xD800 && code <= 0xDBFF && i + 1 < str.length) {
30+
var c2 = str.charCodeAt(i + 1);
31+
if (c2 >= 0xDC00 && c2 <= 0xDFFF) {
32+
code = 0x10000 + (((code & 0x3FF) << 10) | (c2 & 0x3FF));
33+
i++;
34+
}
35+
}
36+
if (code < 0x80) {
37+
bytes.push(code);
38+
} else if (code < 0x800) {
39+
bytes.push(0xC0 | (code >> 6));
40+
bytes.push(0x80 | (code & 0x3F));
41+
} else if (code < 0x10000) {
42+
bytes.push(0xE0 | (code >> 12));
43+
bytes.push(0x80 | ((code >> 6) & 0x3F));
44+
bytes.push(0x80 | (code & 0x3F));
45+
} else {
46+
bytes.push(0xF0 | (code >> 18));
47+
bytes.push(0x80 | ((code >> 12) & 0x3F));
48+
bytes.push(0x80 | ((code >> 6) & 0x3F));
49+
bytes.push(0x80 | (code & 0x3F));
50+
}
51+
}
52+
return new Uint8Array(bytes);
53+
};
54+
TextEncoder.prototype.encodeInto = function (input, dest) {
55+
var arr = this.encode(input);
56+
var n = Math.min(arr.length, dest.length);
57+
for (var i = 0; i < n; i++) dest[i] = arr[i];
58+
return { read: String(input).length, written: n };
59+
};
60+
g.TextEncoder = TextEncoder;
61+
}
62+
63+
if (typeof g.PointerEvent === 'undefined') {
64+
function PointerEvent(type, init) {
65+
init = init || {};
66+
this.type = String(type || '');
67+
this.bubbles = !!init.bubbles;
68+
this.cancelable = init.cancelable !== false;
69+
this.composed = !!init.composed;
70+
this.defaultPrevented = false;
71+
this.target = init.target || null;
72+
this.currentTarget = null;
73+
this.timeStamp = Date.now();
74+
this.pointerId = init.pointerId !== undefined ? init.pointerId : 0;
75+
this.width = init.width !== undefined ? init.width : 1;
76+
this.height = init.height !== undefined ? init.height : 1;
77+
this.pressure = init.pressure !== undefined ? init.pressure : 0.5;
78+
this.tangentialPressure = init.tangentialPressure || 0;
79+
this.tiltX = init.tiltX || 0;
80+
this.tiltY = init.tiltY || 0;
81+
this.twist = init.twist || 0;
82+
this.altitudeAngle = init.altitudeAngle || 0;
83+
this.azimuthAngle = init.azimuthAngle || 0;
84+
this.pointerType = init.pointerType || 'mouse';
85+
this.isPrimary = init.isPrimary !== false;
86+
this.clientX = init.clientX || 0;
87+
this.clientY = init.clientY || 0;
88+
this.offsetX = init.offsetX || 0;
89+
this.offsetY = init.offsetY || 0;
90+
this.pageX = init.pageX || 0;
91+
this.pageY = init.pageY || 0;
92+
this.screenX = init.screenX || 0;
93+
this.screenY = init.screenY || 0;
94+
this.movementX = init.movementX || 0;
95+
this.movementY = init.movementY || 0;
96+
this.button = init.button || 0;
97+
this.buttons = init.buttons || 0;
98+
this.relatedTarget = init.relatedTarget || null;
99+
this.ctrlKey = !!init.ctrlKey;
100+
this.shiftKey = !!init.shiftKey;
101+
this.altKey = !!init.altKey;
102+
this.metaKey = !!init.metaKey;
103+
this.detail = init.detail || 0;
104+
this.view = init.view || null;
105+
}
106+
PointerEvent.prototype.preventDefault = function () { this.defaultPrevented = true; };
107+
PointerEvent.prototype.stopPropagation = function () {};
108+
PointerEvent.prototype.stopImmediatePropagation = function () {};
109+
PointerEvent.prototype.getModifierState = function () { return false; };
110+
PointerEvent.prototype.getCoalescedEvents = function () { return []; };
111+
PointerEvent.prototype.getPredictedEvents = function () { return []; };
112+
g.PointerEvent = PointerEvent;
113+
}
114+
})();

Apps/Playground/Scripts/validation_native.js

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -525,9 +525,52 @@
525525
if (type === "canvas") {
526526
return new OffscreenCanvas(64, 64);
527527
}
528-
return {};
528+
// Generic element stub with a no-op dispatchEvent so serializer
529+
// tests that simulate a click via createEvent/dispatchEvent run
530+
// without throwing.
531+
return {
532+
style: {},
533+
addEventListener: function () { },
534+
removeEventListener: function () { },
535+
dispatchEvent: function () { return true; },
536+
appendChild: function (c) { return c; },
537+
removeChild: function (c) { return c; },
538+
setAttribute: function () { },
539+
getAttribute: function () { return null; },
540+
click: function () { },
541+
};
542+
},
543+
createEvent: function (type) {
544+
var ev = {
545+
type: '',
546+
bubbles: false,
547+
cancelable: false,
548+
defaultPrevented: false,
549+
target: null,
550+
currentTarget: null,
551+
timeStamp: Date.now(),
552+
detail: null,
553+
preventDefault: function () { ev.defaultPrevented = true; },
554+
stopPropagation: function () { },
555+
stopImmediatePropagation: function () { },
556+
};
557+
ev.initEvent = function (t, bubbles, cancelable) {
558+
ev.type = String(t || '');
559+
ev.bubbles = !!bubbles;
560+
ev.cancelable = !!cancelable;
561+
};
562+
ev.initCustomEvent = function (t, bubbles, cancelable, detail) {
563+
ev.initEvent(t, bubbles, cancelable);
564+
ev.detail = detail;
565+
};
566+
ev.initUIEvent = ev.initEvent;
567+
ev.initMouseEvent = ev.initEvent;
568+
return ev;
529569
},
530-
removeEventListener: function () { }
570+
addEventListener: function () { },
571+
removeEventListener: function () { },
572+
dispatchEvent: function () { return true; },
573+
body: { appendChild: function (c) { return c; }, removeChild: function (c) { return c; } },
531574
}
532575

533576
const xhr = new XMLHttpRequest();

Apps/Playground/Shared/AppContext.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <Babylon/Plugins/ShaderCache.h>
1818
#include <Babylon/Plugins/TestUtils.h>
1919

20+
#include <Babylon/Polyfills/AbortController.h>
2021
#include <Babylon/Polyfills/Blob.h>
2122
#include <Babylon/Polyfills/Canvas.h>
2223
#include <Babylon/Polyfills/Console.h>
@@ -185,6 +186,8 @@ AppContext::AppContext(
185186

186187
Babylon::Polyfills::Window::Initialize(env);
187188

189+
Babylon::Polyfills::AbortController::Initialize(env);
190+
188191
Babylon::Polyfills::TextDecoder::Initialize(env);
189192

190193
Babylon::Polyfills::XMLHttpRequest::Initialize(env);
@@ -213,6 +216,7 @@ AppContext::AppContext(
213216
});
214217

215218
m_scriptLoader.emplace(*m_runtime);
219+
m_scriptLoader->LoadScript("app:///Scripts/dom_polyfill.js");
216220
m_scriptLoader->LoadScript("app:///Scripts/ammo.js");
217221
// Commenting out recast.js for now because v8jsi is incompatible with asm.js.
218222
// m_scriptLoader->LoadScript("app:///Scripts/recast.js");

0 commit comments

Comments
 (0)