@nayan-ui/engine 0.0.0-stage → 1.0.0-beta.3
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 +210 -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 +222 -0
- package/lib/module/assets/font.js +136 -0
- package/lib/module/assets/loft.js +142 -0
- package/lib/module/assets/model.js +74 -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 +36 -0
- package/lib/module/assets/storage.web.js +37 -0
- package/lib/module/assets/surface.js +47 -0
- package/lib/module/assets/texture.js +66 -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 +36 -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 +94 -0
- package/lib/module/media/haptics.js +82 -0
- package/lib/module/media/output.web.js +66 -0
- package/lib/module/native/BrowserEngine.js +551 -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 +249 -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/meshes.js +159 -0
- package/lib/module/render/quality.js +112 -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/types.js +30 -0
- package/lib/module/world/Scoreboard.js +143 -0
- package/lib/module/world/TransformBatch.js +126 -0
- package/lib/module/world/World.js +690 -0
- package/lib/module/world/desc.js +186 -0
- package/lib/module/world/effects.js +451 -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 +33 -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 +48 -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 +42 -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 +22 -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 +182 -0
- package/lib/typescript/src/render/GameView.d.ts +95 -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/meshes.d.ts +45 -0
- package/lib/typescript/src/render/quality.d.ts +46 -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/types.d.ts +72 -0
- package/lib/typescript/src/world/Scoreboard.d.ts +65 -0
- package/lib/typescript/src/world/TransformBatch.d.ts +39 -0
- package/lib/typescript/src/world/World.d.ts +199 -0
- package/lib/typescript/src/world/desc.d.ts +17 -0
- package/lib/typescript/src/world/effects.d.ts +156 -0
- package/lib/typescript/src/world/quat.d.ts +31 -0
- package/lib/typescript/src/world/types.d.ts +233 -0
- package/lib/web/audio-worklet.js +29 -0
- package/lib/web/engine_core_bg.wasm +0 -0
- package/lib/web/index.js +7974 -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 +32 -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 +47 -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 +41 -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 +21 -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 +181 -0
- package/lib/web/types/src/render/GameView.d.ts +94 -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/meshes.d.ts +44 -0
- package/lib/web/types/src/render/quality.d.ts +45 -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/types.d.ts +71 -0
- package/lib/web/types/src/world/Scoreboard.d.ts +64 -0
- package/lib/web/types/src/world/TransformBatch.d.ts +38 -0
- package/lib/web/types/src/world/World.d.ts +198 -0
- package/lib/web/types/src/world/desc.d.ts +16 -0
- package/lib/web/types/src/world/effects.d.ts +155 -0
- package/lib/web/types/src/world/quat.d.ts +30 -0
- package/lib/web/types/src/world/types.d.ts +232 -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 +234 -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 +322 -0
- package/native/cpp/NayanEngineModule.cpp +827 -0
- package/native/cpp/NayanEngineModule.h +144 -0
- package/native/cpp/generated/NayanEngineSpecJSI.cpp +2 -0
- package/native/cpp/generated/NayanEngineSpecJSI.h +921 -0
- package/native/ios/NayanEngineProvider.h +5 -0
- package/native/ios/NayanEngineProvider.mm +21 -0
- package/native/ios/NayanEngineSurface.mm +223 -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 +226 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import NativeEngine from '../native/NativeNayanEngine';
|
|
4
|
+
import { meshInfo } from "../render/meshes.js";
|
|
5
|
+
import { registerModelMesh } from "./model.js";
|
|
6
|
+
import { proceduralStorage } from './storage';
|
|
7
|
+
|
|
8
|
+
/** Optional storage override. The engine selects a best-effort platform cache by default. */
|
|
9
|
+
|
|
10
|
+
const MAGIC = 0x3143504e; // NPC1: static vertices (11 floats) and u32 indices, little endian.
|
|
11
|
+
const LIMIT = 32 * 1024 * 1024;
|
|
12
|
+
// JSON is escaped to ASCII so Hermes hosts need no TextEncoder/TextDecoder polyfill.
|
|
13
|
+
function encodeJson(value) {
|
|
14
|
+
const text = JSON.stringify(value).replace(/[\u007f-\uffff]/g, char => '\\u' + char.charCodeAt(0).toString(16).padStart(4, '0'));
|
|
15
|
+
const bytes = new Uint8Array(text.length);
|
|
16
|
+
for (let i = 0; i < text.length; i++) bytes[i] = text.charCodeAt(i);
|
|
17
|
+
return bytes;
|
|
18
|
+
}
|
|
19
|
+
function decodeJson(bytes) {
|
|
20
|
+
let text = '';
|
|
21
|
+
for (let i = 0; i < bytes.length; i += 4096) text += String.fromCharCode(...bytes.subarray(i, i + 4096));
|
|
22
|
+
return JSON.parse(text);
|
|
23
|
+
}
|
|
24
|
+
function checksum(bytes) {
|
|
25
|
+
let hash = 2166136261;
|
|
26
|
+
for (let i = 0; i < bytes.length; i++) hash = Math.imul(hash ^ bytes[i], 16777619);
|
|
27
|
+
return hash >>> 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Persists the output of a deterministic model factory, including nested model arrays/objects.
|
|
32
|
+
* Hits register saved vertices directly: no procedural tessellation, painting or optimization.
|
|
33
|
+
* Imported/skinned/textured models are intentionally excluded; their own loaders retain their rigs.
|
|
34
|
+
*/
|
|
35
|
+
export class ProceduralCache {
|
|
36
|
+
memory = new Map();
|
|
37
|
+
hits = 0;
|
|
38
|
+
builds = 0;
|
|
39
|
+
invalid = 0;
|
|
40
|
+
failures = 0;
|
|
41
|
+
writes = new Set();
|
|
42
|
+
constructor(revision, storage = proceduralStorage(revision)) {
|
|
43
|
+
this.revision = revision;
|
|
44
|
+
this.storage = storage;
|
|
45
|
+
}
|
|
46
|
+
get stats() {
|
|
47
|
+
return {
|
|
48
|
+
hits: this.hits,
|
|
49
|
+
builds: this.builds,
|
|
50
|
+
invalid: this.invalid,
|
|
51
|
+
failures: this.failures,
|
|
52
|
+
pendingWrites: this.writes.size
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async flush() {
|
|
56
|
+
await Promise.all([...this.writes]);
|
|
57
|
+
}
|
|
58
|
+
get(key, build) {
|
|
59
|
+
if (this.memory.has(key)) return this.memory.get(key);
|
|
60
|
+
if (this.storage) {
|
|
61
|
+
try {
|
|
62
|
+
const bytes = this.storage.read(key);
|
|
63
|
+
if (bytes) {
|
|
64
|
+
try {
|
|
65
|
+
const value = this.restore(bytes, key);
|
|
66
|
+
this.memory.set(key, value);
|
|
67
|
+
this.hits++;
|
|
68
|
+
return value;
|
|
69
|
+
} catch {
|
|
70
|
+
this.invalid++;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
this.failures++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const value = build();
|
|
78
|
+
this.builds++;
|
|
79
|
+
this.memory.set(key, value);
|
|
80
|
+
if (this.storage) {
|
|
81
|
+
try {
|
|
82
|
+
const result = this.storage.write(key, this.encode(value, key));
|
|
83
|
+
if (result) {
|
|
84
|
+
const pending = Promise.resolve(result).catch(() => {
|
|
85
|
+
this.failures++;
|
|
86
|
+
}).finally(() => {
|
|
87
|
+
this.writes.delete(pending);
|
|
88
|
+
});
|
|
89
|
+
this.writes.add(pending);
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
this.failures++;
|
|
93
|
+
} // Storage/unsupported values never prevent a game from running.
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
encode(value, key) {
|
|
98
|
+
const models = [];
|
|
99
|
+
const ids = new Map();
|
|
100
|
+
const visit = v => {
|
|
101
|
+
if (v === undefined) return {
|
|
102
|
+
$undefined: true
|
|
103
|
+
};
|
|
104
|
+
if (v === null || typeof v === 'string' || typeof v === 'boolean') return v;
|
|
105
|
+
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
|
106
|
+
if (Array.isArray(v)) return v.map(visit);
|
|
107
|
+
if (typeof v !== 'object' || !v || Object.getPrototypeOf(v) !== Object.prototype) throw Error('not a plain procedural asset');
|
|
108
|
+
const obj = v;
|
|
109
|
+
if (Number.isSafeInteger(obj.id) && Array.isArray(obj.size) && obj.size.length === 3) {
|
|
110
|
+
const model = v;
|
|
111
|
+
const info = meshInfo(model.id);
|
|
112
|
+
if (!info || info.texture !== undefined || 'jointCount' in obj || 'animations' in obj) throw Error('not a static procedural model');
|
|
113
|
+
let index = ids.get(model.id);
|
|
114
|
+
if (index === undefined) {
|
|
115
|
+
index = models.length;
|
|
116
|
+
ids.set(model.id, index);
|
|
117
|
+
models.push(model);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
$mesh: index
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (Number.isSafeInteger(obj.id)) throw Error('native handles cannot be persisted as plain data');
|
|
124
|
+
const result = {};
|
|
125
|
+
for (const name of Object.keys(obj)) {
|
|
126
|
+
if (name.startsWith('$')) throw Error('reserved cache key');
|
|
127
|
+
if (Object.getOwnPropertyDescriptor(obj, name)?.get) throw Error('lazy values must be cached separately');
|
|
128
|
+
result[name] = visit(obj[name]);
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
};
|
|
132
|
+
const root = visit(value);
|
|
133
|
+
const infos = models.map(model => meshInfo(model.id));
|
|
134
|
+
const header = {
|
|
135
|
+
revision: this.revision,
|
|
136
|
+
key,
|
|
137
|
+
value: root,
|
|
138
|
+
meshes: infos.map(info => ({
|
|
139
|
+
floats: info.geometry.vertices.length,
|
|
140
|
+
indices: info.geometry.indices.length,
|
|
141
|
+
shine: info.shine ?? 0
|
|
142
|
+
}))
|
|
143
|
+
};
|
|
144
|
+
const json = encodeJson(header);
|
|
145
|
+
const start = 16 + json.length + 3 & ~3;
|
|
146
|
+
const length = start + infos.reduce((sum, info) => sum + info.geometry.vertices.byteLength + info.geometry.indices.byteLength, 0);
|
|
147
|
+
if (length > LIMIT) throw Error('procedural asset too large');
|
|
148
|
+
const bytes = new Uint8Array(length),
|
|
149
|
+
view = new DataView(bytes.buffer);
|
|
150
|
+
view.setUint32(0, MAGIC, true);
|
|
151
|
+
view.setUint32(4, json.length, true);
|
|
152
|
+
view.setUint32(8, length, true);
|
|
153
|
+
bytes.set(json, 16);
|
|
154
|
+
let offset = start;
|
|
155
|
+
for (const {
|
|
156
|
+
geometry
|
|
157
|
+
} of infos) for (const data of [geometry.vertices, geometry.indices]) {
|
|
158
|
+
bytes.set(new Uint8Array(data.buffer, data.byteOffset, data.byteLength), offset);
|
|
159
|
+
offset += data.byteLength;
|
|
160
|
+
}
|
|
161
|
+
view.setUint32(12, checksum(bytes.subarray(16)), true);
|
|
162
|
+
return bytes;
|
|
163
|
+
}
|
|
164
|
+
restore(raw, key) {
|
|
165
|
+
if (raw.length < 16 || raw.length > LIMIT) throw Error('cache size');
|
|
166
|
+
const bytes = raw.byteOffset % 4 ? raw.slice() : raw;
|
|
167
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
168
|
+
const jsonLength = view.getUint32(4, true);
|
|
169
|
+
if (view.getUint32(0, true) !== MAGIC || view.getUint32(8, true) !== bytes.length || jsonLength > bytes.length - 16 || view.getUint32(12, true) !== checksum(bytes.subarray(16))) throw Error('cache integrity');
|
|
170
|
+
const header = decodeJson(bytes.subarray(16, 16 + jsonLength));
|
|
171
|
+
if (!('value' in header) || header.revision !== this.revision || header.key !== key || !Array.isArray(header.meshes)) throw Error('cache revision');
|
|
172
|
+
let offset = 16 + jsonLength + 3 & ~3;
|
|
173
|
+
const payloads = [];
|
|
174
|
+
// Validate the complete record before registering any mesh (including the value's references).
|
|
175
|
+
for (const mesh of header.meshes) {
|
|
176
|
+
if (!Number.isSafeInteger(mesh.floats) || mesh.floats <= 0 || mesh.floats % 11 || !Number.isSafeInteger(mesh.indices) || mesh.indices <= 0 || mesh.indices % 3 || !Number.isFinite(mesh.shine) || mesh.shine < 0 || mesh.shine > 1) throw Error('cache geometry');
|
|
177
|
+
const end = offset + (mesh.floats + mesh.indices) * 4;
|
|
178
|
+
if (end > bytes.length) throw Error('cache truncated');
|
|
179
|
+
const vertices = new Float32Array(bytes.buffer, bytes.byteOffset + offset, mesh.floats);
|
|
180
|
+
const indices = new Uint32Array(bytes.buffer, bytes.byteOffset + offset + mesh.floats * 4, mesh.indices);
|
|
181
|
+
for (const value of vertices) if (!Number.isFinite(value)) throw Error('cache vertex');
|
|
182
|
+
for (const index of indices) if (index >= mesh.floats / 11) throw Error('cache index');
|
|
183
|
+
payloads.push({
|
|
184
|
+
vertices,
|
|
185
|
+
indices,
|
|
186
|
+
shine: mesh.shine
|
|
187
|
+
});
|
|
188
|
+
offset = end;
|
|
189
|
+
}
|
|
190
|
+
if (offset !== bytes.length) throw Error('cache length');
|
|
191
|
+
const mapValue = (v, models) => {
|
|
192
|
+
if (v === null || typeof v !== 'object') return v;
|
|
193
|
+
if (Array.isArray(v)) return v.map(item => mapValue(item, models));
|
|
194
|
+
if ('$mesh' in v) {
|
|
195
|
+
const index = v.$mesh;
|
|
196
|
+
if (!Number.isSafeInteger(index) || typeof index !== 'number' || index < 0 || index >= header.meshes.length || Object.keys(v).length !== 1) throw Error('cache model reference');
|
|
197
|
+
return models?.[index];
|
|
198
|
+
}
|
|
199
|
+
if ('$undefined' in v) return undefined;
|
|
200
|
+
const result = {};
|
|
201
|
+
for (const name of Object.keys(v)) {
|
|
202
|
+
if (name.startsWith('$') || name === '__proto__') throw Error('cache value');
|
|
203
|
+
result[name] = mapValue(v[name], models);
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
206
|
+
};
|
|
207
|
+
mapValue(header.value);
|
|
208
|
+
if (!NativeEngine) throw Error('native engine unavailable');
|
|
209
|
+
const restored = payloads.map(mesh => {
|
|
210
|
+
let id = NativeEngine.meshCreate(mesh.vertices.slice().buffer, mesh.indices.slice().buffer);
|
|
211
|
+
if (id < 0) throw Error('cache registration');
|
|
212
|
+
id = registerModelMesh(id, mesh.shine);
|
|
213
|
+
const [x, y, z] = NativeEngine.modelSize(id);
|
|
214
|
+
return {
|
|
215
|
+
id,
|
|
216
|
+
size: [x, y, z]
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
return mapValue(header.value, restored);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=ProceduralCache.js.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Fonts for 3D text: each character becomes an extruded mesh, built by the Rust core.
|
|
4
|
+
import { registerMesh } from "../render/meshes.js";
|
|
5
|
+
import { AssetLoader } from "./AssetLoader.js";
|
|
6
|
+
import { nativeGeometry } from "./model.js";
|
|
7
|
+
/** Printable ASCII plus common arithmetic and typography, prewarmed for ordinary labels. */
|
|
8
|
+
export const DEFAULT_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~×÷±−°·•–—…‘’“”';
|
|
9
|
+
|
|
10
|
+
/** A loaded font. Use it on an entity with `text`: `{ text: "2048", font }`. */
|
|
11
|
+
|
|
12
|
+
const loader = new AssetLoader('loadFont');
|
|
13
|
+
const extend = new WeakMap();
|
|
14
|
+
|
|
15
|
+
// Format controls affect presentation, not glyph count or spacing. In particular, a text/emoji
|
|
16
|
+
// variation selector must not append a second fallback question mark to a symbol.
|
|
17
|
+
function isInvisible(ch) {
|
|
18
|
+
const code = ch.codePointAt(0);
|
|
19
|
+
return code >= 0x200b && code <= 0x200f || code >= 0x202a && code <= 0x202e || code >= 0x2060 && code <= 0x206f || code >= 0xfe00 && code <= 0xfe0f || code === 0xfeff || code >= 0xe0100 && code <= 0xe01ef;
|
|
20
|
+
}
|
|
21
|
+
const WHITESPACE = /\s/u;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initializes the engine automatically. Loads a TrueType / OpenType font (.ttf / .otf) for 3D text. The source/depth/weight are cached independently of the requested prewarm characters. Letters are real meshes: lit, shadowed,
|
|
25
|
+
* and every copy of a letter on screen is drawn in one call. At scale 1, letters are 1 unit tall (em).
|
|
26
|
+
* Additional Unicode glyphs are built on demand from the same font and cached. The font must contain
|
|
27
|
+
* an outline for the character; missing characters still use ?, including unsupported emoji.
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* const font = await loadFont(require("./assets/Inter-Bold.ttf"), { chars: "0123456789" });
|
|
31
|
+
* world.spawn({ text: "2048", font, scale: 0.5, color: [1, 1, 1] });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function loadFont(source, options = {}) {
|
|
35
|
+
const depth = options.depth ?? 0.2;
|
|
36
|
+
const chars = [...new Set(`${options.chars ?? DEFAULT_CHARS} ?`.normalize('NFC'))].filter(ch => !isInvisible(ch)).join('');
|
|
37
|
+
const weight = options.weight ?? 0;
|
|
38
|
+
const pending = loader.load(source, (native, bytes) => {
|
|
39
|
+
const id = native.fontLoad(bytes, depth, chars, weight);
|
|
40
|
+
if (id < 0) throw new Error(native.loadError() || 'not a TrueType / OpenType font');
|
|
41
|
+
const glyphs = new Map();
|
|
42
|
+
const registered = new Set();
|
|
43
|
+
const read = fontId => {
|
|
44
|
+
const data = native.fontGlyphs(fontId);
|
|
45
|
+
for (let i = 0; i + 3 <= data.length - 1; i += 3) {
|
|
46
|
+
const mesh = data[i + 1];
|
|
47
|
+
if (mesh >= 0 && !registered.has(mesh)) {
|
|
48
|
+
registerMesh(mesh, {
|
|
49
|
+
geometry: nativeGeometry(mesh)
|
|
50
|
+
});
|
|
51
|
+
registered.add(mesh);
|
|
52
|
+
}
|
|
53
|
+
glyphs.set(String.fromCodePoint(data[i]), {
|
|
54
|
+
mesh,
|
|
55
|
+
advance: data[i + 2]
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return data[data.length - 1];
|
|
59
|
+
};
|
|
60
|
+
const font = {
|
|
61
|
+
id,
|
|
62
|
+
capHeight: read(id),
|
|
63
|
+
glyphs
|
|
64
|
+
};
|
|
65
|
+
const attempted = new Set(chars);
|
|
66
|
+
extend.set(font, text => {
|
|
67
|
+
let missing = '';
|
|
68
|
+
for (const ch of text) {
|
|
69
|
+
if (isInvisible(ch) || attempted.has(ch)) continue;
|
|
70
|
+
attempted.add(ch);
|
|
71
|
+
missing += ch;
|
|
72
|
+
}
|
|
73
|
+
if (!missing) return;
|
|
74
|
+
// One native call per group of new codepoints, never one per letter or repeated label.
|
|
75
|
+
try {
|
|
76
|
+
const extra = native.fontLoad(bytes, depth, missing, weight);
|
|
77
|
+
if (extra < 0) throw new Error(native.loadError());
|
|
78
|
+
read(extra);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
for (const ch of missing) attempted.delete(ch);
|
|
81
|
+
throw new Error(`loadFont: couldn't build additional glyphs: ${error instanceof Error ? error.message : String(error)}`, {
|
|
82
|
+
cause: error
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return font;
|
|
87
|
+
}, [depth, weight]);
|
|
88
|
+
return pending.then(font => {
|
|
89
|
+
extend.get(font)?.(chars);
|
|
90
|
+
return font;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const DIGIT = /[0-9]/;
|
|
94
|
+
/** Walk one normalized line for both measuring and placing, so fallback widths always agree. */
|
|
95
|
+
function walkText(font, text, options, placed) {
|
|
96
|
+
text = text.normalize('NFC');
|
|
97
|
+
extend.get(font)?.(options.tabular ? `${text}0123456789` : text);
|
|
98
|
+
const spacing = options.letterSpacing ?? 0;
|
|
99
|
+
let digit = 0;
|
|
100
|
+
if (options.tabular) for (const d of '0123456789') digit = Math.max(digit, font.glyphs.get(d)?.advance ?? 0);
|
|
101
|
+
let width = 0;
|
|
102
|
+
let letters = 0;
|
|
103
|
+
for (const ch of text) {
|
|
104
|
+
if (isInvisible(ch)) continue;
|
|
105
|
+
const glyph = font.glyphs.get(ch) ?? (WHITESPACE.test(ch) ? font.glyphs.get(' ') : undefined) ?? font.glyphs.get('?');
|
|
106
|
+
if (!glyph) continue;
|
|
107
|
+
const advance = digit > 0 && DIGIT.test(ch) ? digit : glyph.advance;
|
|
108
|
+
if (letters++ > 0) width += spacing;
|
|
109
|
+
if (placed && glyph.mesh >= 0) placed.push({
|
|
110
|
+
mesh: glyph.mesh,
|
|
111
|
+
x: width + (advance - glyph.advance) / 2,
|
|
112
|
+
y: 0
|
|
113
|
+
});
|
|
114
|
+
width += advance;
|
|
115
|
+
}
|
|
116
|
+
return width;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A line's advance in ems, with the same Unicode glyph loading, spacing and fallbacks as rendering. */
|
|
120
|
+
export function measureText(font, text, options = {}) {
|
|
121
|
+
return walkText(font, text, options);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Glyph placements for a line of text: centered vertically on capitals, aligned horizontally. */
|
|
125
|
+
export function layoutText(font, text, align, options = {}) {
|
|
126
|
+
const placed = [];
|
|
127
|
+
const width = walkText(font, text, options, placed);
|
|
128
|
+
const shift = align === 'left' ? 0 : align === 'right' ? -width : -width / 2;
|
|
129
|
+
const lift = -font.capHeight / 2;
|
|
130
|
+
for (const p of placed) {
|
|
131
|
+
p.x += shift;
|
|
132
|
+
p.y = lift;
|
|
133
|
+
}
|
|
134
|
+
return placed;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=font.js.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { lathe, merge, recolor } from "./shape.js";
|
|
4
|
+
import { parametricSurface } from "./surface.js";
|
|
5
|
+
/**
|
|
6
|
+
* Smooth closed body through rounded rectangular cross sections along Z. Useful for vehicle
|
|
7
|
+
* shells, cabins, shoes and fairings. Caps have separate flat normals and planar UVs; sides have
|
|
8
|
+
* continuous longitudinal UVs and a welded seam. Input sections may increase or decrease in Z.
|
|
9
|
+
*/
|
|
10
|
+
export function loft(sections, options = {}) {
|
|
11
|
+
if (sections.length < 2 || sections.some(s => ![s.z, s.bottom, s.top, s.width, s.topWidth ?? s.width, s.square ?? 4].every(Number.isFinite) || s.width < 0 || (s.topWidth ?? s.width) < 0 || s.top <= s.bottom || (s.square ?? 4) < 1)) throw new Error('loft: needs at least two valid cross sections');
|
|
12
|
+
const ordered = sections[0].z > sections[sections.length - 1].z ? [...sections].reverse() : sections;
|
|
13
|
+
if (ordered.some((s, i) => i > 0 && s.z <= ordered[i - 1].z)) throw new Error('loft: sections must be ordered along Z');
|
|
14
|
+
const n = options.around ?? 32,
|
|
15
|
+
steps = options.smoothness ?? 4;
|
|
16
|
+
if (!Number.isInteger(n) || n < 6 || n > 512 || !Number.isInteger(steps) || steps < 1 || steps > 64) throw new Error('loft: invalid around/smoothness');
|
|
17
|
+
const values = ordered.map(s => [s.z, s.bottom, s.top, s.width, s.topWidth ?? s.width, s.square ?? 4]);
|
|
18
|
+
const at = i => values[Math.max(0, Math.min(values.length - 1, i))];
|
|
19
|
+
const row = v => {
|
|
20
|
+
const cursor = v * (values.length - 1),
|
|
21
|
+
i = Math.min(values.length - 2, Math.floor(cursor)),
|
|
22
|
+
t = cursor - i;
|
|
23
|
+
const a = at(i - 1),
|
|
24
|
+
b = at(i),
|
|
25
|
+
c = at(i + 1),
|
|
26
|
+
d = at(i + 2);
|
|
27
|
+
const f = b.map((_, k) => 0.5 * (2 * b[k] + (-a[k] + c[k]) * t + (2 * a[k] - 5 * b[k] + 4 * c[k] - d[k]) * t * t + (-a[k] + 3 * b[k] - 3 * c[k] + d[k]) * t * t * t));
|
|
28
|
+
f[2] = Math.max(f[1] + 1e-6, f[2]);
|
|
29
|
+
f[3] = Math.max(0, f[3]);
|
|
30
|
+
f[4] = Math.max(0, f[4]);
|
|
31
|
+
f[5] = Math.max(1, f[5]);
|
|
32
|
+
return f;
|
|
33
|
+
};
|
|
34
|
+
const rowCount = (sections.length - 1) * steps;
|
|
35
|
+
const rows = Array.from({
|
|
36
|
+
length: rowCount + 1
|
|
37
|
+
}, (_, i) => row(i / rowCount));
|
|
38
|
+
const point = (u, v) => {
|
|
39
|
+
const [z, bottom, top, width, topWidth, square] = rows[Math.round(v * rowCount)],
|
|
40
|
+
angle = -Math.PI / 2 + u * Math.PI * 2;
|
|
41
|
+
const c = Math.cos(angle),
|
|
42
|
+
s = Math.sin(angle),
|
|
43
|
+
exponent = 2 / square;
|
|
44
|
+
const x = Math.sign(c) * Math.abs(c) ** exponent,
|
|
45
|
+
y = Math.sign(s) * Math.abs(s) ** exponent;
|
|
46
|
+
return [x * (y > 0 ? width + (topWidth - width) * y : width), (bottom + top) / 2 + y * (top - bottom) / 2, z];
|
|
47
|
+
};
|
|
48
|
+
const side = parametricSurface(point, {
|
|
49
|
+
uSegments: n,
|
|
50
|
+
vSegments: rowCount,
|
|
51
|
+
wrapU: true
|
|
52
|
+
});
|
|
53
|
+
const parts = [{
|
|
54
|
+
geometry: side
|
|
55
|
+
}];
|
|
56
|
+
for (const [v, facing] of [[0, -1], [1, 1]]) {
|
|
57
|
+
const section = rows[v * rowCount],
|
|
58
|
+
cy = (section[1] + section[2]) / 2;
|
|
59
|
+
const vertices = new Float32Array((n + 1) * 11),
|
|
60
|
+
indices = [];
|
|
61
|
+
vertices.set([0, cy, section[0], 0, 0, facing, 1, 1, 1, 0.5, 0.5]);
|
|
62
|
+
for (let j = 0; j < n; j++) {
|
|
63
|
+
const p = point(j / n, v),
|
|
64
|
+
width = Math.max(section[3], section[4], 1e-6);
|
|
65
|
+
vertices.set([p[0], p[1], p[2], 0, 0, facing, 1, 1, 1, 0.5 + p[0] / (2 * width), (p[1] - section[1]) / (section[2] - section[1])], (j + 1) * 11);
|
|
66
|
+
const a = j + 1,
|
|
67
|
+
b = (j + 1) % n + 1;
|
|
68
|
+
if (facing === 1) indices.push(0, a, b);else indices.push(0, b, a);
|
|
69
|
+
}
|
|
70
|
+
parts.push({
|
|
71
|
+
geometry: {
|
|
72
|
+
vertices,
|
|
73
|
+
indices: new Uint32Array(indices)
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return paint(merge(parts), options.color);
|
|
78
|
+
}
|
|
79
|
+
function paint(geometry, color) {
|
|
80
|
+
return color ? recolor(geometry, typeof color === 'function' ? ({
|
|
81
|
+
position,
|
|
82
|
+
normal
|
|
83
|
+
}) => color(position, normal) : () => color) : geometry;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A rounded rectangular cross-section with flat front/back caps, centered at the origin. */
|
|
87
|
+
export function block(w, h, d, square = 6, color) {
|
|
88
|
+
return loft([{
|
|
89
|
+
z: -d / 2,
|
|
90
|
+
bottom: -h / 2,
|
|
91
|
+
top: h / 2,
|
|
92
|
+
width: w / 2,
|
|
93
|
+
square
|
|
94
|
+
}, {
|
|
95
|
+
z: d / 2,
|
|
96
|
+
bottom: -h / 2,
|
|
97
|
+
top: h / 2,
|
|
98
|
+
width: w / 2,
|
|
99
|
+
square
|
|
100
|
+
}], {
|
|
101
|
+
around: 16,
|
|
102
|
+
smoothness: 1,
|
|
103
|
+
color
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** An exact ellipsoid with analytic normals, rather than capped/truncated poles. */
|
|
108
|
+
export function ellipsoid(radii, color, around = 16) {
|
|
109
|
+
if (radii.some(r => !Number.isFinite(r) || r <= 0)) throw new Error('ellipsoid: radii must be positive');
|
|
110
|
+
const profile = Array.from({
|
|
111
|
+
length: 9
|
|
112
|
+
}, (_, i) => [i === 0 || i === 8 ? 0 : Math.sin(i * Math.PI / 8), Math.cos(i * Math.PI / 8)]);
|
|
113
|
+
return paint(merge([{
|
|
114
|
+
geometry: lathe(profile, {
|
|
115
|
+
segments: around
|
|
116
|
+
}),
|
|
117
|
+
scale: radii
|
|
118
|
+
}]), color);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Smooth Hermite interpolation through increasing [x,value] points; clamped beyond the ends. */
|
|
122
|
+
export function smoothCurve(points) {
|
|
123
|
+
if (points.length < 2 || points.some((p, i) => !p.every(Number.isFinite) || i > 0 && p[0] <= points[i - 1][0])) throw new Error('smoothCurve: needs increasing finite points');
|
|
124
|
+
const n = points.length;
|
|
125
|
+
const slope = i => {
|
|
126
|
+
const a = points[Math.max(0, i - 1)],
|
|
127
|
+
b = points[Math.min(n - 1, i + 1)];
|
|
128
|
+
return (b[1] - a[1]) / (b[0] - a[0]);
|
|
129
|
+
};
|
|
130
|
+
return x => {
|
|
131
|
+
if (x <= points[0][0]) return points[0][1];
|
|
132
|
+
if (x >= points[n - 1][0]) return points[n - 1][1];
|
|
133
|
+
let i = 0;
|
|
134
|
+
while (points[i + 1][0] < x) i++;
|
|
135
|
+
const [x0, y0] = points[i],
|
|
136
|
+
[x1, y1] = points[i + 1],
|
|
137
|
+
h = x1 - x0,
|
|
138
|
+
t = (x - x0) / h;
|
|
139
|
+
return (2 * t * t * t - 3 * t * t + 1) * y0 + (t * t * t - 2 * t * t + t) * h * slope(i) + (-2 * t * t * t + 3 * t * t) * y1 + (t * t * t - t * t) * h * slope(i + 1);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=loft.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import NativeEngine from '../native/NativeNayanEngine';
|
|
4
|
+
import { meshInfo, registerMesh } from "../render/meshes.js";
|
|
5
|
+
import { AssetLoader } from "./AssetLoader.js";
|
|
6
|
+
// External 3D models: glTF 2.0 (.glb, or .gltf with embedded buffers), parsed by the Rust core.
|
|
7
|
+
|
|
8
|
+
/** A loaded model: pass it as an entity's `mesh`. Colliders sized from the mesh fit its bounding box. */
|
|
9
|
+
|
|
10
|
+
/** An animation embedded in a loaded GLB. */
|
|
11
|
+
|
|
12
|
+
const loader = new AssetLoader('loadModel');
|
|
13
|
+
|
|
14
|
+
/** A registered mesh's geometry, aliasing native memory. */
|
|
15
|
+
export function nativeGeometry(mesh) {
|
|
16
|
+
return {
|
|
17
|
+
vertices: new Float32Array(NativeEngine.modelVertices(mesh)),
|
|
18
|
+
indices: new Uint32Array(NativeEngine.modelIndices(mesh))
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const materials = new Map();
|
|
22
|
+
|
|
23
|
+
/** Independent materials share core geometry without overwriting an existing model's appearance. */
|
|
24
|
+
export function registerModelMesh(mesh, shine) {
|
|
25
|
+
const info = meshInfo(mesh);
|
|
26
|
+
if (info && (info.shine ?? 0) === shine) return mesh;
|
|
27
|
+
const key = `${mesh}:${shine}`;
|
|
28
|
+
const cached = materials.get(key);
|
|
29
|
+
if (cached !== undefined) return cached;
|
|
30
|
+
const id = info ? NativeEngine.meshAlias(mesh) : mesh;
|
|
31
|
+
if (id < 0) throw new Error(NativeEngine.loadError() || "couldn't register a material variant");
|
|
32
|
+
registerMesh(id, {
|
|
33
|
+
geometry: info?.geometry ?? nativeGeometry(id),
|
|
34
|
+
texture: info?.texture,
|
|
35
|
+
shine
|
|
36
|
+
});
|
|
37
|
+
materials.set(key, id);
|
|
38
|
+
return id;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Initializes the engine automatically. Loads a glTF model once; pass it as an entity's `mesh`. Meshes, node transforms, material colors
|
|
43
|
+
* and the base color texture are kept (one texture per model); skeletal animation clips are available through `animations` and `World.playClip`.
|
|
44
|
+
* Every entity using the model is drawn in one instanced draw call.
|
|
45
|
+
*
|
|
46
|
+
* ```ts
|
|
47
|
+
* const tree = await loadModel(require("./assets/tree.glb"), { fit: 2 });
|
|
48
|
+
* world.spawn({ mesh: tree, position: [0, 1, 0], physics: "fixed" });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function loadModel(source, options = {}) {
|
|
52
|
+
const center = options.center ?? true;
|
|
53
|
+
const fit = options.fit ?? 0;
|
|
54
|
+
const shine = Math.max(0, Math.min(1, options.shine ?? 0));
|
|
55
|
+
return loader.load(source, (native, bytes) => {
|
|
56
|
+
const mesh = native.modelLoad(bytes, center, fit);
|
|
57
|
+
if (mesh < 0) throw new Error(native.loadError() || 'not a glTF model');
|
|
58
|
+
const texture = native.modelTexture(mesh);
|
|
59
|
+
registerMesh(mesh, {
|
|
60
|
+
geometry: nativeGeometry(mesh),
|
|
61
|
+
texture: texture >= 0 ? texture : undefined,
|
|
62
|
+
shine
|
|
63
|
+
});
|
|
64
|
+
const [x, y, z] = native.modelSize(mesh);
|
|
65
|
+
const rig = JSON.parse(native.modelRig(mesh));
|
|
66
|
+
return {
|
|
67
|
+
animations: rig.clips,
|
|
68
|
+
jointCount: rig.joints,
|
|
69
|
+
id: mesh,
|
|
70
|
+
size: [x, y, z]
|
|
71
|
+
};
|
|
72
|
+
}, [center, fit, shine]);
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=model.js.map
|