@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.
Files changed (77) hide show
  1. package/README.md +95 -4
  2. package/lib/module/assets/ProceduralCache.js +5 -0
  3. package/lib/module/assets/loft.js +15 -63
  4. package/lib/module/assets/procedural.js +500 -0
  5. package/lib/module/assets/shape.js +147 -245
  6. package/lib/module/assets/texture.js +65 -0
  7. package/lib/module/drawing/DrawingDocument.js +1 -1
  8. package/lib/module/index.js +4 -1
  9. package/lib/module/media/audio-worklet.js +27 -0
  10. package/lib/module/media/audio.js +20 -0
  11. package/lib/module/media/input.web.js +107 -0
  12. package/lib/module/media/microphone.js +89 -0
  13. package/lib/module/media/speech.js +80 -0
  14. package/lib/module/media/speechPlatform.js +12 -0
  15. package/lib/module/media/speechPlatform.web.js +16 -0
  16. package/lib/module/native/BrowserEngine.js +122 -0
  17. package/lib/module/native/NativeNayanEngine.web.js +2 -0
  18. package/lib/module/render/GameView.js +45 -13
  19. package/lib/module/render/quality.js +151 -17
  20. package/lib/module/world/World.js +63 -0
  21. package/lib/typescript/src/assets/ProceduralCache.d.ts +2 -0
  22. package/lib/typescript/src/assets/loft.d.ts +3 -1
  23. package/lib/typescript/src/assets/procedural.d.ts +394 -0
  24. package/lib/typescript/src/assets/shape.d.ts +13 -7
  25. package/lib/typescript/src/assets/texture.d.ts +63 -0
  26. package/lib/typescript/src/index.d.ts +8 -2
  27. package/lib/typescript/src/media/audio.d.ts +13 -0
  28. package/lib/typescript/src/media/input.web.d.ts +4 -0
  29. package/lib/typescript/src/media/microphone.d.ts +51 -0
  30. package/lib/typescript/src/media/speech.d.ts +38 -0
  31. package/lib/typescript/src/media/speechPlatform.d.ts +5 -0
  32. package/lib/typescript/src/media/speechPlatform.web.d.ts +5 -0
  33. package/lib/typescript/src/native/EngineBridge.d.ts +7 -1
  34. package/lib/typescript/src/native/spec/NativeNayanEngine.d.ts +72 -0
  35. package/lib/typescript/src/render/GameView.d.ts +30 -3
  36. package/lib/typescript/src/render/quality.d.ts +54 -2
  37. package/lib/typescript/src/types.d.ts +9 -2
  38. package/lib/typescript/src/world/World.d.ts +16 -0
  39. package/lib/typescript/src/world/types.d.ts +8 -0
  40. package/lib/web/audio-worklet.js +27 -0
  41. package/lib/web/engine_core_bg.wasm +0 -0
  42. package/lib/web/index.js +1181 -422
  43. package/lib/web/types/src/assets/ProceduralCache.d.ts +2 -0
  44. package/lib/web/types/src/assets/loft.d.ts +3 -1
  45. package/lib/web/types/src/assets/procedural.d.ts +393 -0
  46. package/lib/web/types/src/assets/shape.d.ts +13 -7
  47. package/lib/web/types/src/assets/texture.d.ts +63 -0
  48. package/lib/web/types/src/index.d.ts +8 -2
  49. package/lib/web/types/src/media/audio.d.ts +13 -0
  50. package/lib/web/types/src/media/input.d.ts +3 -0
  51. package/lib/web/types/src/media/microphone.d.ts +50 -0
  52. package/lib/web/types/src/media/speech.d.ts +37 -0
  53. package/lib/web/types/src/media/speechPlatform.d.ts +4 -0
  54. package/lib/web/types/src/native/EngineBridge.d.ts +7 -1
  55. package/lib/web/types/src/native/spec/NativeNayanEngine.d.ts +72 -0
  56. package/lib/web/types/src/render/GameView.d.ts +30 -3
  57. package/lib/web/types/src/render/quality.d.ts +54 -2
  58. package/lib/web/types/src/types.d.ts +9 -2
  59. package/lib/web/types/src/world/World.d.ts +16 -0
  60. package/lib/web/types/src/world/types.d.ts +8 -0
  61. package/native/android/src/main/AndroidManifest.xml +10 -0
  62. package/native/android/src/main/java/com/nayanui/engine/EngineMicrophone.java +60 -0
  63. package/native/android/src/main/java/com/nayanui/engine/EngineSpeech.java +28 -0
  64. package/native/android/src/main/java/com/nayanui/engine/EngineSurfaceView.java +1 -0
  65. package/native/android/src/main/java/com/nayanui/engine/NayanEnginePackage.java +2 -0
  66. package/native/core/build/EngineCore.xcframework/ios-arm64/libengine_core.a +0 -0
  67. package/native/core/build/EngineCore.xcframework/ios-arm64-simulator/libengine_core.a +0 -0
  68. package/native/core/build/android/arm64-v8a/libengine_core.a +0 -0
  69. package/native/core/build/android/armeabi-v7a/libengine_core.a +0 -0
  70. package/native/core/build/android/x86/libengine_core.a +0 -0
  71. package/native/core/build/android/x86_64/libengine_core.a +0 -0
  72. package/native/core/include/engine_core.h +63 -1
  73. package/native/cpp/NayanEngineModule.cpp +296 -0
  74. package/native/cpp/NayanEngineModule.h +40 -0
  75. package/native/cpp/generated/NayanEngineSpecJSI.h +397 -0
  76. package/package.json +1 -1
  77. 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(lathe([[0, 0], [0.4, 0.1], [0.25, 0.8], [0.3, 1]], { segments: 24 }));
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 (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.
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 { lathe, merge, recolor } from "./shape.js";
4
- import { parametricSurface } from "./surface.js";
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 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
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
- 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);
27
+ return typeof color === 'function' ? paint(g, color) : g;
78
28
  }
79
29
  function paint(geometry, color) {
80
- return color ? recolor(geometry, typeof color === 'function' ? ({
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) : geometry;
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. */