@nayan-ui/engine 1.0.0-beta.5 → 1.0.0-beta.6
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 +95 -4
- package/lib/module/assets/ProceduralCache.js +5 -0
- package/lib/module/assets/loft.js +15 -63
- package/lib/module/assets/procedural.js +500 -0
- package/lib/module/assets/shape.js +147 -245
- package/lib/module/assets/texture.js +65 -0
- package/lib/module/drawing/DrawingDocument.js +1 -1
- package/lib/module/index.js +4 -1
- package/lib/module/media/audio-worklet.js +27 -0
- package/lib/module/media/audio.js +20 -0
- package/lib/module/media/input.web.js +107 -0
- package/lib/module/media/microphone.js +89 -0
- package/lib/module/media/speech.js +80 -0
- package/lib/module/media/speechPlatform.js +12 -0
- package/lib/module/media/speechPlatform.web.js +16 -0
- package/lib/module/native/BrowserEngine.js +122 -0
- package/lib/module/native/NativeNayanEngine.web.js +2 -0
- package/lib/module/render/GameView.js +45 -13
- package/lib/module/render/quality.js +151 -17
- package/lib/module/world/World.js +63 -0
- package/lib/typescript/src/assets/ProceduralCache.d.ts +2 -0
- package/lib/typescript/src/assets/loft.d.ts +3 -1
- package/lib/typescript/src/assets/procedural.d.ts +394 -0
- package/lib/typescript/src/assets/shape.d.ts +13 -7
- package/lib/typescript/src/assets/texture.d.ts +63 -0
- package/lib/typescript/src/index.d.ts +8 -2
- package/lib/typescript/src/media/audio.d.ts +13 -0
- package/lib/typescript/src/media/input.web.d.ts +4 -0
- package/lib/typescript/src/media/microphone.d.ts +51 -0
- package/lib/typescript/src/media/speech.d.ts +38 -0
- package/lib/typescript/src/media/speechPlatform.d.ts +5 -0
- package/lib/typescript/src/media/speechPlatform.web.d.ts +5 -0
- package/lib/typescript/src/native/EngineBridge.d.ts +7 -1
- package/lib/typescript/src/native/spec/NativeNayanEngine.d.ts +72 -0
- package/lib/typescript/src/render/GameView.d.ts +30 -3
- package/lib/typescript/src/render/quality.d.ts +54 -2
- package/lib/typescript/src/types.d.ts +9 -2
- package/lib/typescript/src/world/World.d.ts +16 -0
- package/lib/typescript/src/world/types.d.ts +8 -0
- package/lib/web/audio-worklet.js +27 -0
- package/lib/web/engine_core_bg.wasm +0 -0
- package/lib/web/index.js +1181 -422
- package/lib/web/types/src/assets/ProceduralCache.d.ts +2 -0
- package/lib/web/types/src/assets/loft.d.ts +3 -1
- package/lib/web/types/src/assets/procedural.d.ts +393 -0
- package/lib/web/types/src/assets/shape.d.ts +13 -7
- package/lib/web/types/src/assets/texture.d.ts +63 -0
- package/lib/web/types/src/index.d.ts +8 -2
- package/lib/web/types/src/media/audio.d.ts +13 -0
- package/lib/web/types/src/media/input.d.ts +3 -0
- package/lib/web/types/src/media/microphone.d.ts +50 -0
- package/lib/web/types/src/media/speech.d.ts +37 -0
- package/lib/web/types/src/media/speechPlatform.d.ts +4 -0
- package/lib/web/types/src/native/EngineBridge.d.ts +7 -1
- package/lib/web/types/src/native/spec/NativeNayanEngine.d.ts +72 -0
- package/lib/web/types/src/render/GameView.d.ts +30 -3
- package/lib/web/types/src/render/quality.d.ts +54 -2
- package/lib/web/types/src/types.d.ts +9 -2
- package/lib/web/types/src/world/World.d.ts +16 -0
- package/lib/web/types/src/world/types.d.ts +8 -0
- package/native/android/src/main/AndroidManifest.xml +10 -0
- package/native/android/src/main/java/com/nayanui/engine/EngineMicrophone.java +60 -0
- package/native/android/src/main/java/com/nayanui/engine/EngineSpeech.java +28 -0
- package/native/android/src/main/java/com/nayanui/engine/EngineSurfaceView.java +1 -0
- package/native/android/src/main/java/com/nayanui/engine/NayanEnginePackage.java +2 -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/engine_core.h +63 -1
- package/native/cpp/NayanEngineModule.cpp +296 -0
- package/native/cpp/NayanEngineModule.h +40 -0
- package/native/cpp/generated/NayanEngineSpecJSI.h +397 -0
- package/package.json +1 -1
- package/src/native/spec/NativeNayanEngine.ts +77 -0
package/README.md
CHANGED
|
@@ -216,6 +216,46 @@ audio.enabled = settings.sounds; // respect the player's preferences
|
|
|
216
216
|
haptics.enabled = settings.haptics;
|
|
217
217
|
```
|
|
218
218
|
|
|
219
|
+
### System speech
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import { speech } from '@nayan-ui/engine';
|
|
223
|
+
|
|
224
|
+
await speech.speak('Well done!', { language: 'en-US', rate: 1, pitch: 1, volume: 1 });
|
|
225
|
+
const voices = await speech.getAvailableVoicesAsync();
|
|
226
|
+
await speech.speak('Your next turn', { voice: voices[0]?.identifier });
|
|
227
|
+
await speech.stop(); // Cancel current speech and clear the queue when leaving the screen.
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Speech runs through the Rust core, using iOS AVSpeechSynthesizer through Objective-C bindings,
|
|
231
|
+
Android TextToSpeech through JNI, and browser speechSynthesis through web-sys/WASM.
|
|
232
|
+
It creates no audio files, needs no microphone permission or Expo Speech dependency,
|
|
233
|
+
and plays independently of `audio` and its volume/mute settings. The web adapter automatically
|
|
234
|
+
awaits engine warm-up before sending commands into WASM. Rebuild native apps after upgrading
|
|
235
|
+
from an engine without speech support; `speech.supported` checks API availability.
|
|
236
|
+
|
|
237
|
+
`speak` adds text to the system queue and returns a promise that resolves when queued, **not when
|
|
238
|
+
playback ends**. Initialization, invalid voices, unsupported languages and Android's system text
|
|
239
|
+
length limit can reject that promise. On Android, initialization times out after 15 seconds and a
|
|
240
|
+
later request can retry. Stopping during initialization cancels pending speech immediately (those
|
|
241
|
+
`speak` promises reject); it does not wait for a system callback. Empty/whitespace text is ignored. Options default to the
|
|
242
|
+
system language/voice, rate 1, pitch 1 and volume 1. Rate accepts 0.1–2, pitch 0.5–2 and volume 0–1;
|
|
243
|
+
the platform may clamp the effective rate. Voices and pronunciation depend on the installed system
|
|
244
|
+
speech engine. `getAvailableVoicesAsync()` returns identifiers, names, BCP 47 languages and quality.
|
|
245
|
+
Android lists installed voices that do not require a network connection.
|
|
246
|
+
|
|
247
|
+
`isSpeakingAsync()` reports active speech (including paused speech on iOS/web). `pause()` and
|
|
248
|
+
`resume()` work on iOS/web and reject on Android, whose system API has no pause/resume support.
|
|
249
|
+
`stop()` resets paused browser state and ends the iOS synthesizer session so the next run starts cleanly.
|
|
250
|
+
Browsers may require a user gesture before speaking, and voice discovery can initially be empty.
|
|
251
|
+
As with [Expo Speech](https://docs.expo.dev/versions/latest/sdk/speech/), actual output depends on
|
|
252
|
+
device settings such as silent mode. This API implements the options and methods above, not Expo's
|
|
253
|
+
utterance event callbacks. System playback errors after an utterance is queued are not reported.
|
|
254
|
+
|
|
255
|
+
For a muted iOS integration check, rebuild the core, boot an Apple Silicon simulator and run
|
|
256
|
+
`bun test packages/engine/native/tests/speech-ios.test.ts` from the repository root. This checks the
|
|
257
|
+
real Rust/native bridge, including stopping before playback begins and speaking again afterwards.
|
|
258
|
+
|
|
219
259
|
### Score and timers
|
|
220
260
|
|
|
221
261
|
```ts
|
|
@@ -236,12 +276,61 @@ Build a mesh once at module scope (after `warmUpEngine()`), then spawn it many t
|
|
|
236
276
|
```ts
|
|
237
277
|
import { createShape, lathe } from '@nayan-ui/engine';
|
|
238
278
|
|
|
239
|
-
const vase = createShape(
|
|
279
|
+
const vase = createShape(
|
|
280
|
+
lathe(
|
|
281
|
+
[
|
|
282
|
+
[0, 0],
|
|
283
|
+
[0.4, 0.1],
|
|
284
|
+
[0.25, 0.8],
|
|
285
|
+
[0.3, 1]
|
|
286
|
+
],
|
|
287
|
+
{ segments: 24 }
|
|
288
|
+
)
|
|
289
|
+
);
|
|
240
290
|
world.spawn({ mesh: vase, color: '#C8763A' });
|
|
241
291
|
```
|
|
242
292
|
|
|
243
293
|
Also available: `merge`, `tube`, `extrude`, `loft`, `block`, `ellipsoid`, `parametricSurface`, `Solid` and `shapeGeometry`.
|
|
244
294
|
|
|
295
|
+
### Procedural worlds
|
|
296
|
+
|
|
297
|
+
Describe terrain and placement rules as data; the native core generates the world on a background
|
|
298
|
+
thread and answers walker queries, so none of that work runs in JavaScript.
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
import { FieldGraph, generateWorldAsync } from '@nayan-ui/engine';
|
|
302
|
+
|
|
303
|
+
const f = new FieldGraph();
|
|
304
|
+
const bank = f.sub(f.abs(f.sub(f.x, f.sines(f.x, f.z, 0, [[0, 0.04, 0, 12]]))), 4); // a winding river
|
|
305
|
+
f.output('bank', bank);
|
|
306
|
+
f.output('height', f.select(bank, -1, f.bumps(f.x, f.z, [[30, 20, 5, 15, 12, 0]])));
|
|
307
|
+
f.output('clear', f.sub(bank, 1));
|
|
308
|
+
|
|
309
|
+
const world = await generateWorldAsync({
|
|
310
|
+
seed: 7,
|
|
311
|
+
fields: f,
|
|
312
|
+
terrain: {
|
|
313
|
+
axis: [...Array(101).keys()].map((i) => i * 2 - 100),
|
|
314
|
+
height: 'height',
|
|
315
|
+
regions: [{ field: 'bank', surface: 2, material: 1 }],
|
|
316
|
+
fallback: { surface: 1, material: 0 },
|
|
317
|
+
materials: 2
|
|
318
|
+
},
|
|
319
|
+
walk: { boundary: 95, clearance: 'clear', height: 2 },
|
|
320
|
+
steps: [
|
|
321
|
+
{ kind: 'spawn', near: [-30, 0], rings: 20, samples: 16, radius: 1 },
|
|
322
|
+
{ kind: 'reach', cell: 3, steps: 6, radius: 0.6, extent: 100 },
|
|
323
|
+
{ kind: 'pick', groups: [{ name: 'coins', count: 10, spacing: 8 }] }
|
|
324
|
+
]
|
|
325
|
+
});
|
|
326
|
+
const land = createShape(world.terrain(0), { center: false }); // one mesh per material
|
|
327
|
+
world.move(player, dx, dz, 0.5); // swept move-and-slide; also ground, walkable, canSee, tilt
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Scatter steps place scenery with filters (fields, walkability, surfaces) and optional solids;
|
|
331
|
+
`generateMesh` builds parametric surfaces and seeded cards. Pass `key` to save a world (tied to its
|
|
332
|
+
exact recipe) and restore it on later visits.
|
|
333
|
+
|
|
245
334
|
### Flat boards and puzzles
|
|
246
335
|
|
|
247
336
|
`frontLight` keeps camera-facing surfaces at their exact color.
|
|
@@ -276,7 +365,9 @@ The engine is built so that a game written the obvious way stays smooth on mid-r
|
|
|
276
365
|
|
|
277
366
|
- **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.
|
|
278
367
|
- **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.
|
|
279
|
-
- **Automatic quality.** `quality="auto"` (the default) starts at `high` and steps down (
|
|
368
|
+
- **Automatic quality.** `quality="auto"` (the default) starts at `high` and steps down (shadows off first, then anti-aliasing, resolution last) only while the device can't keep up. Shadows always come from a full-resolution map (soft at `high`, crisp at `medium`): `low` and `lowest` skip the shadow pass rather than show blurred low-resolution shadows. `lowest` smooths edges with FXAA (in the pass that stretches its reduced scene), and `lowest`'s pixel cap shrinks only the 3D scene (stretched to fill the view): the overlay, and so all HUD text, is always drawn at the screen's own resolution. With a `qualityKey` (games pass their id) the level it settles on is saved in the engine's durable store, so later visits, after app restarts too, start there. Pass `adaptQuality={false}` while what's on screen isn't the game being played (a menu, a results card): those frames then never change or save the level. `onStats` reports fps, update and render time, the quality chosen and the graphics backend.
|
|
369
|
+
- **Cached backdrops.** Spawn scenery that never moves (a sky, distant hills, a board's background) with `still: true`, or a whole still scene into `world.backdropLayer(capacity)` (then `update(0)` it). `GameView` draws the world's backdrop layer once into an image behind the scene and draws that image each frame instead, redrawing it only after a still entity changes, the camera moves or the light changes. Live entities are always drawn in front of it and it receives no live shadows, so keep anything that moves, or that live pieces pass behind, in the world. A still entity takes `set` and `despawn` but can't be attached, animated, picked or given physics or text.
|
|
370
|
+
- **Small things stay cheap.** Built-in spheres a few pixels across on screen (stars, pellets, beads) are drawn with 8- or 12-sided stand-ins, and instances scaled to nothing are never drawn, so pools of hidden pieces cost nothing on the GPU.
|
|
280
371
|
- **Background loading.** `loadTexture`, `loadModel`, `loadFont` and `audio.load` decode on native threads in parallel, and meshes are registered with the renderer inside Rust.
|
|
281
372
|
- **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.
|
|
282
373
|
- **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.
|
|
@@ -307,8 +398,8 @@ Keep these habits:
|
|
|
307
398
|
|
|
308
399
|
## Learn more
|
|
309
400
|
|
|
310
|
-
| Topic | Guide
|
|
311
|
-
| ------------------------------------- |
|
|
401
|
+
| Topic | Guide |
|
|
402
|
+
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
312
403
|
| Build a complete game step by step | [First game](https://www.nayanui.com/game-engine/first-game) |
|
|
313
404
|
| Copy-paste solutions | [Recipes](https://www.nayanui.com/game-engine/recipes) |
|
|
314
405
|
| Worlds, entities, rotations, disposal | [World & entities](https://www.nayanui.com/game-engine/world-entities) |
|
|
@@ -43,6 +43,11 @@ export class ProceduralCache {
|
|
|
43
43
|
this.revision = revision;
|
|
44
44
|
this.storage = storage;
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/** The engine storage key of `key` in this revision (for records the native core reads and writes itself). */
|
|
48
|
+
storageKey(key) {
|
|
49
|
+
return `procedural:${this.revision}:${key}`;
|
|
50
|
+
}
|
|
46
51
|
get stats() {
|
|
47
52
|
return {
|
|
48
53
|
hits: this.hits,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { FieldFunction } from "./procedural.js";
|
|
4
|
+
import { lathe, merge, nativeSweep, recolor } from "./shape.js";
|
|
5
5
|
/**
|
|
6
6
|
* Smooth closed body through rounded rectangular cross sections along Z. Useful for vehicle
|
|
7
7
|
* shells, cabins, shoes and fairings. Caps have separate flat normals and planar UVs; sides have
|
|
@@ -15,72 +15,24 @@ export function loft(sections, options = {}) {
|
|
|
15
15
|
steps = options.smoothness ?? 4;
|
|
16
16
|
if (!Number.isInteger(n) || n < 6 || n > 512 || !Number.isInteger(steps) || steps < 1 || steps > 64) throw new Error('loft: invalid around/smoothness');
|
|
17
17
|
const values = ordered.map(s => [s.z, s.bottom, s.top, s.width, s.topWidth ?? s.width, s.square ?? 4]);
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
18
|
+
const color = options.color;
|
|
19
|
+
const g = nativeSweep({
|
|
20
|
+
kind: 'loft',
|
|
21
|
+
sections: values,
|
|
22
|
+
around: n,
|
|
23
|
+
smoothness: steps,
|
|
24
|
+
color: typeof color === 'function' || color instanceof FieldFunction || !color ? undefined : [color[0], color[1], color[2]],
|
|
25
|
+
colorField: color instanceof FieldFunction ? color : undefined
|
|
52
26
|
});
|
|
53
|
-
|
|
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);
|
|
27
|
+
return typeof color === 'function' ? paint(g, color) : g;
|
|
78
28
|
}
|
|
79
29
|
function paint(geometry, color) {
|
|
80
|
-
|
|
30
|
+
if (!color) return geometry;
|
|
31
|
+
if (color instanceof FieldFunction) return recolor(geometry, color);
|
|
32
|
+
return recolor(geometry, typeof color === 'function' ? ({
|
|
81
33
|
position,
|
|
82
34
|
normal
|
|
83
|
-
}) => color(position, normal) : () => color)
|
|
35
|
+
}) => color(position, normal) : () => color);
|
|
84
36
|
}
|
|
85
37
|
|
|
86
38
|
/** A rounded rectangular cross-section with flat front/back caps, centered at the origin. */
|