@geastack/native-webgl-angle 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,9 +50,36 @@ native/angle_webgl_host.mm
50
50
 
51
51
  ## Native sources in apps
52
52
 
53
- Gea app manifests currently accept only app-relative `nativeSources`. Until
54
- that resolver can consume package native sources directly, app consumers keep a
55
- small app-local `.mm` wrapper that includes this package's native implementation.
53
+ Apple apps name the host in `gea.nativeSources`, either app-relative or by
54
+ package path (`@geastack/native-webgl-angle/native/angle_webgl_host.mm`).
55
+
56
+ The Windows desktop target needs nothing in the app: `gea-native.json`
57
+ declares the host (`native/angle_webgl_host.mm` and `native/audio_host.mm`,
58
+ compiled as C++) and `native/win32_webgl_surface.cpp`, a host extension that
59
+ gives the program a child window filling the main window's content area,
60
+ attaches the WebGL context to it before the program's top level runs, makes
61
+ the context current around initialization and each frame, hands frame pacing
62
+ to the presentation, and reports keys (as macOS virtual key codes, with a
63
+ release held until a frame has seen the key down) and the pointer (in CSS
64
+ pixels).
65
+
66
+ The package requires a `gles` provider: an EGL and OpenGL ES 3
67
+ implementation. The app configures it, for example
68
+
69
+ ```json
70
+ "gea": { "targets": { "windows": { "providers": {
71
+ "gles": { "directory": "build/renderer", "files": { "egl": "GL.dll", "gles": "GL.dll" }, "runtimeFiles": ["d3d12/D3D12Core.dll"] }
72
+ } } } }
73
+ ```
74
+
75
+ or `"gles": "path/to/angle"` for a directory holding ANGLE's `libEGL.dll` and
76
+ `libGLESv2.dll`. The build stages those files beside the executable and
77
+ compiles their names in (`GEA_WEBGL_EGL_LIBRARY`, `GEA_WEBGL_GLES_LIBRARY`);
78
+ the host loads them from the executable's directory. `GEA_ANGLE_EGL_DYLIB` and
79
+ `GEA_ANGLE_GLES_DYLIB` still override the paths at run time. Without a
80
+ provider the build stops and says so. Audio files resolve from
81
+ `GEA_THREE_AUDIO_DIR`, else `Resources\Sounds` beside the executable, unless
82
+ the app names a directory with `gea_three_audio_set_directory_uwp`.
56
83
 
57
84
  The bridge implementation itself lives here.
58
85
 
@@ -0,0 +1,16 @@
1
+ {
2
+ "$comment": "What a GeaStack native target needs in order to build this package into an app. Paths are relative to the package root.",
3
+ "windows": {
4
+ "sources": ["native/angle_webgl_host.mm", "native/audio_host.mm", "native/win32_webgl_surface.cpp"],
5
+ "includeDirs": ["native"],
6
+ "requires": {
7
+ "gles": {
8
+ "description": "an EGL and OpenGL ES 3 implementation (ANGLE's libEGL.dll and libGLESv2.dll, or one DLL exporting both)",
9
+ "files": {
10
+ "egl": { "define": "GEA_WEBGL_EGL_LIBRARY", "default": "libEGL.dll" },
11
+ "gles": { "define": "GEA_WEBGL_GLES_LIBRARY", "default": "libGLESv2.dll" }
12
+ }
13
+ }
14
+ }
15
+ }
16
+ }
@@ -1,4 +1,13 @@
1
1
  #include "webgl_readback.h"
2
+ #if defined(_WIN32)
3
+ // The EGL and OpenGL ES libraries the Windows host loads beside the executable.
4
+ #ifndef GEA_WEBGL_EGL_LIBRARY
5
+ #define GEA_WEBGL_EGL_LIBRARY "libEGL.dll"
6
+ #endif
7
+ #ifndef GEA_WEBGL_GLES_LIBRARY
8
+ #define GEA_WEBGL_GLES_LIBRARY "libGLESv2.dll"
9
+ #endif
10
+ #endif
2
11
  // GEA_WEBGL_STATIC_GLES: EGL/GLES are statically linked into the program, so
3
12
  // nothing is loaded and entry points resolve through eglGetProcAddress.
4
13
  #if defined(_WIN32)
@@ -1151,22 +1160,42 @@ extern "C" bool gea_three_webgl_attach(
1151
1160
  gWebGL.egl = &staticGles;
1152
1161
  gWebGL.gles = &staticGles;
1153
1162
  #elif defined(_WIN32)
1154
- // An override (e.g. threejs-rendozer's GeaRendozerGLES.dll) first, then the
1155
- // packaged ANGLE of a UWP app, then a plain desktop load next to the exe.
1156
- auto openWin32 = [](const wchar_t *overrideEnv, const wchar_t *name) -> HMODULE {
1163
+ // An override path from the environment first, then the packaged library of
1164
+ // a UWP app, then the file beside the executable. The file names default to
1165
+ // ANGLE's; a build that stages another EGL/GLES implementation names its
1166
+ // files through GEA_WEBGL_EGL_LIBRARY / GEA_WEBGL_GLES_LIBRARY (paths
1167
+ // relative to the executable's directory; one DLL may implement both).
1168
+ auto openWin32 = [](const wchar_t *overrideEnv, const char *name) -> HMODULE {
1157
1169
  wchar_t overridePath[MAX_PATH];
1158
1170
  const DWORD n = GetEnvironmentVariableW(overrideEnv, overridePath, MAX_PATH);
1159
1171
  if (n > 0 && n < MAX_PATH) {
1160
1172
  if (HMODULE m = LoadLibraryW(overridePath)) return m;
1173
+ smokeLog("[native-webgl-angle] %ls names a library that does not load (error %lu)", overrideEnv, GetLastError());
1161
1174
  }
1175
+ std::wstring file;
1176
+ for (const char *c = name; *c; ++c) file.push_back(*c == '/' ? L'\\' : static_cast<wchar_t>(static_cast<unsigned char>(*c)));
1162
1177
  #if defined(WINAPI_FAMILY) && WINAPI_FAMILY == WINAPI_FAMILY_APP
1163
- return LoadPackagedLibrary(name, 0);
1178
+ return LoadPackagedLibrary(file.c_str(), 0);
1164
1179
  #else
1165
- return LoadLibraryW(name);
1180
+ wchar_t module[MAX_PATH] = {};
1181
+ const DWORD length = GetModuleFileNameW(nullptr, module, MAX_PATH);
1182
+ std::wstring beside(module, length < MAX_PATH ? length : 0);
1183
+ const size_t slash = beside.find_last_of(L"\\/");
1184
+ if (slash != std::wstring::npos) {
1185
+ beside.resize(slash + 1);
1186
+ beside += file;
1187
+ // Altered search path: the library's own dependencies resolve from its directory.
1188
+ if (HMODULE m = LoadLibraryExW(beside.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH)) return m;
1189
+ }
1190
+ return LoadLibraryW(file.c_str());
1166
1191
  #endif
1167
1192
  };
1168
- gWebGL.egl = openWin32(L"GEA_ANGLE_EGL_DYLIB", L"libEGL.dll");
1169
- gWebGL.gles = openWin32(L"GEA_ANGLE_GLES_DYLIB", L"libGLESv2.dll");
1193
+ gWebGL.egl = openWin32(L"GEA_ANGLE_EGL_DYLIB", GEA_WEBGL_EGL_LIBRARY);
1194
+ gWebGL.gles = openWin32(L"GEA_ANGLE_GLES_DYLIB", GEA_WEBGL_GLES_LIBRARY);
1195
+ if (!gWebGL.egl || !gWebGL.gles) {
1196
+ smokeLog("[native-webgl-angle] EGL/GLES implementation not found: %s and %s beside the executable "
1197
+ "(GEA_ANGLE_EGL_DYLIB / GEA_ANGLE_GLES_DYLIB override them)", GEA_WEBGL_EGL_LIBRARY, GEA_WEBGL_GLES_LIBRARY);
1198
+ }
1170
1199
  #elif defined(__APPLE__)
1171
1200
  static const char *const eglPaths[] = {
1172
1201
  "/Applications/Visual Studio Code.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Libraries/libEGL.dylib",
@@ -21,6 +21,9 @@
21
21
  #if defined(_WIN32)
22
22
  #define _USE_MATH_DEFINES
23
23
  #include "audio_uwp.h"
24
+ // Legacy Win16 macros are not C++ identifiers and must not leak into the runtime.
25
+ #undef near
26
+ #undef far
24
27
  #else
25
28
  #import <AVFoundation/AVFoundation.h>
26
29
  #import <Foundation/Foundation.h>
@@ -44,8 +47,30 @@
44
47
 
45
48
  #if defined(_WIN32)
46
49
  static std::wstring geaAudioAssetDirectory;
50
+ static bool geaAudioAssetDirectorySet = false;
47
51
  extern "C" void gea_three_audio_set_directory_uwp(const wchar_t *path) {
48
52
  geaAudioAssetDirectory = path ? path : L"";
53
+ geaAudioAssetDirectorySet = true;
54
+ }
55
+ // Without a driver naming the directory: GEA_THREE_AUDIO_DIR, else the
56
+ // Resources\Sounds folder beside the executable (where the desktop target
57
+ // stages an app's sounds), matching the macOS host's bundle lookup.
58
+ static const std::wstring &geaAudioDirectory() {
59
+ if (!geaAudioAssetDirectorySet) {
60
+ geaAudioAssetDirectorySet = true;
61
+ wchar_t value[MAX_PATH] = {};
62
+ const DWORD n = GetEnvironmentVariableW(L"GEA_THREE_AUDIO_DIR", value, MAX_PATH);
63
+ if (n > 0 && n < MAX_PATH) {
64
+ geaAudioAssetDirectory = value;
65
+ } else {
66
+ wchar_t module[MAX_PATH] = {};
67
+ const DWORD length = GetModuleFileNameW(nullptr, module, MAX_PATH);
68
+ std::wstring directory(module, length < MAX_PATH ? length : 0);
69
+ const size_t slash = directory.find_last_of(L"\\/");
70
+ if (slash != std::wstring::npos) geaAudioAssetDirectory = directory.substr(0, slash) + L"\\Resources\\Sounds";
71
+ }
72
+ }
73
+ return geaAudioAssetDirectory;
49
74
  }
50
75
  #endif
51
76
 
@@ -427,7 +452,7 @@ struct GeaAudioEngine {
427
452
  long loadBufferFile(const std::string &name) {
428
453
  if (name.empty() || name.find('/') != std::string::npos || name.find("..") != std::string::npos) return 0;
429
454
  #if defined(_WIN32)
430
- if (name.find('\\') != std::string::npos || geaAudioAssetDirectory.empty()) return 0;
455
+ if (name.find('\\') != std::string::npos || geaAudioDirectory().empty()) return 0;
431
456
  const int length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, name.data(), static_cast<int>(name.size()), nullptr, 0);
432
457
  if (length <= 0) return 0;
433
458
  std::wstring base(static_cast<size_t>(length), L'\0');
@@ -0,0 +1,230 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The WebGL surface of a Windows desktop build: a child window filling the
3
+ // main window's content area, which the native WebGL host renders into, and
4
+ // the input the host reports to the program.
5
+ //
6
+ // It plugs into the Windows target as a host extension (gea_win32_host.h):
7
+ // the target attaches it before the program's top level runs -- the program
8
+ // creates its WebGL context while it initializes -- and brackets the
9
+ // initialization and every frame with begin/end, which is when the host's
10
+ // EGL context is current. Keys reach the host as macOS virtual key codes, the
11
+ // code space the program's key polling uses on every platform; pointer
12
+ // positions are in CSS pixels of the surface.
13
+ #include "gea_win32_host.h"
14
+
15
+ #include <windowsx.h>
16
+
17
+ #include <algorithm>
18
+ #include <cstdio>
19
+ #include <cstdlib>
20
+
21
+ extern "C" bool gea_three_webgl_attach_uwp(void *nativeWindow, double width, double height, double devicePixelRatio);
22
+ extern "C" void gea_three_webgl_update_size_uwp(double width, double height, double devicePixelRatio);
23
+ extern "C" bool gea_three_webgl_begin_frame_uwp();
24
+ extern "C" void gea_three_webgl_end_frame_uwp();
25
+ extern "C" void gea_three_set_key_state(int code, bool down);
26
+ extern "C" void gea_three_set_pointer_state(double x, double y, double width, double height, bool down);
27
+
28
+ namespace {
29
+
30
+ HWND gSurface = nullptr;
31
+ HWND gMainWindow = nullptr;
32
+ double gScale = 1.0;
33
+ bool gPointerDown = false;
34
+ // The program polls key state once per frame, so a tap whose key-down and
35
+ // key-up both arrive between two frames would never be seen. A release waits
36
+ // here until a frame has run with the key down.
37
+ bool gReleasePending[256] = {};
38
+
39
+ // macOS virtual key codes (Carbon kVK_*), for the keys a game typically polls.
40
+ int macKeyCode(WPARAM vk)
41
+ {
42
+ switch (vk) {
43
+ case VK_LEFT: return 123;
44
+ case VK_RIGHT: return 124;
45
+ case VK_DOWN: return 125;
46
+ case VK_UP: return 126;
47
+ case VK_SPACE: return 49;
48
+ case VK_RETURN: return 36;
49
+ case VK_ESCAPE: return 53;
50
+ case VK_TAB: return 48;
51
+ case VK_BACK: return 51;
52
+ case VK_DELETE: return 117;
53
+ case VK_HOME: return 115;
54
+ case VK_END: return 119;
55
+ case VK_PRIOR: return 116;
56
+ case VK_NEXT: return 121;
57
+ case VK_SHIFT: case VK_LSHIFT: return 56;
58
+ case VK_RSHIFT: return 60;
59
+ case VK_CONTROL: case VK_LCONTROL: return 59;
60
+ case VK_RCONTROL: return 62;
61
+ case VK_MENU: case VK_LMENU: return 58;
62
+ case VK_RMENU: return 61;
63
+ case VK_OEM_MINUS: return 27;
64
+ case VK_OEM_PLUS: return 24;
65
+ case VK_OEM_COMMA: return 43;
66
+ case VK_OEM_PERIOD: return 47;
67
+ case VK_OEM_1: return 41; // ;
68
+ case VK_OEM_2: return 44; // /
69
+ case VK_OEM_3: return 50; // `
70
+ case VK_OEM_4: return 33; // [
71
+ case VK_OEM_5: return 42; // backslash
72
+ case VK_OEM_6: return 30; // ]
73
+ case VK_OEM_7: return 39; // '
74
+ default: break;
75
+ }
76
+ static const int letters[26] = {0, 11, 8, 2, 14, 3, 5, 4, 34, 38, 40, 37, 46, 45, 31, 35, 12, 15, 1, 17, 32, 9, 13, 7, 16, 6};
77
+ if (vk >= 'A' && vk <= 'Z') return letters[vk - 'A'];
78
+ static const int digits[10] = {29, 18, 19, 20, 21, 23, 22, 26, 28, 25};
79
+ if (vk >= '0' && vk <= '9') return digits[vk - '0'];
80
+ static const int functions[12] = {122, 120, 99, 118, 96, 97, 98, 100, 101, 109, 103, 111};
81
+ if (vk >= VK_F1 && vk <= VK_F12) return functions[vk - VK_F1];
82
+ return -1;
83
+ }
84
+
85
+ void releaseAllKeys()
86
+ {
87
+ for (int code = 0; code < 256; ++code) {
88
+ gReleasePending[code] = false;
89
+ gea_three_set_key_state(code, false);
90
+ }
91
+ }
92
+
93
+ void reportPointer(LPARAM lParam)
94
+ {
95
+ RECT client{};
96
+ GetClientRect(gSurface, &client);
97
+ gea_three_set_pointer_state(GET_X_LPARAM(lParam) / gScale, GET_Y_LPARAM(lParam) / gScale, std::max<LONG>(1, client.right) / gScale,
98
+ std::max<LONG>(1, client.bottom) / gScale, gPointerDown);
99
+ }
100
+
101
+ LRESULT CALLBACK surfaceProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
102
+ {
103
+ switch (message) {
104
+ case WM_KEYDOWN:
105
+ case WM_SYSKEYDOWN:
106
+ if (message == WM_SYSKEYDOWN && wParam == VK_F4) break; // Alt+F4 closes the window
107
+ if (const int code = macKeyCode(wParam); code >= 0) {
108
+ gReleasePending[code] = false;
109
+ gea_three_set_key_state(code, true);
110
+ }
111
+ return 0; // no menu activation on Alt or F10
112
+ case WM_KEYUP:
113
+ case WM_SYSKEYUP:
114
+ if (const int code = macKeyCode(wParam); code >= 0) gReleasePending[code] = true;
115
+ return 0;
116
+ case WM_SYSCHAR:
117
+ return 0; // no beep for Alt+key
118
+ case WM_KILLFOCUS:
119
+ gPointerDown = false;
120
+ ReleaseCapture();
121
+ releaseAllKeys();
122
+ return 0;
123
+ case WM_LBUTTONDOWN:
124
+ SetFocus(hwnd);
125
+ gPointerDown = true;
126
+ SetCapture(hwnd);
127
+ reportPointer(lParam);
128
+ return 0;
129
+ case WM_LBUTTONUP:
130
+ gPointerDown = false;
131
+ ReleaseCapture();
132
+ reportPointer(lParam);
133
+ return 0;
134
+ case WM_MOUSEMOVE:
135
+ reportPointer(lParam);
136
+ return 0;
137
+ case WM_ERASEBKGND:
138
+ return 1; // the swap chain owns every pixel
139
+ case WM_PAINT:
140
+ ValidateRect(hwnd, nullptr);
141
+ return 0;
142
+ default:
143
+ break;
144
+ }
145
+ return DefWindowProcW(hwnd, message, wParam, lParam);
146
+ }
147
+
148
+ int attach(HWND window, RECT area, double scale)
149
+ {
150
+ gMainWindow = window;
151
+ gScale = scale > 0 ? scale : 1.0;
152
+ HINSTANCE instance = GetModuleHandleW(nullptr);
153
+ WNDCLASSEXW cls{};
154
+ cls.cbSize = sizeof(cls);
155
+ cls.style = CS_OWNDC | CS_DBLCLKS;
156
+ cls.lpfnWndProc = surfaceProc;
157
+ cls.hInstance = instance;
158
+ cls.hCursor = LoadCursorW(nullptr, IDC_ARROW);
159
+ cls.lpszClassName = L"GeaWebGLSurface";
160
+ RegisterClassExW(&cls);
161
+ // The main window must not paint over its swap-chain child.
162
+ SetWindowLongPtrW(window, GWL_STYLE, GetWindowLongPtrW(window, GWL_STYLE) | WS_CLIPCHILDREN);
163
+ const int width = std::max<int>(1, area.right - area.left);
164
+ const int height = std::max<int>(1, area.bottom - area.top);
165
+ gSurface = CreateWindowExW(0, cls.lpszClassName, L"", WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS, area.left, area.top, width, height, window, nullptr,
166
+ instance, nullptr);
167
+ if (!gSurface) {
168
+ std::fprintf(stderr, "[native-webgl-angle] could not create the surface window (error %lu)\n", GetLastError());
169
+ return 0;
170
+ }
171
+ SetWindowPos(gSurface, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
172
+ // Present with vsync unless the environment says otherwise.
173
+ if (!std::getenv("GEA_ANGLE_SWAP_INTERVAL")) _putenv_s("GEA_ANGLE_SWAP_INTERVAL", "1");
174
+ if (!gea_three_webgl_attach_uwp(gSurface, width, height, gScale)) {
175
+ std::fprintf(stderr, "[native-webgl-angle] the WebGL surface could not start: no usable EGL/OpenGL ES implementation\n");
176
+ return 0;
177
+ }
178
+ // eglSwapBuffers paces the loop: it waits for the refresh with vsync on and
179
+ // renders unthrottled with it off (GEA_ANGLE_SWAP_INTERVAL=0).
180
+ gea_win32_set_frame_pacing(GEA_WIN32_FRAME_PACING_PRESENT);
181
+ SetFocus(gSurface);
182
+ return 1;
183
+ }
184
+
185
+ void layout(RECT area, double scale)
186
+ {
187
+ if (!gSurface) return;
188
+ gScale = scale > 0 ? scale : 1.0;
189
+ const int width = std::max<int>(1, area.right - area.left);
190
+ const int height = std::max<int>(1, area.bottom - area.top);
191
+ SetWindowPos(gSurface, nullptr, area.left, area.top, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
192
+ gea_three_webgl_update_size_uwp(width, height, gScale);
193
+ }
194
+
195
+ int beginFrame() { return gea_three_webgl_begin_frame_uwp() ? 1 : 0; }
196
+
197
+ void endFrame()
198
+ {
199
+ gea_three_webgl_end_frame_uwp();
200
+ for (int code = 0; code < 256; ++code) {
201
+ if (!gReleasePending[code]) continue;
202
+ gReleasePending[code] = false;
203
+ gea_three_set_key_state(code, false);
204
+ }
205
+ }
206
+
207
+ int windowMessage(HWND, UINT message, WPARAM, LPARAM, LRESULT *result)
208
+ {
209
+ // Activating the main window gives it the keyboard focus; the surface is
210
+ // where keys are read.
211
+ if (message == WM_SETFOCUS && gSurface) {
212
+ SetFocus(gSurface);
213
+ *result = 0;
214
+ return 1;
215
+ }
216
+ return 0;
217
+ }
218
+
219
+ void detach()
220
+ {
221
+ if (gSurface) DestroyWindow(gSurface);
222
+ gSurface = nullptr;
223
+ }
224
+
225
+ const gea_win32_host_extension kSurfaceExtension = {
226
+ GEA_WIN32_HOST_EXTENSION_VERSION, "The WebGL surface", attach, layout, beginFrame, endFrame, windowMessage, detach,
227
+ };
228
+ const bool kRegistered = (gea_win32_register_host_extension(&kSurfaceExtension), true);
229
+
230
+ } // namespace
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geastack/native-webgl-angle",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Typed native WebGL compatibility layer backed by ANGLE/Metal",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -28,7 +28,8 @@
28
28
  "./text/native-text-types": "./src/text/native-text-types.ts",
29
29
  "./geatsc-plugin-batched-probe": "./geatsc-plugin-batched-probe.mjs",
30
30
  "./test/batched-render-scene": "./test/batched-render-scene.ts",
31
- "./test/auto-instanced-render-scene": "./test/auto-instanced-render-scene.ts"
31
+ "./test/auto-instanced-render-scene": "./test/auto-instanced-render-scene.ts",
32
+ "./gea-native.json": "./gea-native.json"
32
33
  },
33
34
  "files": [
34
35
  "src",
@@ -40,7 +41,8 @@
40
41
  "geatsc-plugin-batched-probe.mjs",
41
42
  "test/batched-render-scene.ts",
42
43
  "test/auto-instanced-render-scene.ts",
43
- "geatsc-plugin-readback.mjs"
44
+ "geatsc-plugin-readback.mjs",
45
+ "gea-native.json"
44
46
  ],
45
47
  "publishConfig": {
46
48
  "access": "public",
@@ -51,7 +53,7 @@
51
53
  "test:host-effects": "node test/native-host-effects-contract.test.mjs",
52
54
  "test:allocation-contracts": "node test/render-sort-contract.test.mjs && node test/upload-array-contract.test.mjs && node test/draw-dispatch-contract.test.mjs && node test/native-upload-transport.test.mjs && node test/fixed-uniform-contract.test.mjs && node test/uniform-binding-contract.test.mjs && node test/buffer-streaming-contract.test.mjs",
53
55
  "check": "tsc -p tsconfig.json --noEmit",
54
- "test": "node test/native-blending-contract.test.mjs && node test/native-transform-contract.test.mjs && node test/troika-three-text.test.mjs && node test/typed-void-contract.test.mjs && node test/instancing-transport-contract.test.mjs && node test/automatic-instancing-contract.test.mjs && node test/jsdoc-fold-contract.test.mjs",
56
+ "test": "node test/native-blending-contract.test.mjs && node test/native-transform-contract.test.mjs && node test/troika-three-text.test.mjs && node test/typed-void-contract.test.mjs && node test/instancing-transport-contract.test.mjs && node test/automatic-instancing-contract.test.mjs && node test/jsdoc-fold-contract.test.mjs && node test/gea-native-declaration.test.mjs",
55
57
  "test:package": "node --experimental-vm-modules test/package.test.mjs"
56
58
  },
57
59
  "dependencies": {