@nayan-ui/engine 0.0.0-stage → 1.0.0-beta.4
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/LICENSE +21 -0
- package/NayanEngine.podspec +28 -0
- package/README.md +308 -2
- package/lib/module/assets/AssetBytesCache.js +76 -0
- package/lib/module/assets/AssetCache.js +19 -0
- package/lib/module/assets/AssetLoader.js +52 -0
- package/lib/module/assets/CacheGeneration.js +7 -0
- package/lib/module/assets/IndexedDbCache.web.js +149 -0
- package/lib/module/assets/ProceduralCache.js +249 -0
- package/lib/module/assets/font.js +139 -0
- package/lib/module/assets/loft.js +142 -0
- package/lib/module/assets/model.js +96 -0
- package/lib/module/assets/modelling.js +254 -0
- package/lib/module/assets/shape.js +926 -0
- package/lib/module/assets/source.js +58 -0
- package/lib/module/assets/source.web.js +22 -0
- package/lib/module/assets/storage.js +4 -0
- package/lib/module/assets/storage.shared.js +38 -0
- package/lib/module/assets/storage.web.js +38 -0
- package/lib/module/assets/surface.js +47 -0
- package/lib/module/assets/texture.js +69 -0
- package/lib/module/drawing/DrawingCanvas.js +38 -0
- package/lib/module/drawing/DrawingCanvas.web.js +206 -0
- package/lib/module/drawing/DrawingDocument.js +256 -0
- package/lib/module/drawing/types.js +4 -0
- package/lib/module/globals.d.ts +12 -0
- package/lib/module/index.js +39 -0
- package/lib/module/input/Joystick.js +84 -0
- package/lib/module/input/Joystick.web.js +93 -0
- package/lib/module/input/keyboard.js +250 -0
- package/lib/module/media/audio-worklet.js +34 -0
- package/lib/module/media/audio.js +97 -0
- package/lib/module/media/haptics.js +82 -0
- package/lib/module/media/output.web.js +66 -0
- package/lib/module/native/BrowserEngine.js +568 -0
- package/lib/module/native/EngineBridge.js +4 -0
- package/lib/module/native/NativeNayanEngine.js +7 -0
- package/lib/module/native/NativeNayanEngine.web.js +62 -0
- package/lib/module/native/prepare.js +7 -0
- package/lib/module/native/prepare.web.js +17 -0
- package/lib/module/native/spec/NativeNayanEngine.js +17 -0
- package/lib/module/package.json +1 -0
- package/lib/module/render/GameView.js +317 -0
- package/lib/module/render/camera.js +34 -0
- package/lib/module/render/host.js +39 -0
- package/lib/module/render/host.web.js +104 -0
- package/lib/module/render/light.js +24 -0
- package/lib/module/render/meshes.js +159 -0
- package/lib/module/render/quality.js +119 -0
- package/lib/module/render/responder.js +4 -0
- package/lib/module/render/responder.web.js +97 -0
- package/lib/module/render/touch.js +185 -0
- package/lib/module/render/useWorld.js +72 -0
- package/lib/module/types.js +30 -0
- package/lib/module/world/Scoreboard.js +161 -0
- package/lib/module/world/TransformBatch.js +128 -0
- package/lib/module/world/World.js +704 -0
- package/lib/module/world/color.js +80 -0
- package/lib/module/world/desc.js +187 -0
- package/lib/module/world/effects.js +463 -0
- package/lib/module/world/quat.js +86 -0
- package/lib/module/world/types.js +4 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/assets/AssetBytesCache.d.ts +7 -0
- package/lib/typescript/src/assets/AssetCache.d.ts +9 -0
- package/lib/typescript/src/assets/AssetLoader.d.ts +11 -0
- package/lib/typescript/src/assets/CacheGeneration.d.ts +3 -0
- package/lib/typescript/src/assets/IndexedDbCache.web.d.ts +20 -0
- package/lib/typescript/src/assets/ProceduralCache.d.ts +45 -0
- package/lib/typescript/src/assets/font.d.ts +53 -0
- package/lib/typescript/src/assets/loft.d.ts +31 -0
- package/lib/typescript/src/assets/model.d.ts +58 -0
- package/lib/typescript/src/assets/modelling.d.ts +157 -0
- package/lib/typescript/src/assets/shape.d.ts +203 -0
- package/lib/typescript/src/assets/source.d.ts +10 -0
- package/lib/typescript/src/assets/source.web.d.ts +8 -0
- package/lib/typescript/src/assets/storage.d.ts +2 -0
- package/lib/typescript/src/assets/storage.shared.d.ts +7 -0
- package/lib/typescript/src/assets/storage.web.d.ts +7 -0
- package/lib/typescript/src/assets/surface.d.ts +17 -0
- package/lib/typescript/src/assets/texture.d.ts +30 -0
- package/lib/typescript/src/drawing/DrawingCanvas.d.ts +5 -0
- package/lib/typescript/src/drawing/DrawingCanvas.web.d.ts +4 -0
- package/lib/typescript/src/drawing/DrawingDocument.d.ts +210 -0
- package/lib/typescript/src/drawing/types.d.ts +12 -0
- package/lib/typescript/src/index.d.ts +45 -0
- package/lib/typescript/src/input/Joystick.d.ts +20 -0
- package/lib/typescript/src/input/Joystick.web.d.ts +13 -0
- package/lib/typescript/src/input/keyboard.d.ts +17 -0
- package/lib/typescript/src/media/audio.d.ts +46 -0
- package/lib/typescript/src/media/haptics.d.ts +22 -0
- package/lib/typescript/src/media/output.web.d.ts +4 -0
- package/lib/typescript/src/native/BrowserEngine.d.ts +8 -0
- package/lib/typescript/src/native/EngineBridge.d.ts +26 -0
- package/lib/typescript/src/native/NativeNayanEngine.d.ts +5 -0
- package/lib/typescript/src/native/NativeNayanEngine.web.d.ts +10 -0
- package/lib/typescript/src/native/prepare.d.ts +3 -0
- package/lib/typescript/src/native/prepare.web.d.ts +2 -0
- package/lib/typescript/src/native/spec/NativeNayanEngine.d.ts +211 -0
- package/lib/typescript/src/render/GameView.d.ts +107 -0
- package/lib/typescript/src/render/camera.d.ts +6 -0
- package/lib/typescript/src/render/host.d.ts +29 -0
- package/lib/typescript/src/render/host.web.d.ts +16 -0
- package/lib/typescript/src/render/light.d.ts +12 -0
- package/lib/typescript/src/render/meshes.d.ts +45 -0
- package/lib/typescript/src/render/quality.d.ts +48 -0
- package/lib/typescript/src/render/responder.d.ts +2 -0
- package/lib/typescript/src/render/responder.web.d.ts +40 -0
- package/lib/typescript/src/render/touch.d.ts +26 -0
- package/lib/typescript/src/render/useWorld.d.ts +20 -0
- package/lib/typescript/src/types.d.ts +81 -0
- package/lib/typescript/src/world/Scoreboard.d.ts +75 -0
- package/lib/typescript/src/world/TransformBatch.d.ts +39 -0
- package/lib/typescript/src/world/World.d.ts +207 -0
- package/lib/typescript/src/world/color.d.ts +18 -0
- package/lib/typescript/src/world/desc.d.ts +17 -0
- package/lib/typescript/src/world/effects.d.ts +157 -0
- package/lib/typescript/src/world/quat.d.ts +31 -0
- package/lib/typescript/src/world/types.d.ts +238 -0
- package/lib/web/audio-worklet.js +29 -0
- package/lib/web/engine_core_bg.wasm +0 -0
- package/lib/web/index.js +8240 -0
- package/lib/web/types/src/assets/AssetBytesCache.d.ts +6 -0
- package/lib/web/types/src/assets/AssetCache.d.ts +8 -0
- package/lib/web/types/src/assets/AssetLoader.d.ts +10 -0
- package/lib/web/types/src/assets/CacheGeneration.d.ts +2 -0
- package/lib/web/types/src/assets/IndexedDbCache.d.ts +19 -0
- package/lib/web/types/src/assets/ProceduralCache.d.ts +44 -0
- package/lib/web/types/src/assets/font.d.ts +52 -0
- package/lib/web/types/src/assets/loft.d.ts +30 -0
- package/lib/web/types/src/assets/model.d.ts +57 -0
- package/lib/web/types/src/assets/modelling.d.ts +156 -0
- package/lib/web/types/src/assets/shape.d.ts +202 -0
- package/lib/web/types/src/assets/source.d.ts +7 -0
- package/lib/web/types/src/assets/storage.d.ts +6 -0
- package/lib/web/types/src/assets/storage.shared.d.ts +6 -0
- package/lib/web/types/src/assets/surface.d.ts +16 -0
- package/lib/web/types/src/assets/texture.d.ts +29 -0
- package/lib/web/types/src/drawing/DrawingCanvas.d.ts +3 -0
- package/lib/web/types/src/drawing/DrawingDocument.d.ts +209 -0
- package/lib/web/types/src/drawing/types.d.ts +11 -0
- package/lib/web/types/src/index.d.ts +44 -0
- package/lib/web/types/src/input/Joystick.d.ts +12 -0
- package/lib/web/types/src/input/keyboard.d.ts +16 -0
- package/lib/web/types/src/media/audio.d.ts +45 -0
- package/lib/web/types/src/media/haptics.d.ts +21 -0
- package/lib/web/types/src/media/output.d.ts +3 -0
- package/lib/web/types/src/native/BrowserEngine.d.ts +7 -0
- package/lib/web/types/src/native/EngineBridge.d.ts +25 -0
- package/lib/web/types/src/native/NativeNayanEngine.d.ts +9 -0
- package/lib/web/types/src/native/prepare.d.ts +1 -0
- package/lib/web/types/src/native/spec/NativeNayanEngine.d.ts +210 -0
- package/lib/web/types/src/render/GameView.d.ts +106 -0
- package/lib/web/types/src/render/camera.d.ts +5 -0
- package/lib/web/types/src/render/host.d.ts +15 -0
- package/lib/web/types/src/render/light.d.ts +11 -0
- package/lib/web/types/src/render/meshes.d.ts +44 -0
- package/lib/web/types/src/render/quality.d.ts +47 -0
- package/lib/web/types/src/render/responder.d.ts +39 -0
- package/lib/web/types/src/render/touch.d.ts +25 -0
- package/lib/web/types/src/render/useWorld.d.ts +19 -0
- package/lib/web/types/src/types.d.ts +80 -0
- package/lib/web/types/src/world/Scoreboard.d.ts +74 -0
- package/lib/web/types/src/world/TransformBatch.d.ts +38 -0
- package/lib/web/types/src/world/World.d.ts +206 -0
- package/lib/web/types/src/world/color.d.ts +17 -0
- package/lib/web/types/src/world/desc.d.ts +16 -0
- package/lib/web/types/src/world/effects.d.ts +156 -0
- package/lib/web/types/src/world/quat.d.ts +30 -0
- package/lib/web/types/src/world/types.d.ts +237 -0
- package/native/android/CMakeLists.txt +34 -0
- package/native/android/NayanEngineSpec.h +15 -0
- package/native/android/build.gradle +26 -0
- package/native/android/codegen/CMakeLists.txt +3 -0
- package/native/android/proguard-rules.pro +2 -0
- package/native/android/src/main/java/com/nayanui/engine/EngineSurfaceView.java +295 -0
- package/native/android/src/main/java/com/nayanui/engine/EngineSurfaceViewManager.java +45 -0
- package/native/android/src/main/java/com/nayanui/engine/NayanEnginePackage.java +24 -0
- package/native/core/build/EngineCore.xcframework/Info.plist +43 -0
- package/native/core/build/EngineCore.xcframework/ios-arm64/libengine_core.a +0 -0
- package/native/core/build/EngineCore.xcframework/ios-arm64-simulator/libengine_core.a +0 -0
- package/native/core/build/android/arm64-v8a/libengine_core.a +0 -0
- package/native/core/build/android/armeabi-v7a/libengine_core.a +0 -0
- package/native/core/build/android/x86/libengine_core.a +0 -0
- package/native/core/build/android/x86_64/libengine_core.a +0 -0
- package/native/core/include/drawing-licenses.txt +58 -0
- package/native/core/include/engine_core.h +331 -0
- package/native/cpp/NayanEngineModule.cpp +998 -0
- package/native/cpp/NayanEngineModule.h +174 -0
- package/native/cpp/generated/NayanEngineSpecJSI.cpp +2 -0
- package/native/cpp/generated/NayanEngineSpecJSI.h +1026 -0
- package/native/ios/NayanEngineProvider.h +5 -0
- package/native/ios/NayanEngineProvider.mm +21 -0
- package/native/ios/NayanEngineSurface.mm +252 -0
- package/native/scripts/check-package.mjs +39 -0
- package/package.json +162 -4
- package/react-native.config.js +18 -0
- package/src/native/spec/NativeNayanEngine.ts +265 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Niranjan Devasani
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "NayanEngine"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://github.com/ursnj/nayan-ui.git", :tag => "engine-v#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "native/cpp/**/*.{h,cpp}", "native/ios/**/*.{h,m,mm}"
|
|
17
|
+
s.vendored_frameworks = "native/core/build/EngineCore.xcframework"
|
|
18
|
+
# The Rust core's audio (cpal / CoreAudio), haptics (Core Haptics) and renderer (wgpu on Metal) need
|
|
19
|
+
# these system frameworks.
|
|
20
|
+
s.frameworks = "AudioToolbox", "AVFAudio", "CoreAudio", "CoreHaptics", "Foundation", "Metal", "QuartzCore", "UIKit"
|
|
21
|
+
|
|
22
|
+
s.pod_target_xcconfig = {
|
|
23
|
+
"HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/native/core/include\" \"$(PODS_TARGET_SRCROOT)/native/cpp\"",
|
|
24
|
+
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
install_modules_dependencies(s)
|
|
28
|
+
end
|
package/README.md
CHANGED
|
@@ -1,3 +1,309 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nayan-ui/engine
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@nayan-ui/engine) [](#license)
|
|
4
|
+
|
|
5
|
+
A fast 3D game engine for **React** and **React Native**. One TypeScript API on one Rust core: rendering (wgpu), physics (Rapier), skeletal and tween animation, particles, audio, haptics and 2D drawing.
|
|
6
|
+
|
|
7
|
+
**[Documentation](https://www.nayanui.com/game-engine)** · **[First game tutorial](https://www.nayanui.com/game-engine/first-game)** · **[API reference](https://www.nayanui.com/game-engine/api-reference)** · **[Guide for AI agents](https://www.nayanui.com/game-engine/agent-guide)**
|
|
8
|
+
|
|
9
|
+
- **Native speed on phones.** Rendering, physics, animation, culling and audio mixing run in Rust. On iOS and Android the game loop follows the display's clock, and frames are drawn on the engine's own render thread, so your JavaScript never waits on the GPU.
|
|
10
|
+
- **Small API.** `useWorld` + `GameView` is a complete scene. Spawn entities, move them, animate them: the engine steps, draws and cleans up.
|
|
11
|
+
- **Fast loading.** Images, glTF models, fonts and sounds decode on native background threads, and procedural meshes can be cached on disk between launches.
|
|
12
|
+
- **Same code on the web.** The core compiles to WebAssembly and draws with WebGPU, falling back to WebGL2.
|
|
13
|
+
|
|
14
|
+
| Platform | Renders with | Needs |
|
|
15
|
+
| ----------- | ------------------------------ | --------------------------------------------------------- |
|
|
16
|
+
| iOS | Metal | A development or store build (Expo Go doesn't include it) |
|
|
17
|
+
| Android | Vulkan or OpenGL ES | `minSdkVersion` 26, a development or store build |
|
|
18
|
+
| Web (React) | WebAssembly + WebGPU or WebGL2 | A modern browser over HTTPS or localhost |
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install @nayan-ui/engine
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Peer dependencies: `react`, plus `react-native` for apps. `expo-asset` is optional and only needed to load bundled files with `require(...)` in Expo.
|
|
27
|
+
|
|
28
|
+
### React Native (Expo)
|
|
29
|
+
|
|
30
|
+
1. Let Metro bundle 3D models, in `metro.config.js`:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
config.resolver.assetExts.push('glb', 'gltf');
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
2. Target Android 8.0 (API 26) or newer (with `expo-build-properties`), and allow vibration for haptics, in `app.json`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"expo": {
|
|
41
|
+
"android": { "permissions": ["android.permission.VIBRATE"] },
|
|
42
|
+
"plugins": [["expo-build-properties", { "android": { "minSdkVersion": 26 } }]]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
3. Make a development build: `npx expo run:ios` / `npx expo run:android` (or an EAS build). The engine ships prebuilt native libraries, so no Rust toolchain is needed.
|
|
48
|
+
|
|
49
|
+
Bare React Native apps autolink the same way: run `pod install` in `ios/` and set `minSdkVersion` 26 in Gradle.
|
|
50
|
+
|
|
51
|
+
Guard screens for builds without the native module (for example Expo Go) with `isEngineAvailable`:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { isEngineAvailable } from '@nayan-ui/engine';
|
|
55
|
+
|
|
56
|
+
if (!isEngineAvailable) return <Text>This screen needs a development build.</Text>;
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### React (web)
|
|
60
|
+
|
|
61
|
+
Nothing to configure. `useWorld` loads the WebAssembly core for you; if you create worlds yourself, `await warmUpEngine()` first.
|
|
62
|
+
|
|
63
|
+
## Quick start
|
|
64
|
+
|
|
65
|
+
A complete scene that works unchanged on every platform:
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { GameView, useWorld } from '@nayan-ui/engine';
|
|
69
|
+
|
|
70
|
+
export function BouncingBall() {
|
|
71
|
+
const world = useWorld(100, (w) => {
|
|
72
|
+
w.spawn({ mesh: 'plane', scale: [20, 1, 20], color: '#4C9A4C', physics: 'fixed' });
|
|
73
|
+
w.spawn({ mesh: 'sphere', position: [0, 5, 0], color: '#3399FF', physics: { type: 'dynamic', bounce: 0.6 } });
|
|
74
|
+
});
|
|
75
|
+
return world && <GameView source={world} camera={{ eye: [0, 4, 8], target: [0, 0, 0], fov: 1 }} style={{ flex: 1 }} />;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`useWorld` warms the engine up, creates the world (100 entity slots), runs your setup and disposes it on unmount. `GameView` draws it and steps it every frame.
|
|
80
|
+
|
|
81
|
+
## The five rules
|
|
82
|
+
|
|
83
|
+
1. **Own** a world with `useWorld(capacity, setup)`, or create it yourself after `await warmUpEngine()` and `dispose()` it on unmount.
|
|
84
|
+
2. **Play** in `GameView`'s `onUpdate(dt)`: read input, move things, spawn. The world is stepped for you right after (call `world.update(dt)` yourself only to step it earlier or with another `dt`).
|
|
85
|
+
3. **Keep handles**: `spawn` returns an `Entity`; change it with `set`, remove it with `despawn`.
|
|
86
|
+
4. **Batch per-frame moves** in `world.transforms`: they are applied in one native call when the world steps (`flush()` yourself before reading positions).
|
|
87
|
+
5. **Don't track disposal yourself**: check `world.isDisposed`. Cleanup calls are safe no-ops after `dispose()`.
|
|
88
|
+
|
|
89
|
+
## Performance
|
|
90
|
+
|
|
91
|
+
The engine is built so that a game written the obvious way stays smooth on mid-range Android phones.
|
|
92
|
+
|
|
93
|
+
- **Display clock and render thread (iOS and Android).** The display's vsync (`CADisplayLink` on iOS, `Choreographer` on Android) asks for each frame at 60 Hz. `onUpdate` steps the game and hands a copy of the frame to a native render thread, which culls, submits and presents it. The JS thread never waits on the GPU, and a frame that is still waiting when a newer one arrives is replaced, not queued. The clock stops while the app is in the background or the view is off screen. On the web, `requestAnimationFrame` drives the same loop.
|
|
94
|
+
- **Culling in Rust.** Everything outside the camera's view (and outside the light's box for shadows) is skipped every frame, so open levels and long tracks only pay for what is on screen. Don't cull scenery in JS.
|
|
95
|
+
- **Automatic quality.** `quality="auto"` (the default) starts at `high` and steps down (shadow detail first, then anti-aliasing, resolution last) only while the device can't keep up. `onStats` reports fps, update and render time, the quality chosen and the graphics backend.
|
|
96
|
+
- **Background loading.** `loadTexture`, `loadModel`, `loadFont` and `audio.load` decode on native threads in parallel, and meshes are registered with the renderer inside Rust.
|
|
97
|
+
- **Work between frames.** `afterNextFrame(fn)` runs `fn` right after the next frame leaves, so you can spread long setup work (map generation, mesh building) over several frames behind a loading screen.
|
|
98
|
+
- **Cached procedural assets.** `ProceduralCache` saves built geometry on disk keyed by a recipe, so later launches restore it instead of rebuilding; `peek(key)` returns a saved value without building anything.
|
|
99
|
+
|
|
100
|
+
Keep these habits:
|
|
101
|
+
|
|
102
|
+
- Build meshes once, at module scope or behind a cache, and spawn new entities from them. Mesh ids are app-wide and never freed (`getMeshIdLimit()` tells you how many there are), so never create a mesh per spawn or per frame.
|
|
103
|
+
- Move many things each frame through `world.transforms` and the `out` arguments of queries, which allocate nothing.
|
|
104
|
+
- Keep long one-off jobs out of gameplay frames: a single slow frame stalls Android's Hermes far longer than desktop JavaScript suggests.
|
|
105
|
+
|
|
106
|
+
## Common tasks
|
|
107
|
+
|
|
108
|
+
**Physics and collisions**
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const ball = world.spawn({ mesh: 'sphere', position: [0, 5, 0], physics: { type: 'dynamic', bounce: 0.6 } });
|
|
112
|
+
const coin = world.spawn({ mesh: 'cylinder', position: [2, 1, 0], physics: { type: 'fixed', sensor: true } });
|
|
113
|
+
world.impulse(ball, [0, 4, 0]);
|
|
114
|
+
world.forEachCollision((a, b, { started }) => {
|
|
115
|
+
if (started && (a === coin || b === coin)) world.despawn(coin);
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Animation** (runs in Rust and returns a Promise)
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
await world.animate(piece, { position: [2, 0, 3] }, { duration: 0.25, easing: 'back' });
|
|
123
|
+
world.animate(star, { scale: 1.2 }, { repeat: 'forever', yoyo: true });
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Skinned characters**: glTF clips play natively, with crossfades
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const hero = await loadModel(require('./hero.glb'));
|
|
130
|
+
console.log(hero.animations.map((clip) => clip.name)); // e.g. ['idle', 'walk', 'run']
|
|
131
|
+
const player = world.spawn({ mesh: hero });
|
|
132
|
+
world.playClip(player, 'idle', { speed: 0 }); // hold the first pose on the ready screen
|
|
133
|
+
world.playClip(player, 'run', { fade: 0.2 }); // crossfade when play starts
|
|
134
|
+
world.stopClip(player);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`world.sampleSkinnedVertices` reads chosen vertices of the next pose (for planting feet on uneven ground) without skinning the whole character in JS.
|
|
138
|
+
|
|
139
|
+
**Ready-made effects**
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { effects } from '@nayan-ui/engine';
|
|
143
|
+
|
|
144
|
+
effects.confetti(world, {
|
|
145
|
+
position: [0, 2, 0],
|
|
146
|
+
colors: ['#FF4D4D', '#FFD933'],
|
|
147
|
+
size: 0.12,
|
|
148
|
+
speed: 6,
|
|
149
|
+
count: 40,
|
|
150
|
+
lifetime: 1.2,
|
|
151
|
+
gravity: -6
|
|
152
|
+
});
|
|
153
|
+
effects.dust(world, { position: [0, 0, 0], color: [0.8, 0.75, 0.65, 0.6], size: 0.3, speed: 1.5, count: 6, lifetime: 0.5 });
|
|
154
|
+
await effects.pop(world, piece, { scale: 1, color: '#FF4D4D' }); // swell, flash white, shrink away
|
|
155
|
+
world.despawn(piece);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Also available: `sparkle`, `shatter`, `splash`, `sparks`, `explosion` and `flash`.
|
|
159
|
+
|
|
160
|
+
**Per-frame movement without allocations**
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { type MutableQuat, quat } from '@nayan-ui/engine';
|
|
164
|
+
|
|
165
|
+
const turn: MutableQuat = [0, 0, 0, 1];
|
|
166
|
+
const at: [number, number, number] = [0, 0, 0];
|
|
167
|
+
|
|
168
|
+
function onUpdate(dt: number) {
|
|
169
|
+
world.position(player, at); // queries can fill an `out` array
|
|
170
|
+
quat.y(heading, turn);
|
|
171
|
+
world.transforms.transform(car, carX, 0, carZ, turn[0], turn[1], turn[2], turn[3]); // applied in one native call as the world steps
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Models built in code**: bake a shape once, spawn it many times
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { createShape, lathe, merge, tube } from '@nayan-ui/engine';
|
|
179
|
+
|
|
180
|
+
// Built once per app (after warmUpEngine), at module scope.
|
|
181
|
+
const vase = createShape(
|
|
182
|
+
merge([
|
|
183
|
+
{
|
|
184
|
+
geometry: lathe(
|
|
185
|
+
[
|
|
186
|
+
[0, 0],
|
|
187
|
+
[0.4, 0.1],
|
|
188
|
+
[0.25, 0.8],
|
|
189
|
+
[0.3, 1]
|
|
190
|
+
],
|
|
191
|
+
{ segments: 24 }
|
|
192
|
+
)
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
geometry: tube(
|
|
196
|
+
[
|
|
197
|
+
[0.3, 0.9, 0],
|
|
198
|
+
[0.5, 0.7, 0],
|
|
199
|
+
[0.3, 0.5, 0]
|
|
200
|
+
],
|
|
201
|
+
{ radius: 0.04 }
|
|
202
|
+
),
|
|
203
|
+
color: [0.5, 0.25, 0.1]
|
|
204
|
+
} // part colors are 0..1 tuples
|
|
205
|
+
])
|
|
206
|
+
);
|
|
207
|
+
world.spawn({ mesh: vase, color: '#C8763A' });
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Also available: `extrude`, `loft`, `block`, `ellipsoid`, `parametricSurface`, `Solid` and `shapeGeometry` for the built-in shapes.
|
|
211
|
+
|
|
212
|
+
**Score, lives and timers**
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
world.scoreboard.handlers = { onTimeUp: () => endGame('lose') }; // the game's rules
|
|
216
|
+
const stopListening = world.scoreboard.listen({ onScore: setScore, onSecond: setTime }); // UI follows along
|
|
217
|
+
world.scoreboard.reset(3, 60); // 3 lives and a stopped 60 s countdown for the ready screen
|
|
218
|
+
world.scoreboard.resume(); // Start
|
|
219
|
+
world.scoreboard.add(10);
|
|
220
|
+
const timer = world.after(0.5, spawnWave); // game time: pauses with the game
|
|
221
|
+
world.cancelTimer(timer);
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Flat boards and puzzles**: `frontLight` keeps camera-facing surfaces at their exact color
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
<GameView source={world} camera={{ eye: [0, 0, 50], target: [0, 0, 0], fov: 1, ortho: 10 }} light={frontLight([-0.4, 0.6, 1], 0.5)} />
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Input**
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
<GameView
|
|
234
|
+
source={world}
|
|
235
|
+
onTap={(x, y) => {
|
|
236
|
+
const hit = world.pick(x, y);
|
|
237
|
+
if (hit) world.animate(hit.entity, { scale: 1.2 }, { yoyo: true, repeat: 1 });
|
|
238
|
+
}}
|
|
239
|
+
onSwipe={(direction) => move(direction)}
|
|
240
|
+
onDrag={({ phase, x, y }) => drag(phase, x, y)}
|
|
241
|
+
/>
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`onLongPress`, `onPinch`, the on-screen `Joystick` and `createKeyboardInput` (web) cover other controls.
|
|
245
|
+
|
|
246
|
+
**Models, textures, text, sound and haptics**
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import { audio, haptics, loadFont, loadModel, loadTexture } from '@nayan-ui/engine';
|
|
250
|
+
|
|
251
|
+
const car = await loadModel(require('./car.glb'), { fit: 2 }); // or a URL string on the web
|
|
252
|
+
const crate = await loadTexture(require('./crate.png'));
|
|
253
|
+
const font = await loadFont(require('./Inter-Bold.ttf'));
|
|
254
|
+
world.spawn({ mesh: car, position: [0, 0, 0] });
|
|
255
|
+
world.spawn({ mesh: 'cube', texture: crate });
|
|
256
|
+
world.spawn({ text: '2048', font, position: [0, 2, 0], scale: 0.5 });
|
|
257
|
+
|
|
258
|
+
const sfx = audio.load({ coin: require('./coin.wav') });
|
|
259
|
+
sfx.play('coin');
|
|
260
|
+
haptics.impact(0.5);
|
|
261
|
+
audio.enabled = settings.sounds; // respect the player's preferences
|
|
262
|
+
haptics.enabled = settings.haptics;
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Colors** are CSS strings (`'#FF4757'`, `'rgba(0, 0, 0, 0.5)'`) or linear 0..1 tuples (`[1, 0.3, 0.3]`; channels above 1 glow) for entities, animations, particles, lights and the background. Vertex colors baked into geometry are tuples. `toColor('#FF4757')` converts a CSS color when you need the numbers.
|
|
266
|
+
|
|
267
|
+
**Drawing**: a native paint canvas, also usable as a live texture inside games
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
import { DrawingCanvas, DrawingDocument } from '@nayan-ui/engine';
|
|
271
|
+
|
|
272
|
+
const paper = new DrawingDocument(512, 512);
|
|
273
|
+
paper.setBrush({ brushType: 'MARKER', color: '#e53935', width: 12, opacity: 1 });
|
|
274
|
+
<DrawingCanvas document={paper} mode="draw" />;
|
|
275
|
+
world.spawn({ mesh: 'plane', texture: paper.texture() }); // paint on a 3D object
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Drawings save as SVG paths and export transparent PNG snapshots.
|
|
279
|
+
|
|
280
|
+
## Troubleshooting
|
|
281
|
+
|
|
282
|
+
| Symptom | Fix |
|
|
283
|
+
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
284
|
+
| `isEngineAvailable` is `false` | You're in Expo Go or a build made before installing the engine: make a new development build. |
|
|
285
|
+
| `.glb` files fail to bundle | Add `glb` and `gltf` to Metro's `assetExts`. |
|
|
286
|
+
| Android build fails on `minSdkVersion` | Set it to 26 or higher. |
|
|
287
|
+
| An Android screenshot of the game view is blank | The engine draws into a `SurfaceView`: use `captureScreen(..., { handleGLSurfaceViewOnAndroid: true })`. |
|
|
288
|
+
| Out of mesh ids | A mesh is being created per spawn or per frame: build it once and reuse it (`getMeshIdLimit()`). |
|
|
289
|
+
|
|
290
|
+
## Learn more
|
|
291
|
+
|
|
292
|
+
| Topic | Guide |
|
|
293
|
+
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
294
|
+
| Build a complete game step by step | [First game](https://www.nayanui.com/game-engine/first-game) |
|
|
295
|
+
| Copy-paste solutions | [Recipes](https://www.nayanui.com/game-engine/recipes) |
|
|
296
|
+
| Worlds, entities, rotations, disposal | [World & entities](https://www.nayanui.com/game-engine/world-entities) |
|
|
297
|
+
| Effects and particles | [Effects](https://www.nayanui.com/game-engine/particles) |
|
|
298
|
+
| Procedural meshes and modelling | [Geometry](https://www.nayanui.com/game-engine/procedural-geometry) |
|
|
299
|
+
| Drawing and painting | [Drawing](https://www.nayanui.com/game-engine/drawing) |
|
|
300
|
+
| Performance and caching | [Performance](https://www.nayanui.com/game-engine/cache-performance) |
|
|
301
|
+
| Coding agents | [Agent guide](https://www.nayanui.com/game-engine/agent-guide) · [engine.json](https://www.nayanui.com/engine.json) |
|
|
302
|
+
|
|
303
|
+
## License
|
|
304
|
+
|
|
305
|
+
MIT © [Niranjan Devasani](https://github.com/ursnj)
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
Made with ❤️ by [Niranjan Reddy Devasani](https://github.com/ursnj)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { cacheGeneration } from "./CacheGeneration.js";
|
|
4
|
+
import { readPersistentAsset, removePersistentAsset, writePersistentAsset } from './storage';
|
|
5
|
+
// Decoded resources own their lifetime. Only temporary download bytes are evictable.
|
|
6
|
+
const BUDGET = 16 * 1024 * 1024;
|
|
7
|
+
const MAX_ENTRIES = 128;
|
|
8
|
+
const pending = new Map();
|
|
9
|
+
const retained = new Map();
|
|
10
|
+
let used = 0;
|
|
11
|
+
|
|
12
|
+
/** Share concurrent requests across loader variants; failed requests can always be retried. */
|
|
13
|
+
export function assetBytes(key, load) {
|
|
14
|
+
const bytes = retained.get(key);
|
|
15
|
+
if (bytes) {
|
|
16
|
+
retained.delete(key);
|
|
17
|
+
retained.set(key, bytes);
|
|
18
|
+
return Promise.resolve(bytes);
|
|
19
|
+
}
|
|
20
|
+
const existing = pending.get(key);
|
|
21
|
+
if (existing) return existing;
|
|
22
|
+
const epoch = cacheGeneration();
|
|
23
|
+
const request = Promise.resolve().then(async () => {
|
|
24
|
+
let saved = null;
|
|
25
|
+
try {
|
|
26
|
+
saved = await readPersistentAsset(key);
|
|
27
|
+
} catch {
|
|
28
|
+
/* Denied/corrupt storage is a miss. */
|
|
29
|
+
}
|
|
30
|
+
if (saved && epoch === cacheGeneration()) return saved;
|
|
31
|
+
const value = await load();
|
|
32
|
+
if (epoch === cacheGeneration()) {
|
|
33
|
+
// Await only the file write, never permission requests. Callers can rely on a completed load being saved.
|
|
34
|
+
try {
|
|
35
|
+
await writePersistentAsset(key, value);
|
|
36
|
+
} catch {
|
|
37
|
+
/* Keep existing records on quota/disk-full failures. */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}).then(value => {
|
|
42
|
+
if (epoch === cacheGeneration() && value.byteLength <= BUDGET) {
|
|
43
|
+
retained.set(key, value);
|
|
44
|
+
used += value.byteLength;
|
|
45
|
+
while (used > BUDGET || retained.size > MAX_ENTRIES) {
|
|
46
|
+
const oldest = retained.keys().next().value;
|
|
47
|
+
used -= retained.get(oldest).byteLength;
|
|
48
|
+
retained.delete(oldest);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}).finally(() => {
|
|
53
|
+
if (pending.get(key) === request) pending.delete(key);
|
|
54
|
+
});
|
|
55
|
+
pending.set(key, request);
|
|
56
|
+
return request;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A decoder rejected the payload: a later load must fetch fresh bytes, not replay bad data. */
|
|
60
|
+
export function forgetAssetBytes(key) {
|
|
61
|
+
if (key === undefined) return;
|
|
62
|
+
void removePersistentAsset(key).catch(() => {});
|
|
63
|
+
const bytes = retained.get(key);
|
|
64
|
+
if (bytes) {
|
|
65
|
+
used -= bytes.byteLength;
|
|
66
|
+
retained.delete(key);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Clearing disk must also prevent an earlier request from retaining its old bytes. */
|
|
71
|
+
export function clearAssetBytes() {
|
|
72
|
+
retained.clear();
|
|
73
|
+
pending.clear();
|
|
74
|
+
used = 0;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=AssetBytesCache.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { clearAssetBytes } from "./AssetBytesCache.js";
|
|
4
|
+
import { advanceCacheGeneration } from "./CacheGeneration.js";
|
|
5
|
+
import { clearPersistentStorage } from './storage';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Explicitly removes the engine's saved geometry and original model/texture/font/audio files.
|
|
9
|
+
* Also clears temporary download bytes. Existing worlds and loaded handles remain usable;
|
|
10
|
+
* registrations are session resources and will be rebuilt/reloaded on a future app launch.
|
|
11
|
+
* Custom ProceduralStorage adapters belong to their host and are not cleared by this function.
|
|
12
|
+
* Rejects if storage cannot be cleared, allowing a settings screen to report the failure.
|
|
13
|
+
*/
|
|
14
|
+
export async function clearAssetCache() {
|
|
15
|
+
advanceCacheGeneration();
|
|
16
|
+
clearAssetBytes();
|
|
17
|
+
await clearPersistentStorage();
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=AssetCache.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import NativeEngine from '../native/NativeNayanEngine';
|
|
4
|
+
import { prepareEngine } from '../native/prepare';
|
|
5
|
+
import { forgetAssetBytes } from "./AssetBytesCache.js";
|
|
6
|
+
import { assetUri, loadAssetBytes } from './source';
|
|
7
|
+
|
|
8
|
+
/** Shared session handles; source-byte persistence stays in AssetBytesCache and Rust. */
|
|
9
|
+
export class AssetLoader {
|
|
10
|
+
loads = new Map();
|
|
11
|
+
constructor(name) {
|
|
12
|
+
this.name = name;
|
|
13
|
+
}
|
|
14
|
+
load(source, decode, variant = []) {
|
|
15
|
+
let uri;
|
|
16
|
+
try {
|
|
17
|
+
uri = assetUri(source);
|
|
18
|
+
if (!uri) throw new Error("couldn't resolve the asset source");
|
|
19
|
+
} catch (cause) {
|
|
20
|
+
return Promise.reject(this.error(String(source), cause));
|
|
21
|
+
}
|
|
22
|
+
// Tuple encoding prevents a URI containing separators from colliding with loader options.
|
|
23
|
+
const key = JSON.stringify([uri, ...variant]);
|
|
24
|
+
const existing = this.loads.get(key);
|
|
25
|
+
if (existing) return existing;
|
|
26
|
+
const pending = Promise.resolve().then(() => prepareEngine()).then(async () => {
|
|
27
|
+
if (!NativeEngine) throw new Error('the engine core is not linked into this build');
|
|
28
|
+
const {
|
|
29
|
+
bytes
|
|
30
|
+
} = await loadAssetBytes(source);
|
|
31
|
+
try {
|
|
32
|
+
return await decode(NativeEngine, bytes);
|
|
33
|
+
} catch (cause) {
|
|
34
|
+
// Only rejected payloads invalidate saved bytes. Initialization/network failures don't.
|
|
35
|
+
forgetAssetBytes(uri);
|
|
36
|
+
throw cause;
|
|
37
|
+
}
|
|
38
|
+
}).catch(cause => {
|
|
39
|
+
this.loads.delete(key);
|
|
40
|
+
throw this.error(uri, cause);
|
|
41
|
+
});
|
|
42
|
+
this.loads.set(key, pending);
|
|
43
|
+
return pending;
|
|
44
|
+
}
|
|
45
|
+
error(source, cause) {
|
|
46
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
47
|
+
return new Error(`${this.name}: couldn't load ${source}: ${message}`, {
|
|
48
|
+
cause
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=AssetLoader.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Operations started before an explicit clear may finish for their caller, but cannot refill storage.
|
|
4
|
+
let generation = 0;
|
|
5
|
+
export const cacheGeneration = () => generation;
|
|
6
|
+
export const advanceCacheGeneration = () => ++generation;
|
|
7
|
+
//# sourceMappingURL=CacheGeneration.js.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Browser I/O only. Rust owns cache keys, record validation, geometry and the warm-memory budget.
|
|
4
|
+
const DATABASE = 'nayan-engine-assets-v1';
|
|
5
|
+
function complete(transaction) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
transaction.oncomplete = () => resolve();
|
|
8
|
+
transaction.onabort = () => reject(transaction.error ?? new Error('Asset storage transaction aborted'));
|
|
9
|
+
transaction.onerror = () => reject(transaction.error ?? new Error('Asset storage transaction failed'));
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function result(request) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
request.onsuccess = () => resolve(request.result);
|
|
15
|
+
request.onerror = () => reject(request.error ?? new Error('Asset storage request failed'));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export class IndexedDbCache {
|
|
19
|
+
tail = Promise.resolve();
|
|
20
|
+
pending = new Set();
|
|
21
|
+
generation = 0;
|
|
22
|
+
clearOperation = Promise.resolve();
|
|
23
|
+
constructor(database) {
|
|
24
|
+
this.database = database;
|
|
25
|
+
database.onversionchange = () => database.close();
|
|
26
|
+
}
|
|
27
|
+
static open() {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
let settled = false;
|
|
30
|
+
let request;
|
|
31
|
+
try {
|
|
32
|
+
request = indexedDB.open(DATABASE, 1);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
reject(error);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
request.onupgradeneeded = () => {
|
|
38
|
+
try {
|
|
39
|
+
const db = request.result;
|
|
40
|
+
db.createObjectStore('records');
|
|
41
|
+
db.createObjectStore('geometry').createIndex('writtenAt', 'writtenAt');
|
|
42
|
+
} catch {
|
|
43
|
+
request.transaction?.abort();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
request.onsuccess = () => {
|
|
47
|
+
if (settled) {
|
|
48
|
+
request.result.close();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
settled = true;
|
|
52
|
+
resolve(new IndexedDbCache(request.result));
|
|
53
|
+
};
|
|
54
|
+
request.onerror = () => {
|
|
55
|
+
settled = true;
|
|
56
|
+
reject(request.error ?? new Error('Asset database unavailable'));
|
|
57
|
+
};
|
|
58
|
+
request.onblocked = () => {
|
|
59
|
+
settled = true;
|
|
60
|
+
reject(new Error('Another tab blocked the asset database upgrade'));
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
enqueue(operation) {
|
|
65
|
+
const task = this.tail.then(operation);
|
|
66
|
+
this.pending.add(task);
|
|
67
|
+
this.tail = task.catch(() => {}); // A failed write must not prevent a later clear/write.
|
|
68
|
+
void task.then(() => this.pending.delete(task), () => this.pending.delete(task));
|
|
69
|
+
return task;
|
|
70
|
+
}
|
|
71
|
+
/** The Uint8Array is an owned copy made by Rust, never a view into growable WASM memory. */
|
|
72
|
+
write(name, bytes, geometry) {
|
|
73
|
+
const epoch = this.generation;
|
|
74
|
+
return this.enqueue(async () => {
|
|
75
|
+
if (epoch !== this.generation) return;
|
|
76
|
+
const transaction = this.database.transaction(['records', 'geometry'], 'readwrite');
|
|
77
|
+
const saved = complete(transaction);
|
|
78
|
+
try {
|
|
79
|
+
transaction.objectStore('records').put(bytes, name);
|
|
80
|
+
if (geometry) transaction.objectStore('geometry').put({
|
|
81
|
+
bytes: bytes.byteLength,
|
|
82
|
+
writtenAt: Date.now()
|
|
83
|
+
}, name);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
transaction.abort();
|
|
86
|
+
await saved.catch(() => {});
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
await saved;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async read(name) {
|
|
93
|
+
await this.tail;
|
|
94
|
+
const transaction = this.database.transaction('records', 'readonly');
|
|
95
|
+
const finished = complete(transaction);
|
|
96
|
+
const value = await Promise.all([result(transaction.objectStore('records').get(name)), finished]);
|
|
97
|
+
const bytes = value[0];
|
|
98
|
+
return bytes instanceof Uint8Array ? bytes : undefined;
|
|
99
|
+
}
|
|
100
|
+
remove(name) {
|
|
101
|
+
return this.enqueue(async () => {
|
|
102
|
+
const transaction = this.database.transaction(['records', 'geometry'], 'readwrite');
|
|
103
|
+
const finished = complete(transaction);
|
|
104
|
+
transaction.objectStore('records').delete(name);
|
|
105
|
+
transaction.objectStore('geometry').delete(name);
|
|
106
|
+
await finished;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
clear() {
|
|
110
|
+
this.generation++; // Cancel queued pre-clear writes; an active transaction finishes before clear.
|
|
111
|
+
this.clearOperation = this.enqueue(async () => {
|
|
112
|
+
const transaction = this.database.transaction(['records', 'geometry'], 'readwrite');
|
|
113
|
+
const finished = complete(transaction);
|
|
114
|
+
transaction.objectStore('records').clear();
|
|
115
|
+
transaction.objectStore('geometry').clear();
|
|
116
|
+
await finished;
|
|
117
|
+
});
|
|
118
|
+
return this.clearOperation;
|
|
119
|
+
}
|
|
120
|
+
flush() {
|
|
121
|
+
return Promise.all([...this.pending]).then(() => undefined);
|
|
122
|
+
}
|
|
123
|
+
cleared() {
|
|
124
|
+
return this.clearOperation;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Read recent geometry only. Large source files remain in IndexedDB until requested by a loader. */
|
|
128
|
+
warm(canFit, restore) {
|
|
129
|
+
const transaction = this.database.transaction(['records', 'geometry'], 'readonly');
|
|
130
|
+
const finished = complete(transaction);
|
|
131
|
+
const cursor = transaction.objectStore('geometry').index('writtenAt').openCursor(null, 'prev');
|
|
132
|
+
let scanned = 0;
|
|
133
|
+
cursor.onsuccess = () => {
|
|
134
|
+
const current = cursor.result;
|
|
135
|
+
if (!current || !canFit(0) || ++scanned > 4096) return;
|
|
136
|
+
const metadata = current.value;
|
|
137
|
+
if (typeof current.primaryKey === 'string' && metadata !== null && typeof metadata === 'object' && Number.isSafeInteger(metadata.bytes) && metadata.bytes > 0 && canFit(metadata.bytes)) {
|
|
138
|
+
const name = current.primaryKey;
|
|
139
|
+
const record = transaction.objectStore('records').get(name);
|
|
140
|
+
record.onsuccess = () => {
|
|
141
|
+
if (record.result instanceof Uint8Array && record.result.byteLength === metadata.bytes && canFit(record.result.byteLength)) restore(name, record.result);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
current.continue();
|
|
145
|
+
};
|
|
146
|
+
return finished;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=IndexedDbCache.web.js.map
|