@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.
- package/CHANGELOG.md +6 -0
- package/dist/{needle-engine.bundle-Dv-p5m4z.js → needle-engine.bundle-BAUfRyBA.js} +2861 -2708
- package/dist/{needle-engine.bundle-Cb39lTJn.umd.cjs → needle-engine.bundle-BL2wG5Te.umd.cjs} +101 -101
- package/dist/{needle-engine.bundle-BUT4ua7U.min.js → needle-engine.bundle-BpNRkY3s.min.js} +148 -148
- package/dist/needle-engine.d.ts +18 -6
- package/dist/needle-engine.js +401 -401
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/lib/engine/api.d.ts +1 -1
- package/lib/engine/api.js +1 -1
- package/lib/engine/api.js.map +1 -1
- package/lib/engine/engine_init.js +2 -2
- package/lib/engine/engine_init.js.map +1 -1
- package/lib/engine/engine_license.d.ts +45 -7
- package/lib/engine/engine_license.js +276 -77
- package/lib/engine/engine_license.js.map +1 -1
- package/lib/engine/engine_networking.js +6 -0
- package/lib/engine/engine_networking.js.map +1 -1
- package/lib/engine/engine_networking_blob.js +3 -3
- package/lib/engine/engine_networking_blob.js.map +1 -1
- package/lib/engine/engine_utils_qrcode.js +2 -2
- package/lib/engine/engine_utils_qrcode.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu.js +5 -5
- package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.js +31 -2
- package/lib/engine/webcomponents/needle-engine.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.loading.js +2 -2
- package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
- package/lib/engine/xr/NeedleXRSync.js +23 -4
- package/lib/engine/xr/NeedleXRSync.js.map +1 -1
- package/lib/engine/xr/TempXRContext.js +2 -2
- package/lib/engine/xr/TempXRContext.js.map +1 -1
- package/lib/engine-components/SyncedTransform.d.ts +5 -1
- package/lib/engine-components/SyncedTransform.js +45 -8
- package/lib/engine-components/SyncedTransform.js.map +1 -1
- package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
- package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
- package/lib/engine-components/webxr/Avatar.d.ts +8 -0
- package/lib/engine-components/webxr/Avatar.js +60 -32
- package/lib/engine-components/webxr/Avatar.js.map +1 -1
- package/lib/engine-schemes/synced-transform-model.d.ts +2 -0
- package/lib/engine-schemes/synced-transform-model.js +8 -1
- package/lib/engine-schemes/synced-transform-model.js.map +1 -1
- package/package.json +1 -1
- package/plugins/common/license.js +4 -4
- package/plugins/types/userconfig.d.ts +4 -3
- package/plugins/vite/build-pipeline.d.ts +3 -1
- package/plugins/vite/build-pipeline.js +19 -9
- package/plugins/vite/license.js +4 -4
- package/src/engine/api.ts +1 -1
- package/src/engine/engine_init.ts +2 -2
- package/src/engine/engine_license.ts +263 -73
- package/src/engine/engine_networking.ts +5 -0
- package/src/engine/engine_networking_blob.ts +3 -3
- package/src/engine/engine_utils_qrcode.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu.ts +5 -5
- package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
- package/src/engine/webcomponents/needle-engine.ts +31 -2
- package/src/engine/xr/NeedleXRSync.ts +24 -4
- package/src/engine/xr/TempXRContext.ts +2 -2
- package/src/engine-components/SyncedTransform.ts +49 -10
- package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
- package/src/engine-components/webxr/Avatar.ts +60 -33
- package/src/engine-schemes/synced-transform-model.ts +10 -1
- package/src/engine-schemes/transforms.fbs +1 -0
package/plugins/vite/license.js
CHANGED
|
@@ -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("
|
|
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 = "
|
|
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("
|
|
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 = "
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
12
|
+
const _MYuNVg: ((result: boolean) => void)[] = [];
|
|
13
13
|
|
|
14
14
|
// DO NOT EDIT MANUALLY
|
|
15
|
-
let
|
|
15
|
+
let __UoF: string = "";
|
|
16
16
|
// eslint-disable-next-line prefer-const
|
|
17
|
-
let
|
|
17
|
+
let _nMW: string = "";
|
|
18
18
|
|
|
19
19
|
if (debug) {
|
|
20
|
-
console.log("License Type: " +
|
|
21
|
-
if (
|
|
22
|
-
console.log("License JWT: " +
|
|
20
|
+
console.log("License Type: " + __UoF);
|
|
21
|
+
if (_nMW) {
|
|
22
|
+
console.log("License JWT: " + _nMW);
|
|
23
23
|
try {
|
|
24
|
-
const payload = JSON.parse(atob(
|
|
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
|
|
36
|
-
switch (
|
|
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 $
|
|
46
|
-
switch (
|
|
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
|
|
55
|
-
switch (
|
|
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
|
|
64
|
-
return
|
|
63
|
+
export function Jkdun() {
|
|
64
|
+
return _hyjgv() || _$apSvAt() || _KXivjkV();
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
/** @internal */
|
|
69
|
-
export function
|
|
70
|
-
if (
|
|
69
|
+
export function qWc(cb: (result: boolean) => void) {
|
|
70
|
+
if (_hyjgv() || _$apSvAt() || _KXivjkV())
|
|
71
71
|
return cb(true);
|
|
72
|
-
|
|
72
|
+
_MYuNVg.push(cb);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
function $
|
|
76
|
-
for (const cb of
|
|
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 $
|
|
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
|
|
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
|
-
$
|
|
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
|
|
185
|
+
* The engine ONLY trusts the JWT — the plain __UoF string is ignored. */
|
|
186
186
|
let _jwtVerificationPromise: Promise<void> | undefined = undefined;
|
|
187
|
-
async function
|
|
188
|
-
// NOTE: do NOT add an `if (
|
|
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 =
|
|
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
|
-
|
|
195
|
-
const verifiedType = await
|
|
194
|
+
_nMW = "";
|
|
195
|
+
const verifiedType = await _fAd(jwt);
|
|
196
196
|
if (verifiedType) {
|
|
197
|
-
|
|
197
|
+
__UoF = verifiedType;
|
|
198
198
|
if (debug) console.log("License type set from verified JWT: " + verifiedType);
|
|
199
|
-
$
|
|
199
|
+
_$OGB(Jkdun());
|
|
200
200
|
}
|
|
201
201
|
else {
|
|
202
|
-
|
|
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 (
|
|
275
|
-
|
|
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
|
|
566
|
+
export function __NjkWq() {
|
|
382
567
|
|
|
383
|
-
if(
|
|
568
|
+
if(__UoF === "") __UoF = "basic";
|
|
384
569
|
|
|
385
570
|
// Start JWT verification — must be here (not top-level) to avoid tree-shaking
|
|
386
|
-
_jwtVerificationPromise =
|
|
571
|
+
_jwtVerificationPromise = _mGi();
|
|
387
572
|
|
|
388
573
|
Telemetry.init();
|
|
389
574
|
ContextRegistry.registerCallback(ContextEvent.ContextRegistered, evt => {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
setTimeout(() =>
|
|
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
|
|
586
|
+
export let xWfUNf: Promise<void> | undefined = undefined;
|
|
397
587
|
let applicationIsForbidden = false;
|
|
398
588
|
let applicationForbiddenText = "";
|
|
399
|
-
async function
|
|
589
|
+
async function jfCsRiC() {
|
|
400
590
|
// Only perform the runtime license check once
|
|
401
|
-
if (
|
|
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 (
|
|
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
|
-
|
|
419
|
-
$
|
|
608
|
+
__UoF = "pro";
|
|
609
|
+
_$OGB(true);
|
|
420
610
|
}
|
|
421
611
|
else if (res?.status === 403) {
|
|
422
|
-
$
|
|
612
|
+
_$OGB(false);
|
|
423
613
|
applicationIsForbidden = true;
|
|
424
614
|
applicationForbiddenText = await res.text();
|
|
425
615
|
}
|
|
426
616
|
else {
|
|
427
|
-
$
|
|
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
|
-
$
|
|
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 \"" +
|
|
626
|
+
else if (debug) console.log("Runtime license check is skipped because license is already applied as \"" + __UoF + "\"");
|
|
437
627
|
}
|
|
438
|
-
|
|
628
|
+
xWfUNf = jfCsRiC();
|
|
439
629
|
|
|
440
|
-
async function
|
|
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
|
|
695
|
+
async function __BvDhlt(ctx: IContext) {
|
|
506
696
|
try {
|
|
507
|
-
if (!
|
|
508
|
-
return
|
|
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
|
|
703
|
+
return _HlRd(ctx)
|
|
514
704
|
}
|
|
515
|
-
if (debug)
|
|
705
|
+
if (debug) _HlRd(ctx)
|
|
516
706
|
}
|
|
517
707
|
|
|
518
708
|
|
|
519
709
|
|
|
520
|
-
async function
|
|
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
|
|
716
|
+
await xWfUNf?.catch(() => { });
|
|
527
717
|
|
|
528
718
|
|
|
529
|
-
if (
|
|
530
|
-
if (
|
|
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
|
-
|
|
724
|
+
bdcF(ctx);
|
|
535
725
|
}
|
|
536
726
|
else {
|
|
537
727
|
ctx.domElement.addEventListener("ready", () => {
|
|
538
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 {
|
|
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 (
|
|
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 (
|
|
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;
|