@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
@@ -3,9 +3,10 @@
3
3
  // Custom shapes: 2D vector paths, and solids built from them (extrusions, lathes, tubes, merged
4
4
  // parts) that draw, collide and pick like loaded models.
5
5
  import NativeEngine from '../native/NativeNayanEngine';
6
- import { Builder, meshInfo, registerMesh, VERTEX_FLOATS } from "../render/meshes.js";
6
+ import { meshInfo, registerMesh, VERTEX_FLOATS } from "../render/meshes.js";
7
7
  import { SHAPE_MESH } from "../types.js";
8
8
  import { nativeGeometry } from "./model.js";
9
+ import { FieldFunction, paintGeometry } from "./procedural.js";
9
10
  const TAU = Math.PI * 2;
10
11
  /** Line segments per curve, arc or circle quarter when a path is flattened. */
11
12
  const CURVE_STEPS = 12;
@@ -369,10 +370,6 @@ function paint(g, color) {
369
370
  }
370
371
  return g;
371
372
  }
372
- const fromBuilder = b => ({
373
- vertices: new Float32Array(b.vertices),
374
- indices: new Uint32Array(b.indices)
375
- });
376
373
 
377
374
  /** A built-in shape's geometry (unit-sized, centered), to `merge` with others. */
378
375
  export function shapeGeometry(shape, options = {}) {
@@ -411,12 +408,19 @@ export function extrude(path, options = {}) {
411
408
 
412
409
  /** Where a color function is asked about: `u` around (0..1), `v` along (0..1), and the surface there. */
413
410
 
414
- /** A flat color, or one per point of the surface: blushes, stripes, speckles, spots. */
411
+ /**
412
+ * A flat color, or one per point of the surface: blushes, stripes, speckles, spots. Prefer a
413
+ * `surfaceColor` field function (evaluated by the native core) to a JS function for detailed models.
414
+ */
415
415
 
416
- /** A flat color, or one per point of a round face: `r` from its center (0) to its rim (1), `angle` around it. */
416
+ /** A flat color, or one per point of a round face (`faceColor`): `r` from its center (0) to its rim (1), `angle` around it. */
417
417
 
418
+ /** A per-point option given as a JS function or a field function. */
419
+ const perPoint = x => typeof x === 'function' || x instanceof FieldFunction;
420
+ const asField = x => x instanceof FieldFunction ? x : undefined;
418
421
  /** Colors each vertex by `fn` of its surface point (u, v read from the texture coordinates). */
419
422
  function paintBy(g, color) {
423
+ if (color instanceof FieldFunction) return paintGeometry(g, color);
420
424
  if (typeof color !== 'function') return paint(g, color);
421
425
  const v = g.vertices;
422
426
  for (let i = 0; i < v.length; i += VERTEX_FLOATS) {
@@ -441,34 +445,54 @@ export function recolor(geometry, color) {
441
445
  return paintBy(geometry, color);
442
446
  }
443
447
 
448
+ /** A flat color for the native builder (vertex colors are RGB). */
449
+ const flat = color => color ? [color[0], color[1], color[2]] : undefined;
450
+
451
+ /** Builds a sweep (lathe, tube or loft) part in the native core. */
452
+ export function nativeSweep(part) {
453
+ const core = native();
454
+ if (typeof core.worldgenMesh !== 'function') throw new Error("shapes: this app's native core predates native lathes and tubes: rebuild it");
455
+ const built = core.worldgenMesh(JSON.stringify({
456
+ parts: [part]
457
+ }));
458
+ return {
459
+ vertices: new Float32Array(built.vertices),
460
+ indices: new Uint32Array(built.indices)
461
+ };
462
+ }
463
+
464
+ /** Colors vertices [from, to) by `color` of each surface point (u, v from the texture coordinates). */
465
+ function paintRange(g, from, to, color) {
466
+ const v = g.vertices;
467
+ for (let i = from * VERTEX_FLOATS; i < to * VERTEX_FLOATS; i += VERTEX_FLOATS) {
468
+ const c = color({
469
+ u: v[i + 9],
470
+ v: v[i + 10],
471
+ position: [v[i], v[i + 1], v[i + 2]],
472
+ normal: [v[i + 3], v[i + 4], v[i + 5]]
473
+ });
474
+ v[i + 6] = c[0];
475
+ v[i + 7] = c[1];
476
+ v[i + 8] = c[2];
477
+ }
478
+ }
479
+
444
480
  /**
445
- * A round face: a polar grid of `rings` x `segments` around `center` in the plane spanned by unit
446
- * vectors `x` and `y` (radius `radius(angle)` toward the rim), facing `normal`, swept `sweep` radians.
481
+ * Colors a round end face built with raw polar coordinates in its UVs (around 0..1, out 0..1) by
482
+ * `color(r, angle)`, then gives it planar disc UVs.
447
483
  */
448
- function roundFace(center, x, y, normal, radius, sweep, segments, rings, color) {
449
- const b = new Builder();
450
- b.grid(rings, segments, (u, v) => {
451
- const a = u * sweep;
452
- const r = radius(u) * v;
453
- const [c, s] = [Math.cos(a) * r, Math.sin(a) * r];
454
- return [[center[0] + x[0] * c + y[0] * s, center[1] + x[1] * c + y[1] * s, center[2] + x[2] * c + y[2] * s], normal];
455
- });
456
- const g = facingOutward(fromBuilder(b));
484
+ function paintFace(g, from, to, sweep, color) {
457
485
  const vs = g.vertices;
458
- for (let i = 0; i < vs.length; i += VERTEX_FLOATS) {
486
+ for (let i = from * VERTEX_FLOATS; i < to * VERTEX_FLOATS; i += VERTEX_FLOATS) {
459
487
  const a = vs[i + 9] * sweep;
460
488
  const r = vs[i + 10];
461
- // Texture coordinates across the face, like a disc's.
462
489
  vs[i + 9] = 0.5 + Math.cos(a) * r / 2;
463
490
  vs[i + 10] = 0.5 + Math.sin(a) * r / 2;
464
- if (typeof color === 'function') {
465
- const c = color(r, a);
466
- vs[i + 6] = c[0];
467
- vs[i + 7] = c[1];
468
- vs[i + 8] = c[2];
469
- }
491
+ const c = color(r, a);
492
+ vs[i + 6] = c[0];
493
+ vs[i + 7] = c[1];
494
+ vs[i + 8] = c[2];
470
495
  }
471
- return typeof color === 'function' ? g : paint(g, color);
472
496
  }
473
497
 
474
498
  /** Whether a typed array covers its whole ArrayBuffer (so the native passes can take the buffer). */
@@ -544,71 +568,53 @@ export function lathe(profile, options = {}) {
544
568
  const full = sweep >= TAU - 1e-6;
545
569
  const last = points.length - 1;
546
570
 
547
- // Profile normals in the (radius, height) plane: the average of the neighbouring edges', turned outward.
548
- const edge = i => {
549
- const a = points[Math.max(0, i)];
550
- const b = points[Math.min(last, i + 1)];
551
- const len = Math.hypot(b[0] - a[0], b[1] - a[1]) || 1;
552
- return [(a[1] - b[1]) / len, (b[0] - a[0]) / len]; // going down the profile, outward is to the left
553
- };
554
- const normals = points.map(([r], i) => {
555
- // On the axis the surface is a pole: straight up at the top, down at the bottom.
556
- if (r < 1e-6 && (i === 0 || i === last)) return [0, i === 0 ? 1 : -1];
557
- const e1 = edge(i - 1);
558
- const e2 = edge(i);
559
- const n = i === 0 ? e2 : i === last ? e1 : [e1[0] + e2[0], e1[1] + e2[1]];
560
- const len = Math.hypot(n[0], n[1]) || 1;
561
- return [n[0] / len, n[1] / len];
562
- });
563
- const b = new Builder();
571
+ // Only the game's own JS functions run here (field functions run in the core with the surface).
564
572
  const displace = options.displace;
565
- // a = u * sweep around the axis; the same orientation as the built-in sphere, so faces point outward.
566
- b.grid(last, segments, (u, v) => {
567
- const i = Math.round(v * last);
568
- const a = u * sweep;
569
- const [r, y] = points[i];
570
- const [nr, ny] = normals[i];
571
- const n = [nr * Math.cos(a), ny, -nr * Math.sin(a)];
572
- // The seam and the poles must move together, or the surface tears.
573
- const d = displace ? displace(r < 1e-6 || full && u === 1 ? 0 : u, v) : 0;
574
- return [[r * Math.cos(a) + n[0] * d, y + n[1] * d, -r * Math.sin(a) + n[2] * d], n];
575
- });
576
- const parts = [{
577
- geometry: paintBy(displace ? smoothNormals(fromBuilder(b)) : fromBuilder(b), options.color)
578
- }];
579
-
580
- // Ends away from the axis: flat round faces (part of a circle when the sweep is partial).
581
- const capColor = options.capColor ?? (typeof options.color === 'function' ? undefined : options.color);
582
- const rings = Math.max(1, Math.round(options.capRings ?? (typeof capColor === 'function' ? 12 : 1)));
583
- for (const [i, facing] of [[0, 1], [last, -1]]) {
584
- const [r, y] = points[i];
585
- if (r < 1e-6) continue;
586
- parts.push({
587
- geometry: roundFace([0, y, 0], [1, 0, 0], [0, 0, -1], [0, facing, 0], () => r, sweep, segments, rings, capColor)
588
- });
573
+ let offsets;
574
+ if (typeof displace === 'function') {
575
+ offsets = new Array((last + 1) * (segments + 1));
576
+ for (let i = 0; i <= last; i++) {
577
+ const pole = points[i][0] < 1e-6;
578
+ for (let j = 0; j <= segments; j++) {
579
+ const u = j / segments;
580
+ // The seam and the poles must move together, or the surface tears.
581
+ offsets[i * (segments + 1) + j] = displace(pole || full && u === 1 ? 0 : u, i / last);
582
+ }
583
+ }
589
584
  }
590
- if (full) return parts.length > 1 ? merge(parts) : parts[0].geometry;
591
-
592
- // The cut: the region between the profile and the axis, laid into the planes at a = 0 and a = sweep.
593
- // A profile whose height only falls gets a grid (so a color function has points to work with);
594
- // one that turns back (a dimple) is filled exactly by the core.
595
- const cutColor = options.cutColor ?? (typeof options.color === 'function' ? undefined : options.color);
596
- const falling = points.every((p, i) => i === 0 || p[1] <= points[i - 1][1] + 1e-9);
597
- let face;
598
- if (falling) {
599
- const g = new Builder();
600
- const steps = Math.max(1, Math.round(options.cutSteps ?? (typeof cutColor === 'function' ? 12 : 1)));
601
- g.grid(last, steps, (u, v) => {
602
- const [r, y] = points[Math.round(v * last)];
603
- return [[u * r, y, 0], [0, 0, 1]];
604
- });
605
- face = facingOutward(fromBuilder(g));
606
- } else {
607
- face = extrude(new Path().addPolygon([[0, -points[0][1]], ...points.map(([r, y]) => [r, -y]), [0, -points[last][1]]]), {
608
- depth: 0
609
- });
585
+ const color = options.color;
586
+ const capColor = options.capColor ?? (perPoint(color) ? undefined : color);
587
+ const capRings = Math.max(1, Math.round(options.capRings ?? (perPoint(capColor) ? 12 : 1)));
588
+ const cutColor = options.cutColor ?? (perPoint(color) ? undefined : color);
589
+ const cutSteps = Math.max(1, Math.round(options.cutSteps ?? (perPoint(cutColor) ? 12 : 1)));
590
+ const g = nativeSweep({
591
+ kind: 'lathe',
592
+ points,
593
+ segments,
594
+ sweep,
595
+ displace: offsets,
596
+ displaceField: asField(displace),
597
+ color: perPoint(color) ? undefined : flat(color),
598
+ colorField: asField(color),
599
+ capColor: perPoint(capColor) ? undefined : flat(capColor),
600
+ capColorField: asField(capColor),
601
+ rawCaps: typeof capColor === 'function',
602
+ capRings,
603
+ cutColor: perPoint(cutColor) ? undefined : flat(cutColor),
604
+ cutColorField: asField(cutColor),
605
+ cutSteps
606
+ });
607
+
608
+ // Vertices: the side grid, the caps away from the axis (top, bottom), then the two cut faces.
609
+ let at = (last + 1) * (segments + 1);
610
+ if (typeof color === 'function') paintRange(g, 0, at, color);
611
+ for (const i of [0, last]) {
612
+ if (points[i][0] < 1e-6) continue;
613
+ const end = at + (capRings + 1) * (segments + 1);
614
+ if (typeof capColor === 'function') paintFace(g, at, end, sweep, capColor);
615
+ at = end;
610
616
  }
611
- if (typeof cutColor === 'function') {
617
+ if (!full && typeof cutColor === 'function') {
612
618
  const widest = Math.max(...points.map(p => p[0]), 1e-6);
613
619
  const skinAt = y => {
614
620
  // The profile's radius at height y (for r from the axis to the skin).
@@ -620,35 +626,20 @@ export function lathe(profile, options = {}) {
620
626
  }
621
627
  return best || widest;
622
628
  };
623
- const fv = face.vertices;
624
- for (let i = 0; i < fv.length; i += VERTEX_FLOATS) {
625
- const c = cutColor(Math.min(1, fv[i] / skinAt(fv[i + 1])), fv[i + 1]);
626
- fv.set(c.slice(0, 3), i + 6);
629
+ // Both cut faces hold the same points in the same order; the first lies in the a = 0 plane (x = r).
630
+ const v = g.vertices;
631
+ const half = (v.length / VERTEX_FLOATS - at) / 2;
632
+ for (let k = 0; k < half; k++) {
633
+ const i = (at + k) * VERTEX_FLOATS;
634
+ const c = cutColor(Math.min(1, v[i] / skinAt(v[i + 1])), v[i + 1]);
635
+ for (const o of [i, i + half * VERTEX_FLOATS]) {
636
+ v[o + 6] = c[0];
637
+ v[o + 7] = c[1];
638
+ v[o + 8] = c[2];
639
+ }
627
640
  }
628
- } else {
629
- paint(face, cutColor);
630
641
  }
631
- const cut = (angle, flip) => {
632
- const g = {
633
- vertices: face.vertices.slice(),
634
- indices: face.indices.slice()
635
- };
636
- const [c, s] = [Math.cos(angle), Math.sin(angle)];
637
- const n = angle === 0 ? [0, 0, 1] : [-s, 0, -c];
638
- for (let v = 0; v < g.vertices.length; v += VERTEX_FLOATS) {
639
- const r = g.vertices[v];
640
- g.vertices[v] = r * c;
641
- g.vertices[v + 2] = -r * s;
642
- g.vertices.set(n, v + 3);
643
- }
644
- if (flip) for (let t = 0; t < g.indices.length; t += 3) [g.indices[t + 1], g.indices[t + 2]] = [g.indices[t + 2], g.indices[t + 1]];
645
- return g;
646
- };
647
- return merge([...parts, {
648
- geometry: cut(0, false)
649
- }, {
650
- geometry: cut(sweep, true)
651
- }]);
642
+ return g;
652
643
  }
653
644
  /**
654
645
  * A tube along a smooth curve through `points`: bananas, stems, chillies, worms, snakes, ropes,
@@ -662,140 +653,51 @@ export function tube(points, options = {}) {
662
653
  if (closed && points.length > 1 && distance(points[0], points[points.length - 1]) < 1e-9) points = points.slice(0, -1);
663
654
  if (points.length < (closed ? 3 : 2)) throw new Error(`tube: needs at least ${closed ? 'three' : 'two'} distinct points`);
664
655
  const radius = typeof options.radius === 'function' ? options.radius : () => options.radius ?? 0.1;
665
- const shape = options.shape ?? (() => 1);
656
+ const shape = options.shape;
666
657
  const sides = Math.max(3, Math.round(options.sides ?? 12));
667
658
  const steps = Math.max(1, Math.round(options.smoothness ?? 8));
668
659
 
669
- // Catmull-Rom through the points.
670
- const p = i => points[closed ? (i + points.length) % points.length : Math.max(0, Math.min(points.length - 1, i))];
671
- const curve = [];
672
- for (let i = 0; i < points.length - (closed ? 0 : 1); i++) {
673
- const [a, b, c, d] = [p(i - 1), p(i), p(i + 1), p(i + 2)];
674
- for (let s = 0; s < steps; s++) {
675
- const t = s / steps;
676
- const t2 = t * t;
677
- const t3 = t2 * t;
678
- const coordinate = k => 0.5 * (2 * b[k] + (-a[k] + c[k]) * t + (2 * a[k] - 5 * b[k] + 4 * c[k] - d[k]) * t2 + (-a[k] + 3 * b[k] - 3 * c[k] + d[k]) * t3);
679
- curve.push([coordinate(0), coordinate(1), coordinate(2)]);
680
- }
681
- }
682
- curve.push(p(closed ? 0 : points.length - 1));
683
- const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
684
- const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
685
- const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
686
- const norm = a => {
687
- const l = Math.hypot(a[0], a[1], a[2]) || 1;
688
- return [a[0] / l, a[1] / l, a[2] / l];
689
- };
690
- const last = curve.length - 1;
691
- const tangents = curve.map((_, i) => {
692
- let delta = sub(curve[closed && i === last ? 1 : Math.min(last, i + 1)], curve[closed && i === 0 ? last - 1 : Math.max(0, i - 1)]);
693
- // A reversing curve has no central derivative. Use its incoming/outgoing segment instead.
694
- if (Math.hypot(...delta) < 1e-9) delta = i > 0 ? sub(curve[i], curve[i - 1]) : sub(curve[1], curve[0]);
695
- return norm(delta);
660
+ // Only the game's own functions run here (a radius per curve point, a multiplier per side); the
661
+ // core smooths the curve, frames it and builds the surface.
662
+ const count = (points.length - (closed ? 0 : 1)) * steps + 1;
663
+ const last = count - 1;
664
+ const radii = Array.from({
665
+ length: count
666
+ }, (_, i) => Math.max(0, radius(closed && i === last ? 0 : i / last)));
667
+ const color = options.color;
668
+ const caps = !closed && (options.caps ?? true);
669
+ const capColor = options.capColor ?? (perPoint(color) ? undefined : color);
670
+ const capRings = perPoint(capColor) ? 8 : 1;
671
+ const g = nativeSweep({
672
+ kind: 'tube',
673
+ points,
674
+ closed,
675
+ sides,
676
+ steps,
677
+ radii,
678
+ shape: shape && Array.from({
679
+ length: sides + 1
680
+ }, (_, j) => shape(j === sides ? 0 : j / sides)),
681
+ // Taper changes the longitudinal component too; radial normals only describe a cylinder.
682
+ smooth: !!shape || typeof options.radius === 'function',
683
+ caps,
684
+ color: perPoint(color) ? undefined : flat(color),
685
+ colorField: asField(color),
686
+ capColor: perPoint(capColor) ? undefined : flat(capColor),
687
+ capColorField: asField(capColor),
688
+ rawCaps: typeof capColor === 'function',
689
+ capRings
696
690
  });
697
- if (closed) tangents[last] = tangents[0];
698
- // Minimal rotations between tangents: projecting the previous normal onto the next plane
699
- // collapses at right-angle bends and adds unwanted twist to curls.
700
- const t0 = tangents[0];
701
- let normal = norm(cross(t0, Math.abs(t0[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0]));
702
- let previous = t0;
703
- const frames = [];
704
- for (const t of tangents) {
705
- const axis = cross(previous, t);
706
- const cosine = Math.max(-1, Math.min(1, dot(previous, t)));
707
- if (cosine > -1 + 1e-8) {
708
- const turn = cross(axis, normal);
709
- const second = cross(axis, turn);
710
- normal = [normal[0] + turn[0] + second[0] / (1 + cosine), normal[1] + turn[1] + second[1] / (1 + cosine), normal[2] + turn[2] + second[2] / (1 + cosine)];
711
- }
712
- // At a reversal, a half turn about the normal retains it and reverses the binormal.
713
- const projection = dot(normal, t);
714
- normal = norm(sub(normal, [t[0] * projection, t[1] * projection, t[2] * projection]));
715
- frames.push([normal, cross(t, normal)]);
716
- previous = t;
717
- }
718
- if (closed) {
719
- const end = frames[last][0],
720
- start = frames[0][0];
721
- const correction = Math.atan2(dot(t0, cross(end, start)), dot(end, start));
722
- const lengths = [0];
723
- for (let i = 1; i <= last; i++) lengths.push(lengths[i - 1] + distance(curve[i], curve[i - 1]));
724
- for (let i = 1; i <= last; i++) {
725
- const angle = correction * lengths[i] / lengths[last];
726
- const [n, bn] = frames[i];
727
- const c = Math.cos(angle),
728
- s = Math.sin(angle);
729
- const corrected = [n[0] * c + bn[0] * s, n[1] * c + bn[1] * s, n[2] * c + bn[2] * s];
730
- frames[i] = [corrected, cross(tangents[i], corrected)];
731
- }
732
- frames[last] = frames[0];
733
- }
734
- const b = new Builder();
735
- const ring = curve.map((c, i) => {
736
- const r = Math.max(0, radius(closed && i === last ? 0 : i / last));
737
- const [n, bn] = frames[i];
738
- const first = b.vertices.length / VERTEX_FLOATS;
739
- for (let j = 0; j <= sides; j++) {
740
- const a = (j === sides ? 0 : j / sides) * TAU;
741
- const cosine = Math.cos(a),
742
- sine = Math.sin(a);
743
- const dir = [cosine * n[0] + sine * bn[0], cosine * n[1] + sine * bn[1], cosine * n[2] + sine * bn[2]];
744
- const rj = r * shape(j === sides ? 0 : j / sides);
745
- b.vertex([c[0] + dir[0] * rj, c[1] + dir[1] * rj, c[2] + dir[2] * rj], dir, j / sides, i / (curve.length - 1));
746
- }
747
- return {
748
- first,
749
- r
750
- };
751
- });
752
- for (let i = 0; i < ring.length - 1; i++) {
753
- for (let j = 0; j < sides; j++) {
754
- const a = ring[i].first + j;
755
- const c = ring[i + 1].first + j;
756
- b.indices.push(a, a + 1, c, a + 1, c + 1, c);
757
- }
758
- }
759
- let side = facingOutward(fromBuilder(b));
760
- // Taper changes the longitudinal component too; radial normals only describe a cylinder.
761
- if (options.shape || typeof options.radius === 'function') side = smoothNormals(side);
762
- const parts = [{
763
- geometry: paintBy(side, options.color)
764
- }];
765
- if (!closed && (options.caps ?? true)) {
766
- const capColor = options.capColor ?? (typeof options.color === 'function' ? undefined : options.color);
767
- const rings = typeof capColor === 'function' ? 8 : 1;
768
- for (const [i, facing] of [[0, -1], [ring.length - 1, 1]]) {
769
- const {
770
- r
771
- } = ring[i];
772
- if (r < 1e-6) continue;
773
- const t = tangents[i];
774
- const [fn, fb] = frames[i];
775
- parts.push({
776
- geometry: roundFace(curve[i], fn, fb, [t[0] * facing, t[1] * facing, t[2] * facing], u => r * shape(u === 1 ? 0 : u), TAU, sides, rings, capColor)
777
- });
778
- }
779
- }
780
- return parts.length > 1 ? merge(parts) : parts[0].geometry;
781
- }
782
691
 
783
- /** Flips triangles whose winding disagrees with their vertex normals (so back-face culling keeps them). */
784
- function facingOutward(g) {
785
- const native = nativeGeometry3(g);
786
- if (native) {
787
- native.geometryFaceOutward(g.vertices.buffer, g.indices.buffer);
788
- return g;
789
- }
790
- const v = g.vertices;
791
- const at = (i, k) => v[g.indices[i] * VERTEX_FLOATS + k];
792
- for (let t = 0; t < g.indices.length; t += 3) {
793
- const e1 = [0, 1, 2].map(k => at(t + 1, k) - at(t, k));
794
- const e2 = [0, 1, 2].map(k => at(t + 2, k) - at(t, k));
795
- const face = [e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], e1[0] * e2[1] - e1[1] * e2[0]];
796
- const n = [0, 1, 2].map(k => at(t, k + 3) + at(t + 1, k + 3) + at(t + 2, k + 3));
797
- if (face[0] * n[0] + face[1] * n[1] + face[2] * n[2] < 0) {
798
- [g.indices[t + 1], g.indices[t + 2]] = [g.indices[t + 2], g.indices[t + 1]];
692
+ // Vertices: the side rings, then the start and end caps (where the radius isn't 0).
693
+ let at = count * (sides + 1);
694
+ if (typeof color === 'function') paintRange(g, 0, at, color);
695
+ if (caps && typeof capColor === 'function') {
696
+ for (const i of [0, last]) {
697
+ if (radii[i] < 1e-6) continue;
698
+ const end = at + (capRings + 1) * (sides + 1);
699
+ paintFace(g, at, end, TAU, capColor);
700
+ at = end;
799
701
  }
800
702
  }
801
703
  return g;
@@ -3,6 +3,7 @@
3
3
  import NativeEngine from '../native/NativeNayanEngine';
4
4
  // Textures: PNG / JPEG images, decoded by the Rust core, drawn on built-in shapes or models.
5
5
  import { meshInfo } from "../render/meshes.js";
6
+ import { resolveColor } from "../world/color.js";
6
7
  import { AssetLoader } from "./AssetLoader.js";
7
8
  import { registerCoreMesh } from "./model.js";
8
9
 
@@ -66,4 +67,68 @@ export function texturedMesh(mesh, texture) {
66
67
  }
67
68
  return id;
68
69
  }
70
+
71
+ /** One model in a bake. */
72
+
73
+ /** A model's baked picture: its texture, and the square of the model's x/y it covers. */
74
+
75
+ /**
76
+ * Draws a model once, in the native core, into an anti-aliased picture with a transparent
77
+ * background: seen from +z (orthographic), shaded as the renderer shades it under `light`. Show it
78
+ * instead of the model where it only ever faces the camera (icons, tiles, cards): on the built-in
79
+ * `"plane"` turned to face +z, scaled to `2 * extent`, under a `frontLight` (which shows camera-facing
80
+ * surfaces at exactly their color), with an alpha just below 1 so its soft edges blend. One quad
81
+ * then draws what took thousands of triangles, with smooth edges at every quality level.
82
+ */
83
+ export function bakeModel(model, options) {
84
+ const mesh = model == null ? -1 : typeof model === 'number' ? model : model.id;
85
+ const native = NativeEngine;
86
+ if (typeof native.modelBake !== 'function') throw new Error("bakeModel: this app's native core predates baked models: rebuild it");
87
+ const {
88
+ light
89
+ } = options;
90
+ const lightColor = resolveColor(light.color ?? [1, 1, 1]);
91
+ const sky = resolveColor(options.background ?? [0, 0, 0]);
92
+ const spec = {
93
+ size: Math.max(1, Math.min(1024, Math.round(options.size))),
94
+ supersample: options.supersample ?? 4,
95
+ light: light.direction,
96
+ ambient: light.ambient,
97
+ lightColor: [lightColor[0], lightColor[1], lightColor[2]],
98
+ sky: [sky[0], sky[1], sky[2]],
99
+ shine: options.shine ?? meshInfo(mesh)?.shine ?? 0,
100
+ padding: options.padding ?? 0.04,
101
+ parts: options.parts?.map(part => {
102
+ const id = typeof part.model === 'number' ? part.model : part.model.id;
103
+ const scale = part.scale ?? 1;
104
+ const color = resolveColor(part.color ?? [1, 1, 1]);
105
+ return {
106
+ mesh: id,
107
+ position: part.position ?? [0, 0, 0],
108
+ scale: typeof scale === 'number' ? [scale, scale, scale] : scale,
109
+ color: [color[0], color[1], color[2]],
110
+ shine: part.shine ?? meshInfo(id)?.shine ?? 0
111
+ };
112
+ }),
113
+ frame: options.frame && [options.frame.center[0], options.frame.center[1], options.frame.extent],
114
+ key: options.key
115
+ };
116
+ const baked = native.modelBake(Math.max(0, mesh), JSON.stringify(spec));
117
+ if (baked.length !== 4) throw new Error(`bakeModel: ${native.loadError() || 'failed'}`);
118
+ const [texture, x, y, extent] = baked;
119
+ return {
120
+ texture: {
121
+ id: texture,
122
+ width: spec.size,
123
+ height: spec.size
124
+ },
125
+ center: [x, y],
126
+ extent
127
+ };
128
+ }
129
+
130
+ /** Whether a picture was kept under `key` by `bakeModel` (so it can be restored without its models). */
131
+ export function isBakeKept(key) {
132
+ return typeof NativeEngine?.bakeKept === 'function' && NativeEngine.bakeKept(key);
133
+ }
69
134
  //# sourceMappingURL=texture.js.map
@@ -21,7 +21,7 @@ export class DrawingDocument {
21
21
  boundTexture = null;
22
22
  constructor(width, height, pixelRatio = 2) {
23
23
  if (!NativeEngine || typeof NativeEngine.drawingCreate !== 'function') {
24
- throw new Error('Drawing requires a native app rebuild with the updated Nayan engine.');
24
+ throw new Error('Drawing requires a native app rebuild with the updated Nayan Game Engine.');
25
25
  }
26
26
  NativeEngine.renderWarmUp?.();
27
27
  const id = NativeEngine.drawingCreate(width, height, pixelRatio);
@@ -19,14 +19,17 @@ export { warmUpEngine, getMeshIdLimit } from "./render/meshes.js";
19
19
  export { loadModel } from "./assets/model.js";
20
20
  export { clearAssetCache } from "./assets/AssetCache.js";
21
21
  export { ProceduralCache } from "./assets/ProceduralCache.js";
22
- export { loadTexture } from "./assets/texture.js";
22
+ export { bakeModel, isBakeKept, loadTexture } from "./assets/texture.js";
23
23
  export { Solid, solidGeometry, createSolid, optimizeGeometry, createGeometryLods, getGeometryStats, deformGeometry } from "./assets/modelling.js";
24
24
  export { loft, block, ellipsoid, smoothCurve } from "./assets/loft.js";
25
25
  export { parametricSurface } from "./assets/surface.js";
26
+ export { faceColor, FieldFunction, FieldGraph, FieldRef, generateMesh, generateWorld, generateWorldAsync, isWorldKept, ProceduralWorld, sectionColor, surfaceColor, surfaceDisplacement } from "./assets/procedural.js";
26
27
  export { createShape, extrude, lathe, merge, Path, recolor, shapeGeometry, smoothNormals, tube } from "./assets/shape.js";
27
28
  export { loadFont, measureText } from "./assets/font.js";
28
29
  // Sound and haptics.
29
30
  export { audio } from "./media/audio.js";
31
+ export { speech } from "./media/speech.js";
32
+ export { microphone } from "./media/microphone.js";
30
33
  export { haptics } from "./media/haptics.js";
31
34
  // Input.
32
35
  export { Joystick, useJoystick } from './input/Joystick';
@@ -31,4 +31,31 @@ class NayanMixer extends AudioWorkletProcessor {
31
31
  }
32
32
  }
33
33
  registerProcessor('nayan-mixer', NayanMixer);
34
+
35
+ // The page's microphone: mixes each input block to mono and posts ~43 ms batches to the main thread,
36
+ // which hands them to the core's capture buffer.
37
+ class NayanMicrophone extends AudioWorkletProcessor {
38
+ constructor() {
39
+ super();
40
+ this.batch = new Float32Array(2048);
41
+ this.filled = 0;
42
+ }
43
+ process(inputs) {
44
+ const channels = inputs[0];
45
+ if (!channels || !channels.length) return true;
46
+ const frames = channels[0].length;
47
+ for (let i = 0; i < frames; i++) {
48
+ let sum = 0;
49
+ for (let c = 0; c < channels.length; c++) sum += channels[c][i];
50
+ this.batch[this.filled++] = sum / channels.length;
51
+ if (this.filled === this.batch.length) {
52
+ this.port.postMessage(this.batch, [this.batch.buffer]);
53
+ this.batch = new Float32Array(2048);
54
+ this.filled = 0;
55
+ }
56
+ }
57
+ return true;
58
+ }
59
+ }
60
+ registerProcessor('nayan-microphone', NayanMicrophone);
34
61
  //# sourceMappingURL=audio-worklet.js.map
@@ -67,6 +67,26 @@ export const audio = {
67
67
  stop(voice) {
68
68
  if (voice) NativeEngine?.audioStop(voice);
69
69
  },
70
+ /**
71
+ * Frees a sound's samples, for sounds made at runtime (a `microphone.take` recording, once played).
72
+ * Its id then plays nothing; voices already playing it finish first. Don't release a bank's sounds.
73
+ */
74
+ release(sound) {
75
+ if (sound != null) NativeEngine?.audioRelease?.(sound);
76
+ },
77
+ /** A sound's length in seconds (0 if unknown or released). */
78
+ duration(sound) {
79
+ return NativeEngine?.audioDuration?.(sound) ?? 0;
80
+ },
81
+ /**
82
+ * A sound's loudness (RMS, 0..1) over time, one value per `1 / perSecond` seconds from its start:
83
+ * drive a talking mouth, a bobbing body or a meter from it while the sound plays (at `pitch` p,
84
+ * value i plays at i / (perSecond * p) seconds).
85
+ */
86
+ levels(sound, perSecond = 30) {
87
+ const buffer = NativeEngine?.audioLevels?.(sound, perSecond);
88
+ return buffer ? new Float32Array(buffer) : new Float32Array(0);
89
+ },
70
90
  get muted() {
71
91
  return muted;
72
92
  },