@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
|
@@ -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 {
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
446
|
-
*
|
|
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
|
|
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 =
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
670
|
-
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
if (
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
|
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);
|
package/lib/module/index.js
CHANGED
|
@@ -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
|
},
|