@needle-tools/engine 5.1.1 → 5.1.2

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 (68) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/{needle-engine.bundle-Dv-p5m4z.js → needle-engine.bundle-BAUfRyBA.js} +2861 -2708
  3. package/dist/{needle-engine.bundle-Cb39lTJn.umd.cjs → needle-engine.bundle-BL2wG5Te.umd.cjs} +101 -101
  4. package/dist/{needle-engine.bundle-BUT4ua7U.min.js → needle-engine.bundle-BpNRkY3s.min.js} +148 -148
  5. package/dist/needle-engine.d.ts +18 -6
  6. package/dist/needle-engine.js +401 -401
  7. package/dist/needle-engine.min.js +1 -1
  8. package/dist/needle-engine.umd.cjs +1 -1
  9. package/lib/engine/api.d.ts +1 -1
  10. package/lib/engine/api.js +1 -1
  11. package/lib/engine/api.js.map +1 -1
  12. package/lib/engine/engine_init.js +2 -2
  13. package/lib/engine/engine_init.js.map +1 -1
  14. package/lib/engine/engine_license.d.ts +45 -7
  15. package/lib/engine/engine_license.js +276 -77
  16. package/lib/engine/engine_license.js.map +1 -1
  17. package/lib/engine/engine_networking.js +6 -0
  18. package/lib/engine/engine_networking.js.map +1 -1
  19. package/lib/engine/engine_networking_blob.js +3 -3
  20. package/lib/engine/engine_networking_blob.js.map +1 -1
  21. package/lib/engine/engine_utils_qrcode.js +2 -2
  22. package/lib/engine/engine_utils_qrcode.js.map +1 -1
  23. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
  24. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
  25. package/lib/engine/webcomponents/needle menu/needle-menu.js +5 -5
  26. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  27. package/lib/engine/webcomponents/needle-engine.js +31 -2
  28. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  29. package/lib/engine/webcomponents/needle-engine.loading.js +2 -2
  30. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  31. package/lib/engine/xr/NeedleXRSync.js +23 -4
  32. package/lib/engine/xr/NeedleXRSync.js.map +1 -1
  33. package/lib/engine/xr/TempXRContext.js +2 -2
  34. package/lib/engine/xr/TempXRContext.js.map +1 -1
  35. package/lib/engine-components/SyncedTransform.d.ts +5 -1
  36. package/lib/engine-components/SyncedTransform.js +45 -8
  37. package/lib/engine-components/SyncedTransform.js.map +1 -1
  38. package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
  39. package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
  40. package/lib/engine-components/webxr/Avatar.d.ts +8 -0
  41. package/lib/engine-components/webxr/Avatar.js +60 -32
  42. package/lib/engine-components/webxr/Avatar.js.map +1 -1
  43. package/lib/engine-schemes/synced-transform-model.d.ts +2 -0
  44. package/lib/engine-schemes/synced-transform-model.js +8 -1
  45. package/lib/engine-schemes/synced-transform-model.js.map +1 -1
  46. package/package.json +1 -1
  47. package/plugins/common/license.js +4 -4
  48. package/plugins/types/userconfig.d.ts +4 -3
  49. package/plugins/vite/build-pipeline.d.ts +3 -1
  50. package/plugins/vite/build-pipeline.js +19 -9
  51. package/plugins/vite/license.js +4 -4
  52. package/src/engine/api.ts +1 -1
  53. package/src/engine/engine_init.ts +2 -2
  54. package/src/engine/engine_license.ts +263 -73
  55. package/src/engine/engine_networking.ts +5 -0
  56. package/src/engine/engine_networking_blob.ts +3 -3
  57. package/src/engine/engine_utils_qrcode.ts +2 -2
  58. package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
  59. package/src/engine/webcomponents/needle menu/needle-menu.ts +5 -5
  60. package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
  61. package/src/engine/webcomponents/needle-engine.ts +31 -2
  62. package/src/engine/xr/NeedleXRSync.ts +24 -4
  63. package/src/engine/xr/TempXRContext.ts +2 -2
  64. package/src/engine-components/SyncedTransform.ts +49 -10
  65. package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
  66. package/src/engine-components/webxr/Avatar.ts +60 -33
  67. package/src/engine-schemes/synced-transform-model.ts +10 -1
  68. package/src/engine-schemes/transforms.fbs +1 -0
@@ -49,12 +49,12 @@ export function needleLicense(command, config, userSettings) {
49
49
  let modified = false;
50
50
 
51
51
  // Replace license type
52
- const index = src.indexOf("__cOctKuk");
52
+ const index = src.indexOf("__UoF");
53
53
  if (index >= 0) {
54
54
  const end = src.indexOf(";", index);
55
55
  if (end >= 0) {
56
56
  const line = src.substring(index, end);
57
- const replaced = "__cOctKuk = \"" + licenseResult.type + "\"";
57
+ const replaced = "__UoF = \"" + licenseResult.type + "\"";
58
58
  src = src.replace(line, replaced);
59
59
  modified = true;
60
60
  }
@@ -62,12 +62,12 @@ export function needleLicense(command, config, userSettings) {
62
62
 
63
63
  // Replace license JWT (same pattern)
64
64
  if (licenseResult.jwt) {
65
- const jwtIndex = src.indexOf("$nKsEYz");
65
+ const jwtIndex = src.indexOf("_nMW");
66
66
  if (jwtIndex >= 0) {
67
67
  const jwtEnd = src.indexOf(";", jwtIndex);
68
68
  if (jwtEnd >= 0) {
69
69
  const jwtLine = src.substring(jwtIndex, jwtEnd);
70
- const jwtReplaced = "$nKsEYz = \"" + licenseResult.jwt + "\"";
70
+ const jwtReplaced = "_nMW = \"" + licenseResult.jwt + "\"";
71
71
  src = src.replace(jwtLine, jwtReplaced);
72
72
  modified = true;
73
73
  }
package/src/engine/api.ts CHANGED
@@ -230,7 +230,7 @@ export * from "./engine_input.js";
230
230
  export { InstancingUtil } from "./engine_instancing.js";
231
231
 
232
232
  /** License checking utilities */
233
- export { $Mhcqc, $uRk, __dSkNu } from "./engine_license.js";
233
+ export { Jkdun, _$apSvAt, _hyjgv } from "./engine_license.js";
234
234
 
235
235
 
236
236
  // ============================================================================
@@ -8,7 +8,7 @@ import { initBuiltinTypes } from "./codegen/register_types.js";
8
8
  import { initSpatialConsole } from "./debug/debug_spatial_console.js";
9
9
  import { initAddressableSerializers } from "./engine_addressables.js";
10
10
  import { ensureAudioContextIsResumed } from "./engine_audio.js";
11
- import { _KGv } from "./engine_license.js";
11
+ import { __NjkWq } from "./engine_license.js";
12
12
  import { initNeedleLoader } from "./engine_loaders.js";
13
13
  import { initPhysics } from "./engine_physics_rapier.js";
14
14
  import { initBuiltinSerializers } from "./engine_serialization_builtin_serializer.js";
@@ -59,7 +59,7 @@ export function initEngine() {
59
59
  initPhysics();
60
60
  initXR();
61
61
  initSpatialConsole();
62
- _KGv();
62
+ __NjkWq();
63
63
 
64
64
  // Must be last. customElements.define() upgrades existing <needle-engine>
65
65
  // elements synchronously, which can trigger context.create() and dispatch
@@ -5,23 +5,23 @@ import { isLocalNetwork } from "./engine_networking_utils.js";
5
5
  import { Context } from "./engine_setup.js";
6
6
  import { SSR } from "./engine_ssr.js";
7
7
  import type { IContext } from "./engine_types.js";
8
- import { getParam } from "./engine_utils.js";
8
+ import { DeviceUtilities, getParam } from "./engine_utils.js";
9
9
 
10
10
  const debug = getParam("__debuglic__");
11
11
 
12
- const _gsLyleQ: ((result: boolean) => void)[] = [];
12
+ const _MYuNVg: ((result: boolean) => void)[] = [];
13
13
 
14
14
  // DO NOT EDIT MANUALLY
15
- let __cOctKuk: string = "";
15
+ let __UoF: string = "";
16
16
  // eslint-disable-next-line prefer-const
17
- let $nKsEYz: string = "";
17
+ let _nMW: string = "";
18
18
 
19
19
  if (debug) {
20
- console.log("License Type: " + __cOctKuk);
21
- if ($nKsEYz) {
22
- console.log("License JWT: " + $nKsEYz);
20
+ console.log("License Type: " + __UoF);
21
+ if (_nMW) {
22
+ console.log("License JWT: " + _nMW);
23
23
  try {
24
- const payload = JSON.parse(atob($nKsEYz.split(".")[1].replace(/-/g, '+').replace(/_/g, '/')));
24
+ const payload = JSON.parse(atob(_nMW.split(".")[1].replace(/-/g, '+').replace(/_/g, '/')));
25
25
  console.log("License JWT payload:", payload);
26
26
  }
27
27
  catch { console.log("License JWT payload: (failed to decode)"); }
@@ -32,8 +32,8 @@ if (debug) {
32
32
  }
33
33
 
34
34
  /** @internal */
35
- export function __dSkNu() {
36
- switch (__cOctKuk) {
35
+ export function _hyjgv() {
36
+ switch (__UoF) {
37
37
  case "pro":
38
38
  case "enterprise":
39
39
  return true;
@@ -42,8 +42,8 @@ export function __dSkNu() {
42
42
  }
43
43
 
44
44
  /** @internal */
45
- export function $uRk() {
46
- switch (__cOctKuk) {
45
+ export function _$apSvAt() {
46
+ switch (__UoF) {
47
47
  case "indie":
48
48
  return true;
49
49
  }
@@ -51,8 +51,8 @@ export function $uRk() {
51
51
  }
52
52
 
53
53
  /** @internal */
54
- export function _ndIg() {
55
- switch (__cOctKuk) {
54
+ export function _KXivjkV() {
55
+ switch (__UoF) {
56
56
  case "edu":
57
57
  return true;
58
58
  }
@@ -60,20 +60,20 @@ export function _ndIg() {
60
60
  }
61
61
 
62
62
  /** @internal */
63
- export function $Mhcqc() {
64
- return __dSkNu() || $uRk() || _ndIg();
63
+ export function Jkdun() {
64
+ return _hyjgv() || _$apSvAt() || _KXivjkV();
65
65
  }
66
66
 
67
67
 
68
68
  /** @internal */
69
- export function _ynyIW(cb: (result: boolean) => void) {
70
- if (__dSkNu() || $uRk() || _ndIg())
69
+ export function qWc(cb: (result: boolean) => void) {
70
+ if (_hyjgv() || _$apSvAt() || _KXivjkV())
71
71
  return cb(true);
72
- _gsLyleQ.push(cb);
72
+ _MYuNVg.push(cb);
73
73
  }
74
74
 
75
- function $hVWu(result: boolean) {
76
- for (const cb of _gsLyleQ) {
75
+ function _$OGB(result: boolean) {
76
+ for (const cb of _MYuNVg) {
77
77
  try {
78
78
  cb(result);
79
79
  }
@@ -88,7 +88,7 @@ function $hVWu(result: boolean) {
88
88
 
89
89
  // ECDSA P-256 public key for verifying license JWTs (verification-only, safe to ship)
90
90
  /* eslint-disable no-secrets/no-secrets -- public key, not a secret */
91
- const $OabY = {
91
+ const $WYAmrK = {
92
92
  kty: "EC",
93
93
  crv: "P-256",
94
94
  x: "A34nyKMjhQYVgzeE4tyLUYdx34TAKogDa7v7PRaO9Lg",
@@ -112,7 +112,7 @@ function base64urlDecode(str: string): Uint8Array {
112
112
  * Verify a JWT license token and return the `type` claim if valid.
113
113
  * Returns null if the JWT is missing, malformed, or has an invalid signature.
114
114
  */
115
- async function $OBR(jwt: string): Promise<string | null> {
115
+ async function _fAd(jwt: string): Promise<string | null> {
116
116
  if (!jwt) return null;
117
117
  try {
118
118
  const parts = jwt.split(".");
@@ -122,7 +122,7 @@ async function $OBR(jwt: string): Promise<string | null> {
122
122
 
123
123
  const key = await crypto.subtle.importKey(
124
124
  "jwk",
125
- $OabY,
125
+ $WYAmrK,
126
126
  { name: "ECDSA", namedCurve: "P-256" },
127
127
  false,
128
128
  ["verify"]
@@ -182,24 +182,24 @@ async function $OBR(jwt: string): Promise<string | null> {
182
182
  }
183
183
 
184
184
  /** Verify the injected JWT and update the license type if valid.
185
- * The engine ONLY trusts the JWT — the plain __cOctKuk string is ignored. */
185
+ * The engine ONLY trusts the JWT — the plain __UoF string is ignored. */
186
186
  let _jwtVerificationPromise: Promise<void> | undefined = undefined;
187
- async function _$DyIqexB(): Promise<void> {
188
- // NOTE: do NOT add an `if (!$nKsEYz) return` guard here.
187
+ async function _mGi(): Promise<void> {
188
+ // NOTE: do NOT add an `if (!_nMW) return` guard here.
189
189
  // esbuild (Vite dependency pre-bundling) would see the variable as constant ""
190
190
  // and tree-shake the entire JWT verification code path.
191
- const jwt = $nKsEYz;
191
+ const jwt = _nMW;
192
192
  // Clear after reading — this reassignment also prevents esbuild from
193
193
  // constant-folding the variable (it now has multiple assignment sites).
194
- $nKsEYz = "";
195
- const verifiedType = await $OBR(jwt);
194
+ _nMW = "";
195
+ const verifiedType = await _fAd(jwt);
196
196
  if (verifiedType) {
197
- __cOctKuk = verifiedType;
197
+ __UoF = verifiedType;
198
198
  if (debug) console.log("License type set from verified JWT: " + verifiedType);
199
- $hVWu($Mhcqc());
199
+ _$OGB(Jkdun());
200
200
  }
201
201
  else {
202
- __cOctKuk = "basic";
202
+ __UoF = "basic";
203
203
  if (debug && jwt) console.warn("JWT verification failed — license reset to basic");
204
204
  }
205
205
  }
@@ -269,12 +269,13 @@ export namespace Telemetry {
269
269
  if (!domElement) domElement = document.querySelector<HTMLElement>("needle-engine");
270
270
  if (!domElement && !context) return false;
271
271
 
272
+ // The no-telemetry attribute opts out of all telemetry for every license tier.
273
+ // NOTE: this does NOT affect license enforcement — the /check call, the "forbidden"
274
+ // overlay and the watermark/logo run on separate, ungated code paths.
272
275
  const attribute = domElement?.getAttribute("no-telemetry");
273
276
  if (attribute === "" || attribute === "true" || attribute === "1") {
274
- if (__cOctKuk === "pro" || __cOctKuk === "enterprise") {
275
- if (debug) console.debug("Telemetry is disabled via no-telemetry attribute");
276
- return false;
277
- }
277
+ if (debug) console.debug("Telemetry is disabled via no-telemetry attribute");
278
+ return false;
278
279
  }
279
280
  return true;
280
281
  }
@@ -350,6 +351,190 @@ export namespace Telemetry {
350
351
  return doFetch(body);
351
352
  }
352
353
 
354
+ /**
355
+ * Reports a scene load result (success/failure + timings) and, on success, a set of
356
+ * non-identifying device capability buckets. Fired once per load.
357
+ * No fingerprinting data (e.g. the unmasked GPU renderer/vendor string) is collected.
358
+ */
359
+ export function reportLoad(context: IContext | null | undefined, info: {
360
+ success: boolean;
361
+ /** number of glTF/glb files loaded */
362
+ files: number;
363
+ /** true if this load replaced an already-loaded scene (e.g. a src swap) rather than a cold start */
364
+ is_reload: boolean;
365
+ /** duration of context.create() in ms */
366
+ load_ms: number;
367
+ /** time from load start to first rendered frame in ms (success path only) */
368
+ first_frame_ms?: number;
369
+ /** time from navigation start to first rendered frame in ms (success path only) */
370
+ since_navigation_ms?: number;
371
+ /** failure category when success is false */
372
+ error?: string;
373
+ }) {
374
+ if (!isAllowed(context)) return;
375
+ const props: Record<string, any> = {
376
+ action: "load",
377
+ success: info.success,
378
+ files: info.files,
379
+ is_reload: info.is_reload,
380
+ load_ms: Math.round(info.load_ms),
381
+ };
382
+ if (info.error) props.error = info.error;
383
+ if (info.first_frame_ms !== undefined) props.first_frame_ms = Math.round(info.first_frame_ms);
384
+ if (info.since_navigation_ms !== undefined) props.since_navigation_ms = Math.round(info.since_navigation_ms);
385
+ if (info.success) collectCapabilities(context, props);
386
+ sendEvent(context, "engine", props);
387
+ }
388
+
389
+ /**
390
+ * Writes non-identifying device capability buckets (graphics API, limits, compressed-texture
391
+ * support, coarse hardware hints) into the given props object. Deliberately omits the unmasked
392
+ * GPU renderer/vendor strings so this cannot be used to fingerprint a device.
393
+ */
394
+ function collectCapabilities(context: IContext | null | undefined, props: Record<string, any>) {
395
+ const renderer = context?.renderer;
396
+ if (!renderer) return;
397
+ try {
398
+ if ((renderer as { isWebGPURenderer?: boolean }).isWebGPURenderer === true) {
399
+ props.api = "webgpu";
400
+ }
401
+ else {
402
+ const gl = renderer.getContext();
403
+ if (gl) {
404
+ const isWebGL2 = typeof WebGL2RenderingContext !== "undefined" && gl instanceof WebGL2RenderingContext;
405
+ props.api = isWebGL2 ? "webgl2" : "webgl";
406
+ props.max_texture_size = gl.getParameter(gl.MAX_TEXTURE_SIZE);
407
+ if (isWebGL2) props.max_samples = gl.getParameter((gl as WebGL2RenderingContext).MAX_SAMPLES);
408
+ const formats: string[] = [];
409
+ if (gl.getExtension("WEBGL_compressed_texture_astc")) formats.push("astc");
410
+ if (gl.getExtension("WEBGL_compressed_texture_etc")) formats.push("etc");
411
+ if (gl.getExtension("WEBGL_compressed_texture_s3tc")) formats.push("s3tc");
412
+ if (gl.getExtension("WEBGL_compressed_texture_pvrtc")) formats.push("pvrtc");
413
+ if (formats.length) props.compression = formats.join(",");
414
+ }
415
+ }
416
+ props.dpr = renderer.getPixelRatio();
417
+ props.is_mobile = DeviceUtilities.isMobileDevice();
418
+ if (typeof navigator !== "undefined") {
419
+ if (navigator.hardwareConcurrency) props.cores = navigator.hardwareConcurrency;
420
+ const deviceMemory = (navigator as { deviceMemory?: number }).deviceMemory;
421
+ if (typeof deviceMemory === "number") props.device_memory = deviceMemory;
422
+ }
423
+ }
424
+ catch (err) {
425
+ // Capability probing must never break a load — telemetry is strictly best-effort.
426
+ if (debug) console.warn("Telemetry: capability probe failed", err);
427
+ }
428
+ }
429
+
430
+ const _contextLossMonitored = new WeakSet<HTMLCanvasElement>();
431
+ /**
432
+ * Attaches WebGL context-loss / -restored listeners to the renderer canvas (once per canvas)
433
+ * and reports them. Context loss is a real-world GPU crash signal the engine had no visibility
434
+ * into before.
435
+ */
436
+ export function attachContextLossMonitor(context: IContext | null | undefined) {
437
+ const canvas = context?.renderer?.domElement;
438
+ if (!canvas) return;
439
+ if (_contextLossMonitored.has(canvas)) return;
440
+ _contextLossMonitored.add(canvas);
441
+ let lostAt = 0;
442
+ canvas.addEventListener("webglcontextlost", () => {
443
+ lostAt = performance.now();
444
+ sendEvent(context, "engine", {
445
+ action: "context_lost",
446
+ since_navigation_ms: Math.round(performance.now()),
447
+ during_xr: context?.isInXR === true,
448
+ });
449
+ });
450
+ canvas.addEventListener("webglcontextrestored", () => {
451
+ sendEvent(context, "engine", {
452
+ action: "context_restored",
453
+ downtime_ms: lostAt ? Math.round(performance.now() - lostAt) : undefined,
454
+ });
455
+ });
456
+ }
457
+
458
+ const _perfMonitored = new WeakSet<object>();
459
+ /**
460
+ * Accumulates a lightweight runtime performance summary (FPS, frame time, peak draw calls /
461
+ * triangles) and reports it once per foreground segment when the page becomes hidden.
462
+ *
463
+ * The per-frame work is intentionally allocation-free: it only reads scalars the engine and
464
+ * three.js already compute every frame (`time.deltaTimeUnscaled`, `renderer.info.render`) and
465
+ * folds them into running accumulators. No GL state is queried in the loop — querying GL state
466
+ * per frame would force a synchronous GPU-CPU stall.
467
+ *
468
+ * Skipped entirely when telemetry is opted out, so opted-out sessions pay zero overhead.
469
+ */
470
+ export function attachPerformanceMonitor(context: IContext | null | undefined) {
471
+ if (!context) return;
472
+ // requires a browser page lifecycle (visibilitychange) to know when to flush
473
+ if (typeof document === "undefined") return;
474
+ if (_perfMonitored.has(context)) return;
475
+ // respect the opt-out at attach time → no per-frame work at all for opted-out sessions
476
+ if (!isAllowed(context)) return;
477
+ _perfMonitored.add(context);
478
+
479
+ let frames = 0;
480
+ let sumMs = 0;
481
+ let maxDt = 0; // worst (longest) frame in seconds → drives min_fps / max_frame_ms
482
+ let peakCalls = 0;
483
+ let peakTriangles = 0;
484
+
485
+ // Hot path: runs every rendered frame. Scalar-only — no allocations, no GL queries.
486
+ // deltaTimeUnscaled is already clamped to <=100ms upstream, so a backgrounded tab can't
487
+ // inflate the worst-frame figure.
488
+ const sample = () => {
489
+ const dt = context.time.deltaTimeUnscaled;
490
+ if (dt <= 0) return;
491
+ frames++;
492
+ sumMs += dt * 1000;
493
+ if (dt > maxDt) maxDt = dt;
494
+ const render = context.renderer?.info?.render;
495
+ if (render) {
496
+ if (render.calls > peakCalls) peakCalls = render.calls;
497
+ if (render.triangles > peakTriangles) peakTriangles = render.triangles;
498
+ }
499
+ };
500
+ context.post_render_callbacks.push(sample);
501
+
502
+ const reset = () => { frames = 0; sumMs = 0; maxDt = 0; peakCalls = 0; peakTriangles = 0; };
503
+ const flush = () => {
504
+ if (frames <= 0) return;
505
+ sendEvent(context, "engine", {
506
+ action: "performance",
507
+ frames,
508
+ duration_ms: Math.round(sumMs),
509
+ avg_fps: Math.round((1000 * frames) / sumMs),
510
+ min_fps: maxDt > 0 ? Math.round(1 / maxDt) : 0,
511
+ avg_frame_ms: Math.round((sumMs / frames) * 10) / 10,
512
+ max_frame_ms: Math.round(maxDt * 1000 * 10) / 10,
513
+ peak_draw_calls: peakCalls,
514
+ peak_triangles: peakTriangles,
515
+ });
516
+ reset();
517
+ };
518
+
519
+ // visibilitychange→hidden is the last reliably-fired lifecycle event on mobile; pagehide is
520
+ // a desktop backup. Flushing per hidden transition keeps payloads tiny and survives bfcache.
521
+ const onVisibility = () => { if (document.visibilityState === "hidden") flush(); };
522
+ document.addEventListener("visibilitychange", onVisibility);
523
+ window.addEventListener("pagehide", flush);
524
+
525
+ // Clean up listeners + the per-frame callback when this context is destroyed (avoids leaks).
526
+ const unregisterDestroyed = ContextRegistry.registerCallback(ContextEvent.ContextDestroyed, evt => {
527
+ if (evt.context !== context) return;
528
+ flush();
529
+ document.removeEventListener("visibilitychange", onVisibility);
530
+ window.removeEventListener("pagehide", flush);
531
+ const idx = context.post_render_callbacks.indexOf(sample);
532
+ if (idx >= 0) context.post_render_callbacks.splice(idx, 1);
533
+ _perfMonitored.delete(context);
534
+ unregisterDestroyed();
535
+ });
536
+ }
537
+
353
538
  function doFetch(body: Record<string, any>) {
354
539
  try {
355
540
  const url = "https://needle.tools/api/v1/rum/t";
@@ -378,32 +563,37 @@ export namespace Telemetry {
378
563
  }
379
564
 
380
565
 
381
- export function _KGv() {
566
+ export function __NjkWq() {
382
567
 
383
- if(__cOctKuk === "") __cOctKuk = "basic";
568
+ if(__UoF === "") __UoF = "basic";
384
569
 
385
570
  // Start JWT verification — must be here (not top-level) to avoid tree-shaking
386
- _jwtVerificationPromise = _$DyIqexB();
571
+ _jwtVerificationPromise = _mGi();
387
572
 
388
573
  Telemetry.init();
389
574
  ContextRegistry.registerCallback(ContextEvent.ContextRegistered, evt => {
390
- zOvk(evt.context);
391
- $QEbT(evt.context);
392
- setTimeout(() => __YfC(evt.context), 2000);
575
+ __BvDhlt(evt.context);
576
+ __CYPgY(evt.context);
577
+ setTimeout(() => _$xMafOx(evt.context), 2000);
578
+ });
579
+ // ContextCreated fires after the renderer exists, so the canvas is available for context-loss monitoring
580
+ ContextRegistry.registerCallback(ContextEvent.ContextCreated, evt => {
581
+ Telemetry.attachContextLossMonitor(evt.context);
582
+ Telemetry.attachPerformanceMonitor(evt.context);
393
583
  });
394
584
  }
395
585
 
396
- export let __gpBcwS: Promise<void> | undefined = undefined;
586
+ export let xWfUNf: Promise<void> | undefined = undefined;
397
587
  let applicationIsForbidden = false;
398
588
  let applicationForbiddenText = "";
399
- async function $nsBUUjAp() {
589
+ async function jfCsRiC() {
400
590
  // Only perform the runtime license check once
401
- if (__gpBcwS) return __gpBcwS;
591
+ if (xWfUNf) return xWfUNf;
402
592
  // Wait for JWT verification to complete first (if running)
403
593
  if (_jwtVerificationPromise) {
404
594
  await _jwtVerificationPromise;
405
595
  }
406
- if (__cOctKuk === "basic") {
596
+ if (__UoF === "basic") {
407
597
  try {
408
598
  const licenseUrl = "https://needle.tools/api/v1/needle-engine/check?location=" + encodeURIComponent(window.location.href) + "&version=" + VERSION + "&generator=" + encodeURIComponent(GENERATOR);
409
599
  const res = await fetch(licenseUrl, {
@@ -415,29 +605,29 @@ async function $nsBUUjAp() {
415
605
  if (res?.status === 200) {
416
606
  applicationIsForbidden = false;
417
607
  if (debug) console.log("License check succeeded");
418
- __cOctKuk = "pro";
419
- $hVWu(true);
608
+ __UoF = "pro";
609
+ _$OGB(true);
420
610
  }
421
611
  else if (res?.status === 403) {
422
- $hVWu(false);
612
+ _$OGB(false);
423
613
  applicationIsForbidden = true;
424
614
  applicationForbiddenText = await res.text();
425
615
  }
426
616
  else {
427
- $hVWu(false);
617
+ _$OGB(false);
428
618
  if (debug) console.log("License check failed with status " + res?.status);
429
619
  }
430
620
  }
431
621
  catch (err) {
432
- $hVWu(false);
622
+ _$OGB(false);
433
623
  if (debug) console.error("License check failed", err);
434
624
  }
435
625
  }
436
- else if (debug) console.log("Runtime license check is skipped because license is already applied as \"" + __cOctKuk + "\"");
626
+ else if (debug) console.log("Runtime license check is skipped because license is already applied as \"" + __UoF + "\"");
437
627
  }
438
- __gpBcwS = $nsBUUjAp();
628
+ xWfUNf = jfCsRiC();
439
629
 
440
- async function $QEbT(ctx: IContext) {
630
+ async function __CYPgY(ctx: IContext) {
441
631
  function createForbiddenElement() {
442
632
  const div = document.createElement("div");
443
633
  div.className = "needle-forbidden";
@@ -502,40 +692,40 @@ async function $QEbT(ctx: IContext) {
502
692
  }, 500)
503
693
  }
504
694
 
505
- async function zOvk(ctx: IContext) {
695
+ async function __BvDhlt(ctx: IContext) {
506
696
  try {
507
- if (!__dSkNu() && !$uRk()) {
508
- return _kSwtWJd(ctx);
697
+ if (!_hyjgv() && !_$apSvAt()) {
698
+ return _HlRd(ctx);
509
699
  }
510
700
  }
511
701
  catch (err) {
512
702
  if (debug) console.log("License check failed", err)
513
- return _kSwtWJd(ctx)
703
+ return _HlRd(ctx)
514
704
  }
515
- if (debug) _kSwtWJd(ctx)
705
+ if (debug) _HlRd(ctx)
516
706
  }
517
707
 
518
708
 
519
709
 
520
- async function _kSwtWJd(ctx: IContext) {
710
+ async function _HlRd(ctx: IContext) {
521
711
 
522
712
  // if the engine loads faster than the license check, we need to capture the ready event here
523
713
  let isReady = false;
524
714
  ctx.domElement.addEventListener("ready", () => isReady = true);
525
715
 
526
- await __gpBcwS?.catch(() => { });
716
+ await xWfUNf?.catch(() => { });
527
717
 
528
718
 
529
- if (__dSkNu() || $uRk()) return;
530
- if ($Mhcqc() === false) _$xDkZm();
719
+ if (_hyjgv() || _$apSvAt()) return;
720
+ if (Jkdun() === false) IUY();
531
721
 
532
722
  // check if the engine is already ready (meaning has finished loading)
533
723
  if (isReady) {
534
- $Vzv(ctx);
724
+ bdcF(ctx);
535
725
  }
536
726
  else {
537
727
  ctx.domElement.addEventListener("ready", () => {
538
- $Vzv(ctx);
728
+ bdcF(ctx);
539
729
  });
540
730
  }
541
731
  }
@@ -543,7 +733,7 @@ async function _kSwtWJd(ctx: IContext) {
543
733
  // const licenseElementIdentifier = "needle-license-element";
544
734
  // const licenseDuration = 10000;
545
735
  // const licenseDelay = 1200;
546
- function $Vzv(ctx: IContext) {
736
+ function bdcF(ctx: IContext) {
547
737
 
548
738
  const style = `
549
739
  position: relative;
@@ -556,7 +746,7 @@ function $Vzv(ctx: IContext) {
556
746
  padding: 10px;
557
747
  padding-left: 30px;
558
748
  `;
559
- if (__cOctKuk === "edu") {
749
+ if (__UoF === "edu") {
560
750
  if (navigator.webdriver) {
561
751
  console.log("This project is supported by Needle for Education – https://needle.tools");
562
752
  }
@@ -613,7 +803,7 @@ function $Vzv(ctx: IContext) {
613
803
  }
614
804
  }, 1000);
615
805
 
616
- if (_ndIg()) {
806
+ if (_KXivjkV()) {
617
807
  const removeDelay = 20_000;
618
808
  setTimeout(() => {
619
809
  clearInterval(interval);
@@ -622,7 +812,7 @@ function $Vzv(ctx: IContext) {
622
812
  const intervalInMinutes = 5;
623
813
  setTimeout(() => {
624
814
  if (ctx.domElement.parentNode)
625
- $Vzv(ctx);
815
+ bdcF(ctx);
626
816
  }, 1000 * 60 * intervalInMinutes)
627
817
  }, removeDelay);
628
818
  }
@@ -633,7 +823,7 @@ function $Vzv(ctx: IContext) {
633
823
  const base64Logo = "data:image/webp;base64,UklGRrABAABXRUJQVlA4WAoAAAAQAAAAHwAAHwAAQUxQSKEAAAARN6CmbSM4WR7vdARON11EBDq3fLiNbVtVzpMCPlKAEzsx0Y/x+Ovuv4dn0EFE/ydAvz6YggXzgh5sVgXM/zOC/4sii7qgGvB5N7hmuQYwkvazWAu1JPW41FXSHq6pnaQWvqYH18Fc0j1hO/BFTtIeSBlJi5w6qIIO7IOrwhFsB2Yxukif0FTRLpXswHR8MxbslKe9VZsn/Ub5C7YFOpqSTABWUDgg6AAAAFAGAJ0BKiAAIAA+7VyoTqmkpCI3+qgBMB2JbACdMt69DwMIQBLhkTO6XwY00UEDK6cNIDnuNibPf0EgAP7Y1myuiQHLDsF/0h5unrGh6WAbv7aegg2ZMd3uRKfT/3SJztcaujYfTvMXspfCTmYcoO6a+vhC3ss4M8uM58t4siiu59I4aOl59e9Sr6xoxYlHf2v+NnBNpJYeJf8jABQAId/PXuBkLEFkiCucgSGEcfhvajql/j3reCGl0M5/9gQWy7ayNPs+wlvIxFnNfSlfuND4CZOCyxOHhRqOmHN4ULHo3tCSrUNvgAA=";
634
824
 
635
825
  let lastLogTime = 0;
636
- async function _$xDkZm(_logo?: string) {
826
+ async function IUY(_logo?: string) {
637
827
  const now = Date.now();
638
828
  if (now - lastLogTime < 2000) return;
639
829
  lastLogTime = now;
@@ -666,7 +856,7 @@ async function _$xDkZm(_logo?: string) {
666
856
  }
667
857
 
668
858
 
669
- async function __YfC(context: IContext) {
859
+ async function _$xMafOx(context: IContext) {
670
860
  // We can't send beacons from cross-origin isolated pages
671
861
  if (window.crossOriginIsolated) return;
672
862
 
@@ -681,7 +871,7 @@ async function __YfC(context: IContext) {
681
871
 
682
872
  // current url without query parameters
683
873
  const currentUrl = window.location.href.split("?")[0];
684
- const license = __cOctKuk;
874
+ const license = __UoF;
685
875
 
686
876
  const beaconData = {
687
877
  license,
@@ -357,6 +357,11 @@ export class OwnershipModel {
357
357
  */
358
358
  public freeOwnership(): OwnershipModel {
359
359
  // TODO: abort "requestOwnershipIfNotOwned"
360
+ // Only the current owner may release ownership. Otherwise a non-owner (e.g. a remote client whose
361
+ // copy of the object gets deactivated) would tell the server to drop the *real* owner's claim,
362
+ // silently breaking sync for everyone.
363
+ if (!this._hasOwnership) return this;
364
+ this._hasOwnership = false;
360
365
  this.connection.send(OwnershipEvent.RemoveOwnership, { guid: this.guid });
361
366
  if (this._isWaitingForOwnershipResponseCallback) {
362
367
  this.connection.stopListen(OwnershipEvent.ResponseHasOwner, this._isWaitingForOwnershipResponseCallback);
@@ -1,7 +1,7 @@
1
1
  import { FileLoader } from "three";
2
2
 
3
3
  import { showBalloonWarning } from "./debug/index.js";
4
- import { $Mhcqc } from "./engine_license.js";
4
+ import { Jkdun } from "./engine_license.js";
5
5
  import { delay } from "./engine_utils.js";
6
6
  import { md5AsBytes, md5Hex, sha256Base64 } from "./engine_utils_hash.js";
7
7
 
@@ -45,7 +45,7 @@ export namespace BlobStorage {
45
45
  */
46
46
  export function canUpload(info: { filesize: number }) {
47
47
  const sizeInMB = info.filesize / 1024 / 1024;
48
- if ($Mhcqc()) {
48
+ if (Jkdun()) {
49
49
  return sizeInMB < maxSizeInMB;
50
50
  }
51
51
  return sizeInMB < maxFreeSizeInMB;
@@ -100,7 +100,7 @@ export namespace BlobStorage {
100
100
  console.warn(`Your file is too large for uploading (${filesizeInMB.toFixed(1)}MB). Max allowed size is ${maxSizeInMB}MB`);
101
101
  return null;
102
102
  }
103
- else if (!$Mhcqc() && filesizeInMB > maxFreeSizeInMB) {
103
+ else if (!Jkdun() && filesizeInMB > maxFreeSizeInMB) {
104
104
  if (opts?.silent !== true) showBalloonWarning(`File is too large for uploading. Please get a <a href=\"https://needle.tools/pricing\" target=\"_blank\">commercial license</a> to upload files larger than 5MB`);
105
105
  console.warn(`Your file is too large for uploading (${filesizeInMB.toFixed(1)}MB). Max size is 5MB for non-commercial users. Please get a commercial license at https://needle.tools/pricing for larger files (up to 50MB)`);
106
106
  return null;