@needle-tools/engine 5.0.2 → 5.1.0-canary.87c4c44

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 (165) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +6 -7
  3. package/SKILL.md +39 -21
  4. package/components.needle.json +1 -1
  5. package/dist/needle-engine.bundle-BlVD1I5a.min.js +1656 -0
  6. package/dist/{needle-engine.bundle-BoTyA-Le.js → needle-engine.bundle-CFOipCo_.js} +8519 -7920
  7. package/dist/needle-engine.bundle-DX2Y-SQF.umd.cjs +1656 -0
  8. package/dist/needle-engine.d.ts +628 -61
  9. package/dist/needle-engine.js +575 -565
  10. package/dist/needle-engine.min.js +1 -1
  11. package/dist/needle-engine.umd.cjs +1 -1
  12. package/dist/{vendor-vHLk8sXu.js → vendor-CAcsI0eU.js} +116 -115
  13. package/dist/{vendor-CntUvmJu.umd.cjs → vendor-CEM38hLE.umd.cjs} +2 -2
  14. package/dist/{vendor-DPbfJJ4d.min.js → vendor-HRlxIBga.min.js} +2 -2
  15. package/lib/engine/api.d.ts +2 -0
  16. package/lib/engine/api.js +2 -0
  17. package/lib/engine/api.js.map +1 -1
  18. package/lib/engine/engine_addressables.js +5 -1
  19. package/lib/engine/engine_addressables.js.map +1 -1
  20. package/lib/engine/engine_animation.d.ts +14 -7
  21. package/lib/engine/engine_animation.js +49 -9
  22. package/lib/engine/engine_animation.js.map +1 -1
  23. package/lib/engine/engine_components.js +33 -4
  24. package/lib/engine/engine_components.js.map +1 -1
  25. package/lib/engine/engine_context.d.ts +7 -2
  26. package/lib/engine/engine_context.js +10 -2
  27. package/lib/engine/engine_context.js.map +1 -1
  28. package/lib/engine/engine_gameobject.d.ts +4 -0
  29. package/lib/engine/engine_gameobject.js.map +1 -1
  30. package/lib/engine/engine_init.js +2 -0
  31. package/lib/engine/engine_init.js.map +1 -1
  32. package/lib/engine/engine_input.js +4 -1
  33. package/lib/engine/engine_input.js.map +1 -1
  34. package/lib/engine/engine_materialpropertyblock.js +1 -20
  35. package/lib/engine/engine_materialpropertyblock.js.map +1 -1
  36. package/lib/engine/engine_networking.d.ts +11 -8
  37. package/lib/engine/engine_networking.js +43 -26
  38. package/lib/engine/engine_networking.js.map +1 -1
  39. package/lib/engine/engine_networking_instantiate.d.ts +100 -5
  40. package/lib/engine/engine_networking_instantiate.js +150 -16
  41. package/lib/engine/engine_networking_instantiate.js.map +1 -1
  42. package/lib/engine/engine_networking_prefabs.d.ts +59 -0
  43. package/lib/engine/engine_networking_prefabs.js +67 -0
  44. package/lib/engine/engine_networking_prefabs.js.map +1 -0
  45. package/lib/engine/postprocessing/api.d.ts +2 -0
  46. package/lib/engine/postprocessing/api.js +2 -0
  47. package/lib/engine/postprocessing/api.js.map +1 -0
  48. package/lib/engine/postprocessing/index.d.ts +2 -0
  49. package/lib/engine/postprocessing/index.js +2 -0
  50. package/lib/engine/postprocessing/index.js.map +1 -0
  51. package/lib/engine/postprocessing/postprocessing.d.ts +83 -0
  52. package/lib/engine/postprocessing/postprocessing.js +280 -0
  53. package/lib/engine/postprocessing/postprocessing.js.map +1 -0
  54. package/lib/engine/postprocessing/types.d.ts +39 -0
  55. package/lib/engine/postprocessing/types.js +2 -0
  56. package/lib/engine/postprocessing/types.js.map +1 -0
  57. package/lib/engine/webcomponents/WebXRButtons.js +17 -3
  58. package/lib/engine/webcomponents/WebXRButtons.js.map +1 -1
  59. package/lib/engine/webcomponents/icons.js +3 -1
  60. package/lib/engine/webcomponents/icons.js.map +1 -1
  61. package/lib/engine/xr/NeedleXRSession.d.ts +1 -0
  62. package/lib/engine/xr/NeedleXRSession.js +43 -10
  63. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  64. package/lib/engine/xr/init.d.ts +4 -0
  65. package/lib/engine/xr/init.js +49 -0
  66. package/lib/engine/xr/init.js.map +1 -0
  67. package/lib/engine-components/AnimationUtils.d.ts +4 -1
  68. package/lib/engine-components/AnimationUtils.js +7 -19
  69. package/lib/engine-components/AnimationUtils.js.map +1 -1
  70. package/lib/engine-components/AnimatorController.d.ts +135 -2
  71. package/lib/engine-components/AnimatorController.js +216 -13
  72. package/lib/engine-components/AnimatorController.js.map +1 -1
  73. package/lib/engine-components/OrbitControls.d.ts +4 -0
  74. package/lib/engine-components/OrbitControls.js +5 -1
  75. package/lib/engine-components/OrbitControls.js.map +1 -1
  76. package/lib/engine-components/SeeThrough.d.ts +0 -2
  77. package/lib/engine-components/SeeThrough.js +0 -89
  78. package/lib/engine-components/SeeThrough.js.map +1 -1
  79. package/lib/engine-components/SyncedRoom.d.ts +4 -0
  80. package/lib/engine-components/SyncedRoom.js +23 -8
  81. package/lib/engine-components/SyncedRoom.js.map +1 -1
  82. package/lib/engine-components/SyncedTransform.js +5 -5
  83. package/lib/engine-components/SyncedTransform.js.map +1 -1
  84. package/lib/engine-components/Voip.d.ts +46 -0
  85. package/lib/engine-components/Voip.js +126 -2
  86. package/lib/engine-components/Voip.js.map +1 -1
  87. package/lib/engine-components/api.d.ts +1 -0
  88. package/lib/engine-components/api.js +1 -0
  89. package/lib/engine-components/api.js.map +1 -1
  90. package/lib/engine-components/postprocessing/Effects/Tonemapping.d.ts +5 -2
  91. package/lib/engine-components/postprocessing/Effects/Tonemapping.js +11 -18
  92. package/lib/engine-components/postprocessing/Effects/Tonemapping.js.map +1 -1
  93. package/lib/engine-components/postprocessing/PostProcessingEffect.d.ts +3 -4
  94. package/lib/engine-components/postprocessing/PostProcessingEffect.js +6 -15
  95. package/lib/engine-components/postprocessing/PostProcessingEffect.js.map +1 -1
  96. package/lib/engine-components/postprocessing/PostProcessingHandler.d.ts +2 -1
  97. package/lib/engine-components/postprocessing/PostProcessingHandler.js.map +1 -1
  98. package/lib/engine-components/postprocessing/Volume.d.ts +18 -11
  99. package/lib/engine-components/postprocessing/Volume.js +61 -140
  100. package/lib/engine-components/postprocessing/Volume.js.map +1 -1
  101. package/lib/engine-components/postprocessing/index.d.ts +1 -0
  102. package/lib/engine-components/postprocessing/index.js +1 -0
  103. package/lib/engine-components/postprocessing/index.js.map +1 -1
  104. package/lib/engine-components/postprocessing/utils.d.ts +2 -0
  105. package/lib/engine-components/postprocessing/utils.js +2 -0
  106. package/lib/engine-components/postprocessing/utils.js.map +1 -1
  107. package/lib/engine-components/ui/Canvas.js +2 -2
  108. package/lib/engine-components/ui/Canvas.js.map +1 -1
  109. package/lib/engine-components/ui/Graphic.d.ts +3 -3
  110. package/lib/engine-components/ui/Graphic.js +6 -2
  111. package/lib/engine-components/ui/Graphic.js.map +1 -1
  112. package/lib/engine-components/ui/Text.d.ts +64 -11
  113. package/lib/engine-components/ui/Text.js +154 -45
  114. package/lib/engine-components/ui/Text.js.map +1 -1
  115. package/lib/engine-components/ui/index.d.ts +1 -0
  116. package/lib/engine-components/ui/index.js +1 -0
  117. package/lib/engine-components/ui/index.js.map +1 -1
  118. package/lib/engine-components-experimental/networking/PlayerSync.d.ts +25 -3
  119. package/lib/engine-components-experimental/networking/PlayerSync.js +60 -11
  120. package/lib/engine-components-experimental/networking/PlayerSync.js.map +1 -1
  121. package/package.json +6 -5
  122. package/plugins/vite/ai.d.ts +11 -10
  123. package/plugins/vite/ai.js +305 -31
  124. package/plugins/vite/dependencies.js +5 -0
  125. package/src/engine/api.ts +3 -0
  126. package/src/engine/engine_addressables.ts +4 -1
  127. package/src/engine/engine_animation.ts +47 -9
  128. package/src/engine/engine_components.ts +36 -7
  129. package/src/engine/engine_context.ts +11 -2
  130. package/src/engine/engine_gameobject.ts +5 -0
  131. package/src/engine/engine_init.ts +2 -0
  132. package/src/engine/engine_input.ts +2 -1
  133. package/src/engine/engine_materialpropertyblock.ts +1 -20
  134. package/src/engine/engine_networking.ts +46 -23
  135. package/src/engine/engine_networking_instantiate.ts +160 -18
  136. package/src/engine/engine_networking_prefabs.ts +80 -0
  137. package/src/engine/postprocessing/api.ts +2 -0
  138. package/src/engine/postprocessing/index.ts +2 -0
  139. package/src/engine/postprocessing/postprocessing.ts +322 -0
  140. package/src/engine/postprocessing/types.ts +43 -0
  141. package/src/engine/webcomponents/WebXRButtons.ts +21 -4
  142. package/src/engine/webcomponents/icons.ts +5 -3
  143. package/src/engine/xr/NeedleXRSession.ts +50 -15
  144. package/src/engine/xr/init.ts +56 -0
  145. package/src/engine-components/AnimationUtils.ts +7 -17
  146. package/src/engine-components/AnimatorController.ts +288 -18
  147. package/src/engine-components/OrbitControls.ts +5 -1
  148. package/src/engine-components/SeeThrough.ts +0 -116
  149. package/src/engine-components/SyncedRoom.ts +28 -9
  150. package/src/engine-components/SyncedTransform.ts +5 -5
  151. package/src/engine-components/Voip.ts +129 -2
  152. package/src/engine-components/api.ts +1 -0
  153. package/src/engine-components/postprocessing/Effects/Tonemapping.ts +16 -24
  154. package/src/engine-components/postprocessing/PostProcessingEffect.ts +9 -16
  155. package/src/engine-components/postprocessing/PostProcessingHandler.ts +2 -1
  156. package/src/engine-components/postprocessing/Volume.ts +72 -163
  157. package/src/engine-components/postprocessing/index.ts +1 -0
  158. package/src/engine-components/postprocessing/utils.ts +2 -0
  159. package/src/engine-components/ui/Canvas.ts +2 -2
  160. package/src/engine-components/ui/Graphic.ts +7 -3
  161. package/src/engine-components/ui/Text.ts +170 -52
  162. package/src/engine-components/ui/index.ts +2 -1
  163. package/src/engine-components-experimental/networking/PlayerSync.ts +64 -11
  164. package/dist/needle-engine.bundle-B3ywqx5o.min.js +0 -1654
  165. package/dist/needle-engine.bundle-CzOPcOui.umd.cjs +0 -1654
@@ -11,7 +11,7 @@ import type { INetworkConnection } from "./engine_networking_types.js";
11
11
  import type { IModel } from "./engine_networking_types.js";
12
12
  import { SendQueue } from "./engine_networking_types.js";
13
13
  import { Context } from "./engine_setup.js"
14
- import type { IComponent as Component, IGameObject as GameObject } from "./engine_types.js"
14
+ import type { IComponent as Component, IContext,IGameObject as GameObject } from "./engine_types.js"
15
15
  import type { UIDProvider } from "./engine_types.js";
16
16
  import * as utils from "./engine_utils.js"
17
17
 
@@ -154,13 +154,46 @@ export function sendDestroyed(guid: string, con: INetworkConnection, opts?: Sync
154
154
  con.send(InstantiateEvent.InstanceDestroyed, model, SendQueue.Queued);
155
155
  }
156
156
 
157
+ declare type SyncDestroyCallback = (guid: string, object: Object3D) => void;
158
+ const _onSyncDestroyCallbacks: SyncDestroyCallback[] = [];
159
+
160
+ /**
161
+ * Register a callback that fires when a remote `syncDestroy` event is received.
162
+ * The callback receives the guid and the resolved Object3D (or null if not found in the scene).
163
+ * The callback fires **before** the object is destroyed, so you can still access its state.
164
+ * @param callback Called with the guid and the Object3D about to be destroyed
165
+ * @returns An unsubscribe function
166
+ * @category Networking
167
+ * @example
168
+ * ```ts
169
+ * const unsub = onSyncDestroy((guid, obj) => {
170
+ * console.log("Remote object destroyed:", guid, obj?.name);
171
+ * });
172
+ * // later: unsub();
173
+ * ```
174
+ */
175
+ export function onSyncDestroy(callback: SyncDestroyCallback): () => void {
176
+ _onSyncDestroyCallbacks.push(callback);
177
+ return () => {
178
+ const idx = _onSyncDestroyCallbacks.indexOf(callback);
179
+ if (idx >= 0) _onSyncDestroyCallbacks.splice(idx, 1);
180
+ };
181
+ }
182
+
157
183
  export function beginListenDestroy(context: Context) {
158
184
  context.connection.beginListen(InstantiateEvent.InstanceDestroyed, (data: DestroyInstanceModel) => {
159
185
  if (debug)
160
186
  console.log("[Remote] Destroyed", context.scene, data);
161
187
  // TODO: create global lookup table for guids
162
188
  const obj = findByGuid(data.guid, context.scene);
163
- if (obj) destroy(obj);
189
+ if (obj) {
190
+ // Notify listeners before destroying
191
+ for (const cb of _onSyncDestroyCallbacks) {
192
+ try { cb(data.guid, obj as Object3D); }
193
+ catch (err) { console.error("Error in onSyncDestroy callback", err); }
194
+ }
195
+ destroy(obj);
196
+ }
164
197
  });
165
198
  }
166
199
 
@@ -215,27 +248,125 @@ export class NewInstanceModel implements IModel {
215
248
 
216
249
  /**
217
250
  * Instantiation options for {@link syncInstantiate}
251
+ * @category Networking
252
+ * @see {@link syncInstantiate} - Instantiate objects across the network
218
253
  */
219
254
  export type SyncInstantiateOptions = IInstantiateOptions & Pick<IModel, "deleteOnDisconnect">;
220
255
 
221
256
  // #region Sync Instantiate
257
+
258
+ /**
259
+ * Callback type for {@link onSyncInstantiate}
260
+ * @param instance The instantiated object
261
+ * @param model The network model data sent with the instantiate event
262
+ * @param context The network context in which the instantiate event was received
263
+ * @category Networking
264
+ */
265
+ declare type SyncInstantiateCallback = (instance: GameObject, model: NewInstanceModel, context: IContext) => void;
266
+ /** Registered callbacks for remote instantiation events */
267
+ const _onSyncInstantiateCallbacks: SyncInstantiateCallback[] = [];
268
+
222
269
  /**
223
- * Instantiate an object across the network. See also {@link syncDestroy}.
270
+ * Register a callback that fires when a remote `syncInstantiate` object is created on this client.
271
+ * Use this to get references to objects spawned by other users.
272
+ * @param callback Called with the instantiated Object3D, the network model data, and the Needle Engine context in which the instantiate event was received
273
+ * @returns An unsubscribe function
224
274
  * @category Networking
275
+ * @example
276
+ * ```ts
277
+ * const unsub = onSyncInstantiate((instance, model, context) => {
278
+ * console.log("Remote object created:", instance.name, model.originalGuid, context);
279
+ * });
280
+ * // later: unsub();
281
+ * ```
282
+ * @see {@link syncInstantiate} - Instantiate objects across the network
283
+ * @see {@link syncDestroy} - Destroy objects across the network
284
+ */
285
+ export function onSyncInstantiate(callback: SyncInstantiateCallback): () => void {
286
+ _onSyncInstantiateCallbacks.push(callback);
287
+ return () => {
288
+ const idx = _onSyncInstantiateCallbacks.indexOf(callback);
289
+ if (idx >= 0) _onSyncInstantiateCallbacks.splice(idx, 1);
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Instantiate an object across the network. The object is cloned locally and a network message
295
+ * is sent so all connected clients create the same clone. Late joiners receive the message
296
+ * via room state replay (unless `deleteOnDisconnect` is set or `save` is false).
297
+ *
298
+ * ## How it works internally
299
+ * 1. The prefab is cloned locally using a seeded {@link InstantiateIdProvider}
300
+ * 2. The seed ensures all clients generate **identical deterministic guids** for the clone
301
+ * and all its children — no need to send individual guids over the network
302
+ * 3. A {@link NewInstanceModel} message is sent containing the prefab's `originalGuid`,
303
+ * the clone's `guid`, the `seed`, and transform data
304
+ * 4. On receiving clients, the prefab is resolved via {@link registerPrefabProvider} or
305
+ * by searching the scene for an object with matching guid, then cloned with the same seed
306
+ *
307
+ * ## Runtime-created prefabs (no GLB)
308
+ * If the object has a `guid` but no prefab provider is registered for it, `syncInstantiate`
309
+ * will **auto-register** the object as a prefab provider. This means for code-only prefabs
310
+ * you just need to set a `guid` — no manual `registerPrefabProvider` call needed, as long as
311
+ * all clients run the same setup code that creates the same prefab with the same guid.
312
+ *
313
+ * @param object The object to instantiate. Must have a `guid` property (set one for runtime objects).
314
+ * @param opts Options for the instantiation, including the network context to send the instantiate event to
315
+ * @param hostData Optional data about a file to download when this object is instantiated (e.g. when instantiated via file drop)
316
+ * @param save When false, the state of this instance will not be saved in the networking backend. Default is true.
317
+ * @returns The instantiated object, or null if instantiation failed (e.g. missing guid or network context)
318
+ * @see {@link syncDestroy} - Destroy objects across the network
319
+ * @see {@link onSyncInstantiate} - Register a callback to get references to remotely instantiated objects
320
+ * @see {@link registerPrefabProvider} - Manually register a prefab provider (auto-registered by syncInstantiate)
321
+ * @see {@link unregisterPrefabProvider} - Remove a registered prefab provider
322
+ * @category Networking
323
+ *
324
+ * @example Basic usage with a runtime-created prefab
325
+ * ```ts
326
+ * const cookie = ObjectUtils.createPrimitive("Cube", { color: 0xff8c00 });
327
+ * cookie.guid = "cookie-prefab";
328
+ * // No need to call registerPrefabProvider — syncInstantiate auto-registers it
329
+ * syncInstantiate(cookie, { parent: ctx.scene, deleteOnDisconnect: false });
330
+ * ```
331
+ *
332
+ * @example With deterministic seed (advanced)
333
+ * ```ts
334
+ * const idProvider = new InstantiateIdProvider("my-seed");
335
+ * const instance = syncInstantiate(prefab, { context, idProvider });
336
+ * ```
337
+ * The seed generates deterministic guids via UUID v5, so all clients produce identical
338
+ * identifiers for the clone and its children without sending them over the network.
225
339
  */
226
340
  export function syncInstantiate(object: GameObject | Object3D, opts: SyncInstantiateOptions, hostData?: HostData, save?: boolean): GameObject | null {
227
341
 
228
342
  const obj: GameObject = object as GameObject;
229
343
 
230
344
  if (!obj.guid) {
231
- console.warn("Can not instantiate: No guid", obj);
232
- return null;
345
+ // Auto-assign guid from object name if available.
346
+ // The name must be the same on all clients (which it is when both run the same setup code).
347
+ if (obj.name) {
348
+ obj.guid = obj.name;
349
+ }
350
+ else {
351
+ console.error("[syncInstantiate] Can not instantiate: a 'guid' is required. For runtime-created objects, either set a name (`obj.name = 'my-prefab'`) or a guid (`obj.guid = 'my-prefab-id'`). The identifier must be the same on all clients.", obj);
352
+ return null;
353
+ }
233
354
  }
234
355
 
235
- if (!opts.context) opts.context = Context.Current;
356
+ // Auto-register the prefab provider if none exists for this guid.
357
+ // This allows runtime-created objects to work with syncInstantiate without
358
+ // manual Prefabs.register calls, as long as all clients create the
359
+ // same prefab with the same guid in their setup code.
360
+ if (!Prefabs.has(obj.guid)) {
361
+ Prefabs.register(obj.guid, async () => obj);
362
+ }
236
363
 
237
364
  if (!opts.context) {
238
- console.error("Missing network instantiate options / reference to network connection in sync instantiate");
365
+ opts.context = Context.Current;
366
+ }
367
+
368
+ if (!opts.context) {
369
+ console.error("[syncInstantiate] Missing network instantiate options / reference to network connection in sync instantiate. Please pass in the Needle Engine context.");
239
370
  return null;
240
371
  }
241
372
 
@@ -318,7 +449,6 @@ const syncedInstantiated = new Array<WeakRef<Object3D>>();
318
449
 
319
450
  export function beginListenInstantiate(context: Context) {
320
451
 
321
-
322
452
  const cb1 = context.connection.beginListen(InstantiateEvent.NewInstanceCreated, async (model: NewInstanceModel) => {
323
453
  const obj: GameObject | null = await tryResolvePrefab(model.originalGuid, context.scene) as GameObject;
324
454
  if (model.preventCreation === true) {
@@ -350,6 +480,11 @@ export function beginListenInstantiate(context: Context) {
350
480
  context.scene.add(inst);
351
481
  if (debug)
352
482
  console.log("[Remote] new instance", "gameobject:", inst?.guid, obj);
483
+ // Notify listeners about the remote instantiation
484
+ for (const cb of _onSyncInstantiateCallbacks) {
485
+ try { cb(inst, model, context); }
486
+ catch (err) { console.error("Error in onSyncInstantiate callback", err); }
487
+ }
353
488
  }
354
489
  });
355
490
  const cb2 = context.connection.beginListen("left-room", () => {
@@ -377,21 +512,28 @@ function instantiateSeeded(obj: GameObject, opts: IInstantiateOptions | null): {
377
512
  return { seed: seed, instance: instance };
378
513
  }
379
514
 
380
- export declare type PrefabProviderCallback = (guid: string) => Promise<Object3D | null>;
515
+ export { type PrefabProviderCallback,Prefabs } from "./engine_networking_prefabs.js";
516
+ import { Prefabs } from "./engine_networking_prefabs.js";
381
517
 
382
- const registeredPrefabProviders: { [key: string]: PrefabProviderCallback } = {};
518
+ /**
519
+ * Register a prefab provider. Forwards to {@link Prefabs.register}.
520
+ * @category Networking
521
+ */
522
+ export function registerPrefabProvider(key: string, fn: (guid: string) => Promise<Object3D | null>) {
523
+ Prefabs.register(key, fn);
524
+ }
383
525
 
384
- //** e.g. provide a function that can return a instantiated object when instantiation event is received */
385
- export function registerPrefabProvider(key: string, fn: PrefabProviderCallback) {
386
- registeredPrefabProviders[key] = fn;
526
+ /**
527
+ * Unregister a prefab provider. Forwards to {@link Prefabs.unregister}.
528
+ * @category Networking
529
+ */
530
+ export function unregisterPrefabProvider(key: string) {
531
+ Prefabs.unregister(key);
387
532
  }
388
533
 
389
534
  async function tryResolvePrefab(guid: string, obj: Object3D): Promise<Object3D | null> {
390
- const prov = registeredPrefabProviders[guid];
391
- if (prov !== null && prov !== undefined) {
392
- const res = await prov(guid);
393
- if (res) return res;
394
- }
535
+ const res = await Prefabs.resolve(guid);
536
+ if (res) return res;
395
537
  return tryFindObjectByGuid(guid, obj) as Object3D;
396
538
  }
397
539
 
@@ -0,0 +1,80 @@
1
+ import { Object3D } from "three";
2
+
3
+ /**
4
+ * Callback type for prefab providers.
5
+ * @param guid The guid of the prefab to resolve
6
+ * @returns The prefab Object3D, or null if not found
7
+ */
8
+ export declare type PrefabProviderCallback = (guid: string) => Promise<Object3D | null>;
9
+
10
+ const registeredProviders: { [key: string]: PrefabProviderCallback } = {};
11
+
12
+ /**
13
+ * Prefab registry for networked instantiation.
14
+ *
15
+ * When a remote {@link syncInstantiate} event is received, the engine looks up the prefab
16
+ * by its guid using this registry. Both GLB-loaded objects and runtime-created objects
17
+ * can be registered here.
18
+ *
19
+ * Note: {@link syncInstantiate} auto-registers prefabs if no provider exists for their guid,
20
+ * so manual registration is only needed for custom resolution logic.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { Prefabs, ObjectUtils } from "@needle-tools/engine";
25
+ *
26
+ * const cookie = ObjectUtils.createPrimitive("Cube", { color: 0xff8c00 });
27
+ * cookie.guid = "cookie-prefab";
28
+ * Prefabs.register("cookie-prefab", async () => cookie);
29
+ *
30
+ * console.log(Prefabs.list()); // ["cookie-prefab"]
31
+ * Prefabs.unregister("cookie-prefab");
32
+ * ```
33
+ *
34
+ * @category Networking
35
+ */
36
+ export const Prefabs = {
37
+
38
+ /**
39
+ * Register a prefab provider that resolves objects by guid for networked instantiation.
40
+ * When a remote `syncInstantiate` event is received, the engine uses this to find the prefab
41
+ * to clone on the receiving client.
42
+ *
43
+ * @param key The guid to register the provider for
44
+ * @param fn Callback that returns the prefab Object3D for the given guid
45
+ */
46
+ register(key: string, fn: PrefabProviderCallback) {
47
+ registeredProviders[key] = fn;
48
+ },
49
+
50
+ /**
51
+ * Unregister a previously registered prefab provider.
52
+ * @param key The guid to unregister
53
+ */
54
+ unregister(key: string) {
55
+ delete registeredProviders[key];
56
+ },
57
+
58
+ /**
59
+ * Returns a list of all registered prefab provider keys (guids).
60
+ * Useful for debugging to see which prefabs are available for networked instantiation.
61
+ */
62
+ list(): string[] {
63
+ return Object.keys(registeredProviders);
64
+ },
65
+
66
+ /**
67
+ * Check if a prefab provider is registered for the given guid.
68
+ * @param key The guid to check
69
+ */
70
+ has(key: string): boolean {
71
+ return key in registeredProviders;
72
+ },
73
+
74
+ /** @internal Resolve a prefab by guid. Used by the networking system. */
75
+ async resolve(guid: string): Promise<Object3D | null> {
76
+ const prov = registeredProviders[guid];
77
+ if (prov) return prov(guid);
78
+ return null;
79
+ },
80
+ } as const;
@@ -0,0 +1,2 @@
1
+ export { PostProcessing } from "./index.js";
2
+ export type { IPostProcessingEffect, IPostProcessingHandler, ITonemappingEffect } from "./types.js";
@@ -0,0 +1,2 @@
1
+ export { PostProcessing } from "./postprocessing.js";
2
+ export type { IPostProcessingEffect, IPostProcessingHandler, ITonemappingEffect } from "./types.js";
@@ -0,0 +1,322 @@
1
+ import type { EffectComposer } from "postprocessing";
2
+ import type { ToneMapping } from "three";
3
+ import type { EffectComposer as ThreeEffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
4
+
5
+ import { isDevEnvironment } from "../debug/index.js";
6
+ import type { Context } from "../engine_context.js";
7
+ import { DeviceUtilities, getParam } from "../engine_utils.js";
8
+ import type { IPostProcessingEffect, IPostProcessingHandler, ITonemappingEffect } from "./types.js";
9
+
10
+ const debug = getParam("debugpost");
11
+
12
+ /**
13
+ * Core postprocessing stack accessible via `context.postprocessing`.
14
+ * Manages the effect pipeline independently of any specific component.
15
+ *
16
+ * Volumes and individual PostProcessingEffect components add/remove effects
17
+ * to this stack. The stack builds the EffectComposer pipeline when dirty.
18
+ *
19
+ * @example Add an effect directly
20
+ * ```ts
21
+ * const bloom = new BloomEffect({ intensity: 3 });
22
+ * this.context.postprocessing.addEffect(bloom);
23
+ * ```
24
+ *
25
+ * @example Remove an effect
26
+ * ```ts
27
+ * this.context.postprocessing.removeEffect(bloom);
28
+ * ```
29
+ */
30
+ export class PostProcessing {
31
+
32
+ private readonly _context: Context;
33
+ private _handler: IPostProcessingHandler | null = null;
34
+ private readonly _effects: IPostProcessingEffect[] = [];
35
+ private _isDirty: boolean = false;
36
+
37
+ /** Currently active postprocessing effects in the stack */
38
+ get effects(): readonly IPostProcessingEffect[] {
39
+ return this._effects;
40
+ }
41
+
42
+ get dirty() { return this._isDirty; }
43
+ set dirty(value: boolean) { this._isDirty = value; }
44
+
45
+ /** The internal PostProcessingHandler that manages the EffectComposer pipeline */
46
+ get handler(): IPostProcessingHandler | null { return this._handler; }
47
+
48
+ /**
49
+ * The effect composer used to render postprocessing effects.
50
+ * This is set internally by the PostProcessingHandler when effects are applied.
51
+ */
52
+ get composer(): EffectComposer | ThreeEffectComposer | null { return this._composer; }
53
+ set composer(value: EffectComposer | ThreeEffectComposer | null) { this._composer = value; }
54
+ private _composer: EffectComposer | ThreeEffectComposer | null = null;
55
+
56
+ /**
57
+ * Set multisampling to "auto" to automatically adjust the multisampling level based on performance.
58
+ * Set to a number to manually set the multisampling level.
59
+ * @default "auto"
60
+ */
61
+ multisampling: "auto" | number = "auto";
62
+
63
+ /** When enabled, the device pixel ratio will be gradually reduced when FPS is low
64
+ * and restored when performance recovers.
65
+ * @default true
66
+ */
67
+ adaptiveResolution: boolean = true;
68
+
69
+ constructor(context: Context) {
70
+ this._context = context;
71
+ }
72
+
73
+ /**
74
+ * Add a post processing effect to the stack.
75
+ * The effect stack will be rebuilt on the next update.
76
+ */
77
+ addEffect(effect: IPostProcessingEffect): void {
78
+ if (this._effects.includes(effect)) return;
79
+ this._effects.push(effect);
80
+ this._isDirty = true;
81
+ }
82
+
83
+ /**
84
+ * Remove a post processing effect from the stack.
85
+ * The effect stack will be rebuilt on the next update.
86
+ */
87
+ removeEffect(effect: IPostProcessingEffect): void {
88
+ const index = this._effects.indexOf(effect);
89
+ if (index !== -1) {
90
+ this._effects.splice(index, 1);
91
+ this._isDirty = true;
92
+ }
93
+ }
94
+
95
+ /** Mark the stack as dirty so the effects are rebuilt on the next update */
96
+ markDirty(): void {
97
+ this._isDirty = true;
98
+ }
99
+
100
+ // --- Adaptive multisampling state ---
101
+ private _enabledTime: number = -1;
102
+ private _multisampleAutoChangeTime: number = 0;
103
+ private _multisampleAutoDecreaseTime: number = 0;
104
+
105
+ /** @internal Called from the context render loop to update the postprocessing pipeline */
106
+ update(): void {
107
+ const context = this._context;
108
+ if (context.isInXR) return;
109
+
110
+ // Wait for a camera before applying
111
+ if (this._isDirty && context.mainCamera) {
112
+ this.apply();
113
+ }
114
+
115
+ // In tonemapping-only mode, keep renderer values in sync with the active effect
116
+ if (this._tonemappingOnlyActive) {
117
+ const activeEffects = this._effects.filter(e => e.active && e.enabled && e.isToneMapping === true);
118
+ if (activeEffects.length > 0) {
119
+ const effect = activeEffects[activeEffects.length - 1] as ITonemappingEffect;
120
+ context.renderer.toneMapping = effect.threeToneMapping;
121
+ context.renderer.toneMappingExposure = effect.toneMappingExposure;
122
+ }
123
+ return;
124
+ }
125
+
126
+ if (!this._handler || !this._composer || this._handler.composer !== this._composer) return;
127
+
128
+ // The composer is always a pmndrs EffectComposer (created by PostProcessingHandler)
129
+ const composer = this._composer as EffectComposer;
130
+
131
+ // Handle context lost
132
+ if (context.renderer.getContext().isContextLost()) {
133
+ context.renderer.forceContextRestore();
134
+ }
135
+ if (composer.getRenderer() !== context.renderer)
136
+ composer.setRenderer(context.renderer);
137
+
138
+ composer.setMainScene(context.scene);
139
+
140
+ // --- Adaptive multisampling ---
141
+ if (this.multisampling === "auto") {
142
+ if (this._handler.hasSmaaEffect) {
143
+ if (this._handler.multisampling !== 0) {
144
+ this._handler.multisampling = 0;
145
+ if (debug || isDevEnvironment()) {
146
+ console.log(`[PostProcessing] multisampling is disabled because it's set to 'auto' and there is an SMAA effect.\n\nIf you need multisampling consider changing 'auto' to a fixed value (e.g. 4).`);
147
+ }
148
+ }
149
+ }
150
+ else {
151
+ const timeSinceLastChange = context.time.realtimeSinceStartup - this._multisampleAutoChangeTime;
152
+
153
+ if (context.time.realtimeSinceStartup - this._enabledTime > 2
154
+ && timeSinceLastChange > .5
155
+ ) {
156
+ const prev = this._handler.multisampling;
157
+
158
+ if (this._handler.multisampling > 0 && context.time.smoothedFps <= 50) {
159
+ this._multisampleAutoChangeTime = context.time.realtimeSinceStartup;
160
+ this._multisampleAutoDecreaseTime = context.time.realtimeSinceStartup;
161
+ let newMultiSample = this._handler.multisampling * .5;
162
+ newMultiSample = Math.floor(newMultiSample);
163
+ if (newMultiSample != this._handler.multisampling) {
164
+ this._handler.multisampling = newMultiSample;
165
+ }
166
+ if (debug) console.debug(`[PostProcessing] Reduced multisampling from ${prev} to ${this._handler.multisampling}`);
167
+ }
168
+ else if (timeSinceLastChange > 1
169
+ && context.time.smoothedFps >= 59
170
+ && this._handler.multisampling < context.renderer.capabilities.maxSamples
171
+ && context.time.realtimeSinceStartup - this._multisampleAutoDecreaseTime > 10
172
+ ) {
173
+ this._multisampleAutoChangeTime = context.time.realtimeSinceStartup;
174
+ let newMultiSample = this._handler.multisampling <= 0 ? 1 : this._handler.multisampling * 2;
175
+ newMultiSample = Math.floor(newMultiSample);
176
+ if (newMultiSample !== this._handler.multisampling) {
177
+ this._handler.multisampling = newMultiSample;
178
+ }
179
+ if (debug) console.debug(`[PostProcessing] Increased multisampling from ${prev} to ${this._handler.multisampling}`);
180
+ }
181
+ }
182
+ }
183
+ }
184
+ else {
185
+ const newMultiSample = Math.max(0, Math.min(this.multisampling as number, context.renderer.capabilities.maxSamples));
186
+ if (newMultiSample !== this._handler.multisampling)
187
+ this._handler.multisampling = newMultiSample;
188
+ }
189
+
190
+ // --- Adaptive pixel ratio ---
191
+ this._handler.adaptivePixelRatio = this.adaptiveResolution;
192
+ this._handler.updateAdaptivePixelRatio();
193
+
194
+ // Update camera on passes if needed
195
+ if (context.mainCamera) {
196
+ const passes = composer.passes;
197
+ for (const pass of passes) {
198
+ if (pass.mainCamera && pass.mainCamera !== context.mainCamera) {
199
+ composer.setMainCamera(context.mainCamera);
200
+ break;
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ private _lastApplyTime?: number;
207
+ private _rapidApplyCount = 0;
208
+
209
+ // --- Tonemapping-only state ---
210
+ /** When true, tonemapping is applied directly to the renderer (no full pipeline) */
211
+ private _tonemappingOnlyActive = false;
212
+ private _previousToneMapping?: ToneMapping;
213
+ private _previousToneMappingExposure?: number;
214
+
215
+ private apply() {
216
+ if (debug) console.log(`[PostProcessing] Apply stack (${this._effects.length} effects)`);
217
+
218
+ if (isDevEnvironment()) {
219
+ if (this._lastApplyTime !== undefined && Date.now() - this._lastApplyTime < 100) {
220
+ this._rapidApplyCount++;
221
+ if (this._rapidApplyCount === 5)
222
+ console.warn("[PostProcessing] Detected rapid post processing modifications - this might be a bug");
223
+ }
224
+ this._lastApplyTime = Date.now();
225
+ }
226
+
227
+ this._isDirty = false;
228
+
229
+ // Collect active effects
230
+ const activeEffects = this._effects.filter(e => e.active && e.enabled);
231
+
232
+ if (activeEffects.length <= 0) {
233
+ this.restoreTonemapping();
234
+ this._handler?.unapply(false);
235
+ return;
236
+ }
237
+
238
+ // Check if ALL active effects are tonemapping-only
239
+ const allToneMapping = activeEffects.every(e => e.isToneMapping === true);
240
+
241
+ if (allToneMapping) {
242
+ // Use the last tonemapping effect added (last in the array)
243
+ const tonemappingEffect = activeEffects[activeEffects.length - 1] as ITonemappingEffect;
244
+
245
+ if (debug) console.log(`[PostProcessing] Only tonemapping effects in stack — applying directly to renderer`);
246
+
247
+ // Store previous values on first activation
248
+ if (!this._tonemappingOnlyActive) {
249
+ this._previousToneMapping = this._context.renderer.toneMapping as ToneMapping;
250
+ this._previousToneMappingExposure = this._context.renderer.toneMappingExposure;
251
+ this._tonemappingOnlyActive = true;
252
+ }
253
+
254
+ // Apply tonemapping directly to renderer
255
+ this._context.renderer.toneMapping = tonemappingEffect.threeToneMapping;
256
+ this._context.renderer.toneMappingExposure = tonemappingEffect.toneMappingExposure;
257
+
258
+ // Tear down any existing postprocessing pipeline
259
+ this._handler?.unapply(false);
260
+ return;
261
+ }
262
+
263
+ // We have non-tonemapping effects — restore renderer tonemapping if we were in tonemapping-only mode
264
+ this.restoreTonemapping();
265
+
266
+ // Build full postprocessing pipeline
267
+ this.ensureHandler()
268
+ .then(handler => {
269
+ if (!handler) return;
270
+ return handler.apply(activeEffects) as Promise<void> | void;
271
+ })
272
+ .then(() => {
273
+ if (this._handler) {
274
+ if (this.multisampling === "auto") {
275
+ this._handler.multisampling = DeviceUtilities.isMobileDevice()
276
+ ? 2
277
+ : 4;
278
+ }
279
+ else {
280
+ this._handler.multisampling = Math.max(0, Math.min(this.multisampling as number, this._context.renderer.capabilities.maxSamples));
281
+ }
282
+ if (debug) console.debug(`[PostProcessing] Set multisampling to ${this._handler.multisampling} (Is Mobile: ${DeviceUtilities.isMobileDevice()})`);
283
+ }
284
+ });
285
+
286
+ this._enabledTime = this._context.time.realtimeSinceStartup;
287
+ }
288
+
289
+ /** Restore renderer tonemapping to previous values when leaving tonemapping-only mode */
290
+ private restoreTonemapping() {
291
+ if (this._tonemappingOnlyActive) {
292
+ if (this._previousToneMapping !== undefined)
293
+ this._context.renderer.toneMapping = this._previousToneMapping;
294
+ if (this._previousToneMappingExposure !== undefined)
295
+ this._context.renderer.toneMappingExposure = this._previousToneMappingExposure;
296
+ this._tonemappingOnlyActive = false;
297
+ this._previousToneMapping = undefined;
298
+ this._previousToneMappingExposure = undefined;
299
+ if (debug) console.log(`[PostProcessing] Restored renderer tonemapping`);
300
+ }
301
+ }
302
+
303
+ /** Lazily creates the PostProcessingHandler to avoid loading the postprocessing library until actually needed */
304
+ private async ensureHandler(): Promise<IPostProcessingHandler> {
305
+ if (!this._handler) {
306
+ const { PostProcessingHandler } = await import("../../engine-components/postprocessing/PostProcessingHandler.js");
307
+ if (!this._handler) {
308
+ this._handler = new PostProcessingHandler(this._context);
309
+ }
310
+ }
311
+ return this._handler;
312
+ }
313
+
314
+ /** @internal */
315
+ dispose() {
316
+ this.restoreTonemapping();
317
+ this._handler?.dispose();
318
+ this._handler = null;
319
+ this._composer = null;
320
+ this._effects.length = 0;
321
+ }
322
+ }