@nayan-ui/engine 1.0.0-beta.4 → 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 +225 -116
- 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
|
@@ -1,44 +1,93 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
// Rendering quality: how many pixels are drawn and how much each costs. Phones' GPUs are mostly
|
|
4
|
-
// limited by fill rate, so resolution and anti-aliasing matter most, then
|
|
4
|
+
// limited by fill rate, so resolution and anti-aliasing matter most, then shadows.
|
|
5
5
|
|
|
6
6
|
/** `"auto"` starts at "high" and steps down only while the device really can't keep up. */
|
|
7
7
|
|
|
8
8
|
/** From best looking to cheapest; "auto" walks down this list only as far as it must. */
|
|
9
9
|
export const QUALITIES = ['high', 'medium', 'low', 'lowest'];
|
|
10
10
|
export const QUALITY_LEVELS = {
|
|
11
|
-
// Ordered by what each step costs the eye
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// Ordered by what each step costs the eye. Shadows always use a full-resolution map: a low-resolution
|
|
12
|
+
// map's soft, smeared edges look worse than no shadow. "medium" keeps them with crisp single-tap
|
|
13
|
+
// edges (cheaper than the 3x3 filter); "low" drops the shadow pass entirely (which also saves its
|
|
14
|
+
// GPU time) and anti-aliasing; only the last resort draws fewer pixels, since anything below the screen's own resolution is stretched up and looks
|
|
15
|
+
// soft (text most of all). Fill rate dominates a low-end GPU: an Adreno 610 tablet drew 2.4 MP in
|
|
16
|
+
// 42 ms and 0.78 MP in 21 ms (23 -> 41 fps).
|
|
14
17
|
high: {
|
|
15
18
|
maxPixelRatio: 3,
|
|
16
19
|
samples: 4,
|
|
20
|
+
shadows: true,
|
|
17
21
|
shadowSize: 2048,
|
|
18
22
|
softShadows: true
|
|
19
23
|
},
|
|
20
24
|
medium: {
|
|
21
25
|
maxPixelRatio: 3,
|
|
22
26
|
samples: 4,
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
shadows: true,
|
|
28
|
+
shadowSize: 2048,
|
|
29
|
+
softShadows: false
|
|
25
30
|
},
|
|
26
31
|
low: {
|
|
27
32
|
maxPixelRatio: 3,
|
|
28
33
|
samples: 1,
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
shadows: false,
|
|
35
|
+
shadowSize: 2048,
|
|
36
|
+
softShadows: true
|
|
31
37
|
},
|
|
32
38
|
lowest: {
|
|
33
39
|
maxPixelRatio: 2,
|
|
34
40
|
samples: 1,
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
fxaa: true,
|
|
42
|
+
shadows: false,
|
|
43
|
+
shadowSize: 2048,
|
|
44
|
+
softShadows: true,
|
|
45
|
+
maxPixels: 800_000
|
|
37
46
|
}
|
|
38
47
|
};
|
|
39
48
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
49
|
+
/**
|
|
50
|
+
* What "auto" walks through, from best to cheapest: a level, and the most pixels its 3D scene is drawn
|
|
51
|
+
* with. Before giving up an effect it draws the scene a little smaller, as native games scale their
|
|
52
|
+
* resolution: stretched over the screen under the full-resolution HUD, 1.5 MP (0.8x a 1200x2000
|
|
53
|
+
* tablet) can't be told from the screen's own resolution and 1.1 MP barely, while every effect stays.
|
|
54
|
+
* On the Adreno 610 tablet that took Crossy Road from 47 to 60 fps and Whack a Mole from 53 to 59 at
|
|
55
|
+
* "high": a tile-based GPU's cost under 4x MSAA grows with pixels times the triangles in each tile.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
export const AUTO_STEPS = [{
|
|
59
|
+
quality: 'high'
|
|
60
|
+
}, {
|
|
61
|
+
quality: 'high',
|
|
62
|
+
maxPixels: 1_500_000
|
|
63
|
+
}, {
|
|
64
|
+
quality: 'high',
|
|
65
|
+
maxPixels: 1_100_000
|
|
66
|
+
}, {
|
|
67
|
+
quality: 'medium',
|
|
68
|
+
maxPixels: 1_100_000
|
|
69
|
+
}, {
|
|
70
|
+
quality: 'low',
|
|
71
|
+
maxPixels: 1_100_000
|
|
72
|
+
}, {
|
|
73
|
+
quality: 'lowest'
|
|
74
|
+
}];
|
|
75
|
+
|
|
76
|
+
/** A step's level: its quality's, with the step's pixel budget when that is smaller. */
|
|
77
|
+
export function stepLevel(step) {
|
|
78
|
+
const level = QUALITY_LEVELS[step.quality];
|
|
79
|
+
if (!step.maxPixels || level.maxPixels && level.maxPixels <= step.maxPixels) return level;
|
|
80
|
+
return {
|
|
81
|
+
...level,
|
|
82
|
+
maxPixels: step.maxPixels
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A typical frame slower than this (under ~54 fps) is a frame the device can't hold: on a 60 Hz screen
|
|
88
|
+
* that is a visible stutter, and the first steps down cost only a little resolution.
|
|
89
|
+
*/
|
|
90
|
+
const SLOW_FRAME_MS = 18.5;
|
|
42
91
|
/** Under ~33 fps the game is plainly struggling: one such second is enough to step down. */
|
|
43
92
|
const VERY_SLOW_FRAME_MS = 30;
|
|
44
93
|
/** A typical frame this quick (60 fps with room to spare) is one it holds comfortably. */
|
|
@@ -52,16 +101,69 @@ const SETTLE_MS = 1500;
|
|
|
52
101
|
const SLOW_SECONDS = 3;
|
|
53
102
|
const VERY_SLOW_SECONDS = 1;
|
|
54
103
|
const FAST_SECONDS = 10;
|
|
104
|
+
/**
|
|
105
|
+
* Seconds with the GPU drawing in at most this share of a 60 Hz frame: plenty of headroom (a light
|
|
106
|
+
* board game on a level below what it can hold, e.g. one remembered from a heavier version), so step
|
|
107
|
+
* up after only `HEADROOM_SECONDS` instead of FAST_SECONDS. These needn't be smooth: a frame now and
|
|
108
|
+
* then that the game loop (not drawing) drops can't be helped by a lower level, and would otherwise keep
|
|
109
|
+
* a game that draws in a few milliseconds at 57 fps on its remembered level for good.
|
|
110
|
+
*/
|
|
111
|
+
const HEADROOM_RENDER_MS = 10;
|
|
112
|
+
const HEADROOM_SECONDS = 2;
|
|
55
113
|
/** Stepping down again this soon after a step up means that level is too much: never return to it. */
|
|
56
114
|
const RELAPSE_MS = 20_000;
|
|
57
115
|
|
|
116
|
+
/** The pixel ratio a level draws a `width` x `height` point view at, on a screen of ratio `screen`. */
|
|
117
|
+
export function levelPixelRatio(level, screen, width, height) {
|
|
118
|
+
const ratio = Math.min(screen, level.maxPixelRatio);
|
|
119
|
+
const area = width * height;
|
|
120
|
+
if (!level.maxPixels || area <= 0) return ratio;
|
|
121
|
+
return Math.min(ratio, Math.sqrt(level.maxPixels / area));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Durable records (the engine's Rust store); absent on builds without it, which then remember per session. */
|
|
125
|
+
|
|
126
|
+
// Steps of AUTO_STEPS (the older `quality:` records held a level's index).
|
|
127
|
+
const storeKey = key => `quality-step:${key}`;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The step (of AUTO_STEPS) "auto" settled on per `qualityKey` (e.g. a game's id), kept in `store`
|
|
131
|
+
* across app launches: every visit of the same game starts there instead of spending its first seconds
|
|
132
|
+
* at "high" on a device that can't hold it (it still steps back up after smooth seconds).
|
|
133
|
+
*/
|
|
134
|
+
const settled = new Map();
|
|
135
|
+
export function settledQuality(key, store) {
|
|
136
|
+
if (!key) return undefined;
|
|
137
|
+
const known = settled.get(key);
|
|
138
|
+
if (known !== undefined || typeof store?.cacheRead !== 'function') return known;
|
|
139
|
+
try {
|
|
140
|
+
const bytes = new Uint8Array(store.cacheRead(storeKey(key)));
|
|
141
|
+
const step = bytes.length === 1 && bytes[0] < AUTO_STEPS.length ? bytes[0] : undefined;
|
|
142
|
+
if (step !== undefined) settled.set(key, step);
|
|
143
|
+
return step;
|
|
144
|
+
} catch {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Records the step for `key`, writing it to `store` only when it changes. */
|
|
149
|
+
export function rememberQuality(key, step, store) {
|
|
150
|
+
if (!key || settledQuality(key, store) === step) return;
|
|
151
|
+
settled.set(key, step);
|
|
152
|
+
try {
|
|
153
|
+
store?.cacheWrite?.(storeKey(key), new Uint8Array([step]).buffer);
|
|
154
|
+
} catch {
|
|
155
|
+
// Remembered for this session; the next launch starts from the stored (or default) step.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
58
159
|
/** One second of frames, as the frame loop measured it. */
|
|
59
160
|
|
|
60
161
|
/**
|
|
61
|
-
* Picks the
|
|
162
|
+
* Picks the step (of AUTO_STEPS) for "auto": starts at the best, steps down one after `SLOW_SECONDS`
|
|
62
163
|
* consecutive seconds whose typical frame misses ~50 fps while drawing took most of it (after a
|
|
63
164
|
* single second when it misses ~33 fps, so a struggling phone recovers in seconds rather than
|
|
64
|
-
* stuttering through every level), and steps back up after `FAST_SECONDS` smooth seconds
|
|
165
|
+
* stuttering through every level), and steps back up after `FAST_SECONDS` smooth seconds (`HEADROOM_SECONDS` when
|
|
166
|
+
* the GPU has plenty of time to spare). A level that fails again soon after being restored
|
|
65
167
|
* becomes the ceiling, so it never bounces.
|
|
66
168
|
*/
|
|
67
169
|
export class AutoQuality {
|
|
@@ -74,8 +176,25 @@ export class AutoQuality {
|
|
|
74
176
|
slow = 0;
|
|
75
177
|
verySlow = 0;
|
|
76
178
|
fast = 0;
|
|
179
|
+
headroom = 0;
|
|
180
|
+
|
|
181
|
+
/** `start`: the step to begin at (one remembered from an earlier visit), or a quality's first step. */
|
|
182
|
+
constructor(start = 0) {
|
|
183
|
+
const index = typeof start === 'number' ? Math.floor(start) : AUTO_STEPS.findIndex(s => s.quality === start);
|
|
184
|
+
this.index = Math.min(Math.max(0, index), AUTO_STEPS.length - 1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The current step's index in AUTO_STEPS. */
|
|
188
|
+
get step() {
|
|
189
|
+
return this.index;
|
|
190
|
+
}
|
|
77
191
|
get quality() {
|
|
78
|
-
return
|
|
192
|
+
return AUTO_STEPS[this.index].quality;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** What to draw with: the step's level and pixel budget. */
|
|
196
|
+
get level() {
|
|
197
|
+
return stepLevel(AUTO_STEPS[this.index]);
|
|
79
198
|
}
|
|
80
199
|
|
|
81
200
|
/** Feeds one second of frames; returns true when the level changed. */
|
|
@@ -87,24 +206,39 @@ export class AutoQuality {
|
|
|
87
206
|
this.slow = gpuBound ? this.slow + 1 : 0;
|
|
88
207
|
this.verySlow = gpuBound && frames.frameMs > VERY_SLOW_FRAME_MS ? this.verySlow + 1 : 0;
|
|
89
208
|
this.fast = smooth ? this.fast + 1 : 0;
|
|
90
|
-
|
|
209
|
+
this.headroom = !gpuBound && frames.renderMs <= HEADROOM_RENDER_MS ? this.headroom + 1 : 0;
|
|
210
|
+
if ((this.slow >= SLOW_SECONDS || this.verySlow >= VERY_SLOW_SECONDS) && this.index < AUTO_STEPS.length - 1) {
|
|
91
211
|
// Failing again right after being restored: this level is too much for the device
|
|
92
212
|
if (now - this.raisedAt < RELAPSE_MS) this.ceiling = Math.max(this.ceiling, this.index + 1);
|
|
93
213
|
this.index++;
|
|
94
214
|
return this.moved(now);
|
|
95
215
|
}
|
|
96
|
-
if (this.fast >= FAST_SECONDS && this.index > this.ceiling) {
|
|
216
|
+
if ((this.fast >= FAST_SECONDS || this.headroom >= HEADROOM_SECONDS) && this.index > this.ceiling) {
|
|
97
217
|
this.index--;
|
|
98
218
|
this.raisedAt = now;
|
|
99
219
|
return this.moved(now);
|
|
100
220
|
}
|
|
101
221
|
return false;
|
|
102
222
|
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Frames that don't count (a results card over the game): forgets the streaks and, once frames
|
|
226
|
+
* count again, waits `SETTLE_MS` before judging, as after a change of level.
|
|
227
|
+
*/
|
|
228
|
+
hold(now) {
|
|
229
|
+
if (!this.started) this.started = now;
|
|
230
|
+
this.changed = now;
|
|
231
|
+
this.slow = 0;
|
|
232
|
+
this.verySlow = 0;
|
|
233
|
+
this.fast = 0;
|
|
234
|
+
this.headroom = 0;
|
|
235
|
+
}
|
|
103
236
|
moved(now) {
|
|
104
237
|
this.changed = now;
|
|
105
238
|
this.slow = 0;
|
|
106
239
|
this.verySlow = 0;
|
|
107
240
|
this.fast = 0;
|
|
241
|
+
this.headroom = 0;
|
|
108
242
|
return true;
|
|
109
243
|
}
|
|
110
244
|
}
|
|
@@ -77,6 +77,10 @@ export class World {
|
|
|
77
77
|
boundsValue = null;
|
|
78
78
|
listenerValue = null;
|
|
79
79
|
updates = 0;
|
|
80
|
+
/** The layer still entities live in, the stand-in each one has here, and whether one changed. */
|
|
81
|
+
backdropValue = null;
|
|
82
|
+
stills = new Map();
|
|
83
|
+
stillChanged = false;
|
|
80
84
|
constructor(capacity) {
|
|
81
85
|
this.capacity = capacity;
|
|
82
86
|
if (!NativeEngine) {
|
|
@@ -114,6 +118,7 @@ export class World {
|
|
|
114
118
|
|
|
115
119
|
/** Adds an entity (one native call; text adds one per letter). Throws if the world is full or an option is invalid. */
|
|
116
120
|
spawn(options = {}) {
|
|
121
|
+
if (options.still) return this.spawnStill(options);
|
|
117
122
|
if (options.parent != null && options.physics != null) {
|
|
118
123
|
throw new Error("World.spawn: attached entities can't have physics (put it on the parent)");
|
|
119
124
|
}
|
|
@@ -146,12 +151,57 @@ export class World {
|
|
|
146
151
|
return e;
|
|
147
152
|
}
|
|
148
153
|
|
|
154
|
+
/**
|
|
155
|
+
* The still entities' layer (see `EntityOptions.still`), drawn once into a cached image behind this
|
|
156
|
+
* world; null until it's first used. GameView draws it; it's disposed with this world.
|
|
157
|
+
*/
|
|
158
|
+
get backdrop() {
|
|
159
|
+
return this.backdropValue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The still layer itself, made on first use with room for `capacity` entities: for a whole still
|
|
164
|
+
* scene (a meadow) spawned straight into it. Step it (`update(0)`) after changing it, which is
|
|
165
|
+
* what tells GameView to draw it again; it isn't stepped otherwise, so nothing in it moves.
|
|
166
|
+
*/
|
|
167
|
+
backdropLayer(capacity = 64) {
|
|
168
|
+
if (this.disposed) throw new Error('World: used after dispose()');
|
|
169
|
+
return this.backdropValue ??= new World(capacity);
|
|
170
|
+
}
|
|
171
|
+
spawnStill({
|
|
172
|
+
still: _,
|
|
173
|
+
...options
|
|
174
|
+
}) {
|
|
175
|
+
if (options.parent != null || options.physics != null || options.text !== undefined) {
|
|
176
|
+
throw new Error("World.spawn: a still entity can't be attached or have physics or text");
|
|
177
|
+
}
|
|
178
|
+
const layer = this.backdropLayer();
|
|
179
|
+
const entity = layer.spawn({
|
|
180
|
+
pickable: false,
|
|
181
|
+
...options
|
|
182
|
+
});
|
|
183
|
+
// A stand-in here keeps the handle this world's own (unique, and cleared with it)
|
|
184
|
+
const standIn = this.spawn({
|
|
185
|
+
mesh: 'none',
|
|
186
|
+
pickable: false
|
|
187
|
+
});
|
|
188
|
+
this.stills.set(standIn, entity);
|
|
189
|
+
this.stillChanged = true;
|
|
190
|
+
return standIn;
|
|
191
|
+
}
|
|
192
|
+
|
|
149
193
|
/**
|
|
150
194
|
* Changes an entity (one native call). Only the options you pass change; `null` removes.
|
|
151
195
|
* Returns false if the entity is gone, the world is disposed, or an option was rejected (the others still apply).
|
|
152
196
|
*/
|
|
153
197
|
set(e, options) {
|
|
154
198
|
if (this.disposed) return false;
|
|
199
|
+
const still = this.stills.get(e);
|
|
200
|
+
if (still !== undefined) {
|
|
201
|
+
if (options.parent != null || options.physics != null) throw new Error("World.set: a still entity can't be attached or have physics");
|
|
202
|
+
this.stillChanged = true;
|
|
203
|
+
return this.backdropValue.set(still, options);
|
|
204
|
+
}
|
|
155
205
|
const text = this.texts.get(e);
|
|
156
206
|
// A text entity stays an invisible root; its letters are rebuilt or recolored below.
|
|
157
207
|
encode(this.desc, options, text ? undefined : this.resolveMesh(options, 'set'));
|
|
@@ -203,6 +253,12 @@ export class World {
|
|
|
203
253
|
/** Removes an entity and anything attached to it. Returns false if it was already gone. */
|
|
204
254
|
despawn(e) {
|
|
205
255
|
if (this.disposed) return false;
|
|
256
|
+
const still = this.stills.get(e);
|
|
257
|
+
if (still !== undefined) {
|
|
258
|
+
this.stills.delete(e);
|
|
259
|
+
this.backdropValue.despawn(still);
|
|
260
|
+
this.stillChanged = true;
|
|
261
|
+
}
|
|
206
262
|
const existed = this.native.despawn(this.id, e);
|
|
207
263
|
this.texts.delete(e);
|
|
208
264
|
this.resolveFinished(); // its animations, and those of attached entities that went with it
|
|
@@ -667,6 +723,11 @@ export class World {
|
|
|
667
723
|
this.resolveFinished();
|
|
668
724
|
this.runFallbackTimers(dt);
|
|
669
725
|
if (!this.disposed) this.scoreboardValue?.dispatch();
|
|
726
|
+
// Still entities changed: step their layer once, which tells GameView to have it drawn again
|
|
727
|
+
if (this.stillChanged && this.backdropValue && !this.disposed) {
|
|
728
|
+
this.stillChanged = false;
|
|
729
|
+
this.backdropValue.update(0);
|
|
730
|
+
}
|
|
670
731
|
}
|
|
671
732
|
|
|
672
733
|
/**
|
|
@@ -698,6 +759,8 @@ export class World {
|
|
|
698
759
|
this.pending.clear();
|
|
699
760
|
this.timers.clear();
|
|
700
761
|
this.fallbackTimers = [];
|
|
762
|
+
this.stills.clear();
|
|
763
|
+
this.backdropValue?.dispose();
|
|
701
764
|
NativeEngine.destroyWorld(this.id);
|
|
702
765
|
}
|
|
703
766
|
}
|
|
@@ -24,6 +24,8 @@ export declare class ProceduralCache {
|
|
|
24
24
|
private failures;
|
|
25
25
|
private writes;
|
|
26
26
|
constructor(revision: string, storage?: ProceduralStorage | undefined);
|
|
27
|
+
/** The engine storage key of `key` in this revision (for records the native core reads and writes itself). */
|
|
28
|
+
storageKey(key: string): string;
|
|
27
29
|
get stats(): {
|
|
28
30
|
hits: number;
|
|
29
31
|
builds: number;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Vec3 } from '../types.js';
|
|
2
2
|
import type { Color } from '../world/types.js';
|
|
3
|
+
import { FieldFunction } from './procedural.js';
|
|
3
4
|
import { type Geometry } from './shape.js';
|
|
4
5
|
export interface LoftSection {
|
|
5
6
|
z: number;
|
|
@@ -14,7 +15,8 @@ export interface LoftSection {
|
|
|
14
15
|
export interface LoftOptions {
|
|
15
16
|
around?: number;
|
|
16
17
|
smoothness?: number;
|
|
17
|
-
color
|
|
18
|
+
/** One color, a function of position and normal, or a `surfaceColor` field function (native). */
|
|
19
|
+
color?: Color | ((position: Vec3, normal: Vec3) => Color) | FieldFunction;
|
|
18
20
|
}
|
|
19
21
|
/**
|
|
20
22
|
* Smooth closed body through rounded rectangular cross sections along Z. Useful for vehicle
|