@needle-tools/engine 5.1.0-alpha → 5.1.0-canary.30cc545

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 (166) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/SKILL.md +39 -21
  3. package/components.needle.json +1 -1
  4. package/dist/{gltf-progressive-DJBMx-zB.umd.cjs → gltf-progressive-BmblPzFj.umd.cjs} +4 -4
  5. package/dist/{gltf-progressive-BryRjllq.min.js → gltf-progressive-CN_mbb66.min.js} +2 -2
  6. package/dist/{gltf-progressive-Cl167Vjx.js → gltf-progressive-DUlhxdv4.js} +5 -2
  7. package/dist/{needle-engine.bundle-wM-BWPX9.umd.cjs → needle-engine.bundle-BMlLSACE.umd.cjs} +250 -174
  8. package/dist/{needle-engine.bundle-qDahLTqW.min.js → needle-engine.bundle-BXPPQRer.min.js} +242 -166
  9. package/dist/{needle-engine.bundle-CwhCzjep.js → needle-engine.bundle-d_9mSxN4.js} +12930 -12465
  10. package/dist/needle-engine.d.ts +267 -16
  11. package/dist/needle-engine.js +569 -563
  12. package/dist/needle-engine.min.js +1 -1
  13. package/dist/needle-engine.umd.cjs +1 -1
  14. package/dist/{postprocessing-B_9sKVU7.min.js → postprocessing-B571qGWR.min.js} +34 -34
  15. package/dist/{postprocessing-WDc9WwI3.js → postprocessing-CfrLAbLX.js} +0 -1
  16. package/dist/{postprocessing-B2wb6pzI.umd.cjs → postprocessing-CiGkAeM9.umd.cjs} +17 -17
  17. package/dist/{vendor-CAcsI0eU.js → vendor-BFrMaK9q.js} +8983 -9136
  18. package/dist/vendor-CJmyOrCq.min.js +1116 -0
  19. package/dist/vendor-DkMW3WY4.umd.cjs +1116 -0
  20. package/lib/engine/api.d.ts +12 -0
  21. package/lib/engine/api.js +2 -0
  22. package/lib/engine/api.js.map +1 -1
  23. package/lib/engine/debug/debug_environment.js +1 -1
  24. package/lib/engine/debug/debug_environment.js.map +1 -1
  25. package/lib/engine/engine_application.js +8 -6
  26. package/lib/engine/engine_application.js.map +1 -1
  27. package/lib/engine/engine_components.js +5 -1
  28. package/lib/engine/engine_components.js.map +1 -1
  29. package/lib/engine/engine_constants.js +6 -0
  30. package/lib/engine/engine_constants.js.map +1 -1
  31. package/lib/engine/engine_context.d.ts +25 -0
  32. package/lib/engine/engine_context.js +27 -0
  33. package/lib/engine/engine_context.js.map +1 -1
  34. package/lib/engine/engine_context_registry.js +1 -1
  35. package/lib/engine/engine_context_registry.js.map +1 -1
  36. package/lib/engine/engine_init.js +2 -0
  37. package/lib/engine/engine_init.js.map +1 -1
  38. package/lib/engine/engine_input.d.ts +3 -2
  39. package/lib/engine/engine_input.js +3 -2
  40. package/lib/engine/engine_input.js.map +1 -1
  41. package/lib/engine/engine_license.js +11 -9
  42. package/lib/engine/engine_license.js.map +1 -1
  43. package/lib/engine/engine_networking_blob.d.ts +1 -1
  44. package/lib/engine/engine_networking_blob.js +5 -11
  45. package/lib/engine/engine_networking_blob.js.map +1 -1
  46. package/lib/engine/engine_physics_rapier.d.ts +3 -0
  47. package/lib/engine/engine_physics_rapier.js +13 -10
  48. package/lib/engine/engine_physics_rapier.js.map +1 -1
  49. package/lib/engine/engine_pmrem.js +2 -2
  50. package/lib/engine/engine_pmrem.js.map +1 -1
  51. package/lib/engine/engine_scenedata.d.ts +36 -0
  52. package/lib/engine/engine_scenedata.js +111 -0
  53. package/lib/engine/engine_scenedata.js.map +1 -0
  54. package/lib/engine/engine_ssr.d.ts +16 -0
  55. package/lib/engine/engine_ssr.js +38 -0
  56. package/lib/engine/engine_ssr.js.map +1 -0
  57. package/lib/engine/engine_three_utils.d.ts +14 -7
  58. package/lib/engine/engine_three_utils.js +14 -7
  59. package/lib/engine/engine_three_utils.js.map +1 -1
  60. package/lib/engine/engine_utils.js +4 -2
  61. package/lib/engine/engine_utils.js.map +1 -1
  62. package/lib/engine/engine_utils_hash.d.ts +9 -0
  63. package/lib/engine/engine_utils_hash.js +112 -0
  64. package/lib/engine/engine_utils_hash.js.map +1 -0
  65. package/lib/engine/webcomponents/logo-element.d.ts +2 -1
  66. package/lib/engine/webcomponents/logo-element.js +2 -1
  67. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  68. package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +2 -1
  69. package/lib/engine/webcomponents/needle menu/needle-menu.js +2 -1
  70. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  71. package/lib/engine/webcomponents/needle-button.d.ts +2 -1
  72. package/lib/engine/webcomponents/needle-button.js +2 -1
  73. package/lib/engine/webcomponents/needle-button.js.map +1 -1
  74. package/lib/engine/webcomponents/needle-engine.d.ts +2 -1
  75. package/lib/engine/webcomponents/needle-engine.js +2 -1
  76. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  77. package/lib/engine/xr/NeedleXRSession.d.ts +1 -0
  78. package/lib/engine/xr/NeedleXRSession.js +5 -5
  79. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  80. package/lib/engine/xr/events.d.ts +30 -3
  81. package/lib/engine/xr/events.js +38 -0
  82. package/lib/engine/xr/events.js.map +1 -1
  83. package/lib/engine/xr/init.js +1 -7
  84. package/lib/engine/xr/init.js.map +1 -1
  85. package/lib/engine-components/AnimatorController.d.ts +135 -2
  86. package/lib/engine-components/AnimatorController.js +218 -2
  87. package/lib/engine-components/AnimatorController.js.map +1 -1
  88. package/lib/engine-components/GroundProjection.d.ts +1 -0
  89. package/lib/engine-components/GroundProjection.js +184 -48
  90. package/lib/engine-components/GroundProjection.js.map +1 -1
  91. package/lib/engine-components/RigidBody.js +3 -3
  92. package/lib/engine-components/RigidBody.js.map +1 -1
  93. package/lib/engine-components/SceneSwitcher.js +2 -0
  94. package/lib/engine-components/SceneSwitcher.js.map +1 -1
  95. package/lib/engine-components/api.d.ts +1 -0
  96. package/lib/engine-components/api.js +1 -0
  97. package/lib/engine-components/api.js.map +1 -1
  98. package/lib/engine-components/codegen/components.d.ts +1 -0
  99. package/lib/engine-components/codegen/components.js +1 -0
  100. package/lib/engine-components/codegen/components.js.map +1 -1
  101. package/lib/engine-components/postprocessing/Effects/BloomEffect.d.ts +1 -1
  102. package/lib/engine-components/postprocessing/Effects/Sharpening.js +1 -2
  103. package/lib/engine-components/postprocessing/Effects/Sharpening.js.map +1 -1
  104. package/lib/engine-components/postprocessing/PostProcessingHandler.js +5 -6
  105. package/lib/engine-components/postprocessing/PostProcessingHandler.js.map +1 -1
  106. package/lib/engine-components/web/ScrollFollow.d.ts +0 -1
  107. package/lib/engine-components/web/ScrollFollow.js +3 -2
  108. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  109. package/package.json +6 -5
  110. package/plugins/common/logger.js +42 -19
  111. package/plugins/dts-generator/dts.codegen.js +129 -0
  112. package/plugins/dts-generator/dts.scan.js +71 -0
  113. package/plugins/dts-generator/dts.writer.js +59 -0
  114. package/plugins/dts-generator/glb.discovery.js +162 -0
  115. package/plugins/dts-generator/glb.extractor.js +175 -0
  116. package/plugins/dts-generator/glb.reader.js +114 -0
  117. package/plugins/dts-generator/index.js +36 -0
  118. package/plugins/dts-generator/manifest.types.js +174 -0
  119. package/plugins/types/index.d.ts +2 -1
  120. package/plugins/types/needle-bindings.d.ts +19 -0
  121. package/plugins/types/userconfig.d.ts +9 -2
  122. package/plugins/vite/dts-generator.d.ts +7 -0
  123. package/plugins/vite/dts-generator.js +157 -0
  124. package/plugins/vite/index.d.ts +1 -0
  125. package/plugins/vite/index.js +4 -0
  126. package/plugins/vite/logger.client.js +4 -3
  127. package/plugins/vite/logging.js +2 -2
  128. package/plugins/vite/reload.js +2 -1
  129. package/src/engine/api.ts +15 -1
  130. package/src/engine/debug/debug_environment.ts +1 -1
  131. package/src/engine/engine_application.ts +8 -6
  132. package/src/engine/engine_components.ts +7 -4
  133. package/src/engine/engine_constants.ts +11 -6
  134. package/src/engine/engine_context.ts +29 -0
  135. package/src/engine/engine_context_registry.ts +1 -1
  136. package/src/engine/engine_init.ts +2 -0
  137. package/src/engine/engine_input.ts +3 -2
  138. package/src/engine/engine_license.ts +11 -9
  139. package/src/engine/engine_networking_blob.ts +5 -11
  140. package/src/engine/engine_physics_rapier.ts +14 -12
  141. package/src/engine/engine_pmrem.ts +3 -3
  142. package/src/engine/engine_scenedata.ts +110 -0
  143. package/src/engine/engine_ssr.ts +45 -0
  144. package/src/engine/engine_three_utils.ts +15 -7
  145. package/src/engine/engine_utils.ts +3 -2
  146. package/src/engine/engine_utils_hash.ts +65 -0
  147. package/src/engine/webcomponents/logo-element.ts +2 -1
  148. package/src/engine/webcomponents/needle menu/needle-menu.ts +2 -1
  149. package/src/engine/webcomponents/needle-button.ts +2 -1
  150. package/src/engine/webcomponents/needle-engine.ts +2 -1
  151. package/src/engine/xr/NeedleXRSession.ts +6 -6
  152. package/src/engine/xr/events.ts +44 -1
  153. package/src/engine/xr/init.ts +0 -7
  154. package/src/engine-components/AnimatorController.ts +286 -4
  155. package/src/engine-components/GroundProjection.ts +226 -52
  156. package/src/engine-components/RigidBody.ts +3 -3
  157. package/src/engine-components/SceneSwitcher.ts +1 -0
  158. package/src/engine-components/api.ts +1 -0
  159. package/src/engine-components/codegen/components.ts +1 -0
  160. package/src/engine-components/postprocessing/Effects/BloomEffect.ts +1 -1
  161. package/src/engine-components/postprocessing/Effects/Sharpening.ts +1 -2
  162. package/src/engine-components/postprocessing/PostProcessingHandler.ts +4 -8
  163. package/src/engine-components/web/ScrollFollow.ts +2 -2
  164. package/src/vite-env.d.ts +16 -0
  165. package/dist/vendor-CEM38hLE.umd.cjs +0 -1116
  166. package/dist/vendor-HRlxIBga.min.js +0 -1116
@@ -12,6 +12,7 @@ import { patchLayers } from "./js-extensions/Layers.js";
12
12
  import { initObject3DExtensions } from "./js-extensions/Object3D.js";
13
13
  import { initVectorExtensions } from "./js-extensions/Vector.js";
14
14
  import { initWebComponents } from "./webcomponents/init.js";
15
+ import { initPhysics } from "./engine_physics_rapier.js";
15
16
  import { initXR } from "./xr/init.js";
16
17
 
17
18
  let initialized = false;
@@ -43,5 +44,6 @@ export function initEngine() {
43
44
  initAnimationAutoplay();
44
45
  initSkyboxAttributes();
45
46
  initSceneSwitcherAttributes();
47
+ initPhysics();
46
48
  initXR();
47
49
  }
@@ -1,6 +1,7 @@
1
1
  import { Intersection, Matrix4, Object3D, Ray, Vector2, Vector3 } from 'three';
2
2
 
3
3
  import { showBalloonMessage, showBalloonWarning } from './debug/debug.js';
4
+ import { PointerEventBase, KeyboardEventBase } from './engine_ssr.js';
4
5
  import { Context } from './engine_setup.js';
5
6
  import { getTempVector, getWorldQuaternion } from './engine_three_utils.js';
6
7
  import type { ButtonName, CursorTypeName, IGameObject, IInput, MouseButtonName, Vec2 } from './engine_types.js';
@@ -116,7 +117,7 @@ export declare type NEPointerEventIntersection = Intersection & { event?: NEPoin
116
117
  * @see {@link Input} for the input management system
117
118
  * @see {@link PointerType} for available pointer types
118
119
  */
119
- export class NEPointerEvent extends PointerEvent {
120
+ export class NEPointerEvent extends PointerEventBase {
120
121
 
121
122
  /**
122
123
  * Spatial input data
@@ -242,7 +243,7 @@ export class NEPointerEvent extends PointerEvent {
242
243
  if (debug) console.warn("Stop propagation...", this.pointerId, this.pointerType)
243
244
  }
244
245
  }
245
- export class NEKeyboardEvent extends KeyboardEvent {
246
+ export class NEKeyboardEvent extends KeyboardEventBase {
246
247
  source?: Event
247
248
  constructor(type: InputEvents, source: Event, init: KeyboardEventInit) {
248
249
  super(type, init)
@@ -77,16 +77,18 @@ function invokeLicenseCheckResultChanged(result: boolean) {
77
77
  // #region Telemetry
78
78
  export namespace Telemetry {
79
79
 
80
- window.addEventListener("error", (event: ErrorEvent) => {
81
- sendError(Context.Current, "unhandled_error", event);
82
- });
83
- window.addEventListener("unhandledrejection", (event: PromiseRejectionEvent) => {
84
- sendError(Context.Current, "unhandled_promise_rejection", {
85
- message: event.reason?.message,
86
- stack: event.reason?.stack,
87
- timestamp: Date.now(),
80
+ if (typeof window !== "undefined") {
81
+ window.addEventListener("error", (event: ErrorEvent) => {
82
+ sendError(Context.Current, "unhandled_error", event);
88
83
  });
89
- });
84
+ window.addEventListener("unhandledrejection", (event: PromiseRejectionEvent) => {
85
+ sendError(Context.Current, "unhandled_promise_rejection", {
86
+ message: event.reason?.message,
87
+ stack: event.reason?.stack,
88
+ timestamp: Date.now(),
89
+ });
90
+ });
91
+ }
90
92
 
91
93
  onInitialized((ctx => sendPageViewEvent(ctx)), { once: true });
92
94
 
@@ -1,11 +1,9 @@
1
- import * as _md5 from "md5";
2
- // CJS interop: md5 may appear as { default: fn } or fn depending on bundler
3
- const md5 = typeof _md5 === "function" ? _md5 : (_md5 as any).default;
4
1
  import { FileLoader } from "three";
5
2
 
6
3
  import { showBalloonWarning } from "./debug/index.js";
7
4
  import { hasCommercialLicense } from "./engine_license.js";
8
5
  import { delay } from "./engine_utils.js";
6
+ import { md5Hex, md5AsBytes, sha256Base64 } from "./engine_utils_hash.js";
9
7
 
10
8
 
11
9
  export namespace BlobStorage {
@@ -22,21 +20,17 @@ export namespace BlobStorage {
22
20
  /**
23
21
  * Generates an md5 hash from a given buffer
24
22
  * @param buffer The buffer to hash
25
- * @returns The md5 hash
23
+ * @returns The md5 hash as a hex string
26
24
  */
27
25
  export function hashMD5(buffer: ArrayBuffer): string {
28
- return md5(new Uint8Array(buffer))
26
+ return md5Hex(new Uint8Array(buffer));
29
27
  }
30
28
  export function hashMD5_Base64(buffer: ArrayBuffer): string {
31
- const bytes = md5(new Uint8Array(buffer), { encoding: "binary", asBytes: true });
29
+ const bytes = md5AsBytes(new Uint8Array(buffer));
32
30
  return btoa(String.fromCharCode(...bytes));
33
31
  }
34
32
  export function hashSha256(buffer: ArrayBuffer): Promise<string> {
35
- const bytes = new Uint8Array(buffer);
36
- const hash = crypto.subtle.digest('SHA-256', bytes).then(res => {
37
- return btoa(String.fromCharCode(...new Uint8Array(res)));
38
- })
39
- return hash;
33
+ return sha256Base64(buffer);
40
34
  }
41
35
 
42
36
  export type Upload_Result = {
@@ -44,18 +44,20 @@ const $bodyKey = Symbol("physics body");
44
44
  const $colliderRigidbody = Symbol("rigidbody");
45
45
 
46
46
 
47
- declare const NEEDLE_USE_RAPIER: boolean;
48
- globalThis["NEEDLE_USE_RAPIER"] = globalThis["NEEDLE_USE_RAPIER"] !== undefined ? globalThis["NEEDLE_USE_RAPIER"] : true;
49
- if (debugPhysics)
50
- console.log("Use Rapier", NEEDLE_USE_RAPIER, globalThis["NEEDLE_USE_RAPIER"])
51
-
52
- if (NEEDLE_USE_RAPIER) {
53
- ContextRegistry.registerCallback(ContextEvent.ContextCreationStart, evt => {
54
- if (debugPhysics)
55
- console.log("Register rapier physics backend")
56
- evt.context.physics.engine = new RapierPhysics(evt.context);
57
- // We do not initialize physics immediately to avoid loading the physics engine if it is not needed
58
- });
47
+ /** Register the Rapier physics backend. Called from {@link initEngine}
48
+ * to ensure it runs regardless of tree-shaking. */
49
+ export function initPhysics() {
50
+ if (debugPhysics)
51
+ console.log("Use Rapier", NEEDLE_USE_RAPIER, globalThis["NEEDLE_USE_RAPIER"])
52
+
53
+ if (NEEDLE_USE_RAPIER) {
54
+ ContextRegistry.registerCallback(ContextEvent.ContextCreationStart, evt => {
55
+ if (debugPhysics)
56
+ console.log("Register rapier physics backend")
57
+ evt.context.physics.engine = new RapierPhysics(evt.context);
58
+ // We do not initialize physics immediately to avoid loading the physics engine if it is not needed
59
+ });
60
+ }
59
61
  }
60
62
 
61
63
 
@@ -1,8 +1,8 @@
1
1
  import { createLoaders } from "@needle-tools/gltf-progressive";
2
2
  import { CubeUVReflectionMapping, EquirectangularRefractionMapping, SRGBColorSpace, Texture, TextureLoader, WebGLRenderer } from "three";
3
- import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader";
4
- import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader";
5
- import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
3
+ import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
4
+ import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader.js";
5
+ import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
6
6
 
7
7
  const running: Map<string, Promise<Texture | null>> = new Map();
8
8
 
@@ -0,0 +1,110 @@
1
+ import type { SceneData } from "needle:bindings";
2
+ import type { IContext } from "./engine_types.js";
3
+ import { getComponent } from "./engine_components.js";
4
+ import { TypeStore } from "./engine_typestore.js";
5
+ import { isDevEnvironment } from "./debug/index.js";
6
+ import { ContextRegistry } from "./engine_context_registry.js";
7
+
8
+ /**
9
+ * Global proxy for the primary Needle Engine context.
10
+ * Resolves lazily on property access via `ContextRegistry.Current` —
11
+ * safe to import at module level, including in SSR environments
12
+ * (returns a silent error proxy when no context is active).
13
+ *
14
+ * Use this outside of Needle component lifecycle (e.g. in Svelte/React/Vue
15
+ * components, button handlers, or vanilla JS) instead of threading `ctx` around.
16
+ *
17
+ * For multiple `<needle-engine>` elements on the same page, use `ctx` directly.
18
+ *
19
+ * @example
20
+ * import { needle } from "@needle-tools/engine";
21
+ * button.onclick = () => {
22
+ * needle.sceneData.Camera.OrbitControls.autoRotate = true;
23
+ * };
24
+ */
25
+ export const needle: IContext = new Proxy({} as IContext, {
26
+ get(_target, prop: string) {
27
+ if (prop === "then") return undefined; // not a Promise
28
+ const ctx = ContextRegistry.Current;
29
+ if (!ctx) {
30
+ const fn = isDevEnvironment() ? console.error : console.warn;
31
+ fn(`[needle] needle.${prop} accessed before scene started`);
32
+ return makeErrorProxy(`needle not ready — scene hasn't started yet`);
33
+ }
34
+ const val = (ctx as any)[prop];
35
+ return typeof val === "function" ? val.bind(ctx) : val;
36
+ },
37
+ set(_target, prop: string, value: unknown) {
38
+ const ctx = ContextRegistry.Current;
39
+ if (!ctx) {
40
+ const fn = isDevEnvironment() ? console.error : console.warn;
41
+ fn(`[needle] needle.${prop} set before scene started`);
42
+ return true;
43
+ }
44
+ (ctx as any)[prop] = value;
45
+ return true;
46
+ },
47
+ });
48
+
49
+ const cache = new WeakMap<IContext, SceneData>();
50
+
51
+ /**
52
+ * Returns a proxy that silently absorbs any property get/set and logs a
53
+ * developer-friendly warning. Used when a node or component lookup fails so
54
+ * that chained access like `ctx.sceneData.Foo.Bar.baz = 1` never throws.
55
+ */
56
+ function makeErrorProxy(message: string): object {
57
+ const handler: ProxyHandler<object> = {
58
+ get(_t, prop: string) {
59
+ if (prop === "then") return undefined; // not a Promise
60
+ const fn = isDevEnvironment() ? console.error : console.warn;
61
+ fn(`[SceneData] ${message}`);
62
+ return makeErrorProxy(message);
63
+ },
64
+ set(_t, prop: string) {
65
+ const fn = isDevEnvironment() ? console.error : console.warn;
66
+ fn(`[SceneData] ${message} (tried to set "${prop}")`);
67
+ return true; // suppress TypeError
68
+ },
69
+ };
70
+ return new Proxy({}, handler);
71
+ }
72
+
73
+ /**
74
+ * Returns a lazily-resolved proxy typed as {@link SceneData}.
75
+ * The proxy is cached per context — each context gets exactly one instance.
76
+ *
77
+ * Accessing a property traverses the scene graph on demand:
78
+ * - First level → find Object3D by node name
79
+ * - Second level → getComponent by component name (via TypeStore)
80
+ * - Third level → read/write a field on the real component instance
81
+ *
82
+ * If a node or component is not found, property accesses log a warning
83
+ * instead of throwing, so chained access never crashes.
84
+ *
85
+ * @example
86
+ * ctx.sceneData.Camera.OrbitControls.autoRotate = true;
87
+ */
88
+ export function getSceneData(ctx: IContext): SceneData {
89
+ let proxy = cache.get(ctx);
90
+ if (!proxy) {
91
+ proxy = new Proxy({} as SceneData, {
92
+ get(_target, nodeName: string) {
93
+ return new Proxy({}, {
94
+ get(_target, compName: string) {
95
+ const node = ctx.scene.getObjectByName(nodeName) ?? null;
96
+ if (!node) return makeErrorProxy(`Node "${nodeName}" not found in scene`);
97
+ if (compName === "$node") return node;
98
+ const ctor = TypeStore.get(compName);
99
+ if (!ctor) return makeErrorProxy(`Component type "${compName}" not registered (node "${nodeName}")`);
100
+ const comp = getComponent(node, ctor);
101
+ if (!comp) return makeErrorProxy(`Component "${compName}" not found on node "${nodeName}"`);
102
+ return comp;
103
+ }
104
+ });
105
+ }
106
+ });
107
+ cache.set(ctx, proxy);
108
+ }
109
+ return proxy;
110
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * SSR-safe base classes for browser globals that are not available in Node/SSR environments.
3
+ *
4
+ * Use these instead of extending browser globals directly so that class definitions
5
+ * do not throw a ReferenceError at module evaluation time in SSR/Node contexts
6
+ * (SvelteKit, Next.js, etc.).
7
+ *
8
+ * In browser environments each constant is the real global; in SSR it falls back
9
+ * to a plain empty class so that `class Foo extends HTMLElementBase` is valid.
10
+ */
11
+
12
+
13
+ /** SSR-safe base class for web components. */
14
+ export const HTMLElementBase: typeof HTMLElement =
15
+ typeof HTMLElement !== "undefined" ? HTMLElement : (class { } as unknown as typeof HTMLElement);
16
+
17
+ /** SSR-safe base class for pointer events. */
18
+ export const PointerEventBase: typeof PointerEvent =
19
+ typeof PointerEvent !== "undefined" ? PointerEvent : (class { } as unknown as typeof PointerEvent);
20
+
21
+ /** SSR-safe base class for keyboard events. */
22
+ export const KeyboardEventBase: typeof KeyboardEvent =
23
+ typeof KeyboardEvent !== "undefined" ? KeyboardEvent : (class { } as unknown as typeof KeyboardEvent);
24
+
25
+
26
+
27
+
28
+ // #region minimal polyfills
29
+ // Three.js FileLoader uses ProgressEvent in fetch stream callbacks.
30
+ // It doesn't exist in Node — install a minimal stub so SSR doesn't crash.
31
+ if (typeof globalThis.ProgressEvent === "undefined") {
32
+ (globalThis as any).ProgressEvent = class ProgressEvent {
33
+ readonly type: string;
34
+ readonly lengthComputable: boolean;
35
+ readonly loaded: number;
36
+ readonly total: number;
37
+ constructor(type: string, init?: { lengthComputable?: boolean; loaded?: number; total?: number }) {
38
+ this.type = type;
39
+ this.lengthComputable = init?.lengthComputable ?? false;
40
+ this.loaded = init?.loaded ?? 0;
41
+ this.total = init?.total ?? 0;
42
+ }
43
+ };
44
+ }
45
+ // #endregion
@@ -493,13 +493,21 @@ void main(){
493
493
  * Utility class to perform various graphics operations like copying textures to canvas
494
494
  */
495
495
  export class Graphics {
496
- private static readonly planeGeometry = new PlaneGeometry(2, 2, 1, 1);
497
- private static readonly renderer: WebGLRenderer = new WebGLRenderer({ antialias: false, alpha: true });
498
- private static readonly perspectiveCam = new PerspectiveCamera();
499
- private static readonly orthographicCam = new OrthographicCamera();
500
- private static readonly scene = new Scene();
501
- private static readonly blitMaterial: BlitMaterial = new BlitMaterial();
502
- private static readonly mesh: Mesh = new Mesh(Graphics.planeGeometry, Graphics.blitMaterial);
496
+ private static _planeGeometry: PlaneGeometry | undefined;
497
+ private static _renderer: WebGLRenderer | undefined;
498
+ private static _perspectiveCam: PerspectiveCamera | undefined;
499
+ private static _orthographicCam: OrthographicCamera | undefined;
500
+ private static _scene: Scene | undefined;
501
+ private static _blitMaterial: BlitMaterial | undefined;
502
+ private static _mesh: Mesh | undefined;
503
+
504
+ private static get planeGeometry() { return this._planeGeometry ??= new PlaneGeometry(2, 2, 1, 1); }
505
+ private static get renderer() { return this._renderer ??= new WebGLRenderer({ antialias: false, alpha: true }); }
506
+ private static get perspectiveCam() { return this._perspectiveCam ??= new PerspectiveCamera(); }
507
+ private static get orthographicCam() { return this._orthographicCam ??= new OrthographicCamera(); }
508
+ private static get scene() { return this._scene ??= new Scene(); }
509
+ private static get blitMaterial() { return this._blitMaterial ??= new BlitMaterial(); }
510
+ private static get mesh() { return this._mesh ??= new Mesh(Graphics.planeGeometry, Graphics.blitMaterial); }
503
511
 
504
512
 
505
513
  /**
@@ -184,12 +184,12 @@ export function setOrAddParamsToUrl(url: URLSearchParams, paramName: string, par
184
184
 
185
185
  /** Adds an entry to the browser history. Internally uses `window.history.pushState` */
186
186
  export function pushState(title: string, urlParams: URLSearchParams, state?: any) {
187
- window.history.pushState(state, title, "?" + urlParams.toString());
187
+ window.history.pushState(state, title, "?" + urlParams.toString() + window.location.hash);
188
188
  }
189
189
 
190
190
  /** Replaces the current entry in the browser history. Internally uses `window.history.replaceState` */
191
191
  export function setState(title: string, urlParams: URLSearchParams, state?: any) {
192
- window.history.replaceState(state, title, "?" + urlParams.toString());
192
+ window.history.replaceState(state, title, "?" + urlParams.toString() + window.location.hash);
193
193
  }
194
194
 
195
195
  // for room id
@@ -597,6 +597,7 @@ export namespace DeviceUtilities {
597
597
  /** @returns `true` for MacOS or Windows devices. `false` for Hololens and other headsets. */
598
598
  export function isDesktop() {
599
599
  if (_isDesktop !== undefined) return _isDesktop;
600
+ if (typeof window === "undefined") return _isDesktop = false;
600
601
  const ua = window.navigator.userAgent;
601
602
  const standalone = /Windows|MacOS|Mac OS/.test(ua);
602
603
  const isHololens = /Windows NT/.test(ua) && /Edg/.test(ua) && !/Win64/.test(ua);
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Pure-JS hashing utilities — no external dependencies, works in browser and Node/SSR.
3
+ */
4
+
5
+ // MD5 based on the public-domain RSA Data Security MD5 algorithm.
6
+ function md5Bytes(input: Uint8Array): number[] {
7
+ function safeAdd(x: number, y: number) { const lsw = (x & 0xffff) + (y & 0xffff); return ((x >> 16) + (y >> 16) + (lsw >> 16)) << 16 | lsw & 0xffff; }
8
+ function bitRotateLeft(num: number, cnt: number) { return num << cnt | num >>> 32 - cnt; }
9
+ function md5cmn(q: number, a: number, b: number, x: number, s: number, t: number) { return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b); }
10
+ function md5ff(a: number, b: number, c: number, d: number, x: number, s: number, t: number) { return md5cmn(b & c | ~b & d, a, b, x, s, t); }
11
+ function md5gg(a: number, b: number, c: number, d: number, x: number, s: number, t: number) { return md5cmn(b & d | c & ~d, a, b, x, s, t); }
12
+ function md5hh(a: number, b: number, c: number, d: number, x: number, s: number, t: number) { return md5cmn(b ^ c ^ d, a, b, x, s, t); }
13
+ function md5ii(a: number, b: number, c: number, d: number, x: number, s: number, t: number) { return md5cmn(c ^ (b | ~d), a, b, x, s, t); }
14
+
15
+ const len8 = input.length;
16
+ const padded = new Uint8Array((Math.ceil((len8 + 9) / 64) * 64));
17
+ padded.set(input);
18
+ padded[len8] = 0x80;
19
+ const view = new DataView(padded.buffer);
20
+ view.setUint32(padded.length - 8, len8 << 3, true);
21
+ view.setUint32(padded.length - 4, (len8 / 0x20000000) | 0, true);
22
+
23
+ let a = 0x67452301, b = 0xefcdab89, c = 0x98badcfe, d = 0x10325476;
24
+ for (let i = 0; i < padded.length >> 2; i += 16) {
25
+ const M = (j: number) => view.getInt32((i + j) * 4, true);
26
+ const [oa, ob, oc, od] = [a, b, c, d];
27
+ a = md5ff(a,b,c,d,M(0),7,-680876936); d=md5ff(d,a,b,c,M(1),12,-389564586); c=md5ff(c,d,a,b,M(2),17,606105819); b=md5ff(b,c,d,a,M(3),22,-1044525330);
28
+ a=md5ff(a,b,c,d,M(4),7,-176418897); d=md5ff(d,a,b,c,M(5),12,1200080426); c=md5ff(c,d,a,b,M(6),17,-1473231341); b=md5ff(b,c,d,a,M(7),22,-45705983);
29
+ a=md5ff(a,b,c,d,M(8),7,1770035416); d=md5ff(d,a,b,c,M(9),12,-1958414417); c=md5ff(c,d,a,b,M(10),17,-42063); b=md5ff(b,c,d,a,M(11),22,-1990404162);
30
+ a=md5ff(a,b,c,d,M(12),7,1804603682); d=md5ff(d,a,b,c,M(13),12,-40341101); c=md5ff(c,d,a,b,M(14),17,-1502002290); b=md5ff(b,c,d,a,M(15),22,1236535329);
31
+ a=md5gg(a,b,c,d,M(1),5,-165796510); d=md5gg(d,a,b,c,M(6),9,-1069501632); c=md5gg(c,d,a,b,M(11),14,643717713); b=md5gg(b,c,d,a,M(0),20,-373897302);
32
+ a=md5gg(a,b,c,d,M(5),5,-701558691); d=md5gg(d,a,b,c,M(10),9,38016083); c=md5gg(c,d,a,b,M(15),14,-660478335); b=md5gg(b,c,d,a,M(4),20,-405537848);
33
+ a=md5gg(a,b,c,d,M(9),5,568446438); d=md5gg(d,a,b,c,M(14),9,-1019803690); c=md5gg(c,d,a,b,M(3),14,-187363961); b=md5gg(b,c,d,a,M(8),20,1163531501);
34
+ a=md5gg(a,b,c,d,M(13),5,-1444681467); d=md5gg(d,a,b,c,M(2),9,-51403784); c=md5gg(c,d,a,b,M(7),14,1735328473); b=md5gg(b,c,d,a,M(12),20,-1926607734);
35
+ a=md5hh(a,b,c,d,M(5),4,-378558); d=md5hh(d,a,b,c,M(8),11,-2022574463); c=md5hh(c,d,a,b,M(11),16,1839030562); b=md5hh(b,c,d,a,M(14),23,-35309556);
36
+ a=md5hh(a,b,c,d,M(1),4,-1530992060); d=md5hh(d,a,b,c,M(4),11,1272893353); c=md5hh(c,d,a,b,M(7),16,-155497632); b=md5hh(b,c,d,a,M(10),23,-1094730640);
37
+ a=md5hh(a,b,c,d,M(13),4,681279174); d=md5hh(d,a,b,c,M(0),11,-358537222); c=md5hh(c,d,a,b,M(3),16,-722521979); b=md5hh(b,c,d,a,M(6),23,76029189);
38
+ a=md5hh(a,b,c,d,M(9),4,-640364487); d=md5hh(d,a,b,c,M(12),11,-421815835); c=md5hh(c,d,a,b,M(15),16,530742520); b=md5hh(b,c,d,a,M(2),23,-995338651);
39
+ a=md5ii(a,b,c,d,M(0),6,-198630844); d=md5ii(d,a,b,c,M(7),10,1126891415); c=md5ii(c,d,a,b,M(14),15,-1416354905); b=md5ii(b,c,d,a,M(5),21,-57434055);
40
+ a=md5ii(a,b,c,d,M(12),6,1700485571); d=md5ii(d,a,b,c,M(3),10,-1894986606); c=md5ii(c,d,a,b,M(10),15,-1051523); b=md5ii(b,c,d,a,M(1),21,-2054922799);
41
+ a=md5ii(a,b,c,d,M(8),6,1873313359); d=md5ii(d,a,b,c,M(15),10,-30611744); c=md5ii(c,d,a,b,M(6),15,-1560198380); b=md5ii(b,c,d,a,M(13),21,1309151649);
42
+ a=md5ii(a,b,c,d,M(4),6,-145523070); d=md5ii(d,a,b,c,M(11),10,-1120210379); c=md5ii(c,d,a,b,M(2),15,718787259); b=md5ii(b,c,d,a,M(9),21,-343485551);
43
+ a=safeAdd(a,oa); b=safeAdd(b,ob); c=safeAdd(c,oc); d=safeAdd(d,od);
44
+ }
45
+ const result = new DataView(new ArrayBuffer(16));
46
+ result.setUint32(0, a, true); result.setUint32(4, b, true); result.setUint32(8, c, true); result.setUint32(12, d, true);
47
+ return Array.from(new Uint8Array(result.buffer));
48
+ }
49
+
50
+ /** Returns the MD5 hash of the given bytes as a lowercase hex string. */
51
+ export function md5Hex(input: Uint8Array): string {
52
+ return md5Bytes(input).map(b => b.toString(16).padStart(2, "0")).join("");
53
+ }
54
+
55
+ /** Returns the MD5 hash of the given bytes as a raw byte array. */
56
+ export function md5AsBytes(input: Uint8Array): number[] {
57
+ return md5Bytes(input);
58
+ }
59
+
60
+ /** Returns the SHA-256 hash of the given buffer as a base64 string. */
61
+ export function sha256Base64(buffer: ArrayBuffer): Promise<string> {
62
+ return crypto.subtle.digest("SHA-256", new Uint8Array(buffer)).then(res =>
63
+ btoa(String.fromCharCode(...new Uint8Array(res)))
64
+ );
65
+ }
@@ -1,4 +1,5 @@
1
1
  import { madeWithNeedleSVG, needleLogoOnlySVG, needleLogoSVG } from "../assets/index.js";
2
+ import { HTMLElementBase } from "../engine_ssr.js";
2
3
 
3
4
  const elementName = "needle-logo-element";
4
5
 
@@ -12,7 +13,7 @@ declare global {
12
13
  * Needle logo web component used in the hosting UI (small, compact logo or full)
13
14
  * @element needle-logo-element
14
15
  */
15
- export class NeedleLogoElement extends HTMLElement {
16
+ export class NeedleLogoElement extends HTMLElementBase {
16
17
 
17
18
  static get elementName() { return elementName; }
18
19
 
@@ -1,4 +1,5 @@
1
1
  import { showBalloonMessage } from "../../debug/debug.js";
2
+ import { HTMLElementBase } from "../../engine_ssr.js";
2
3
  import type { Context } from "../../engine_context.js";
3
4
  import { hasCommercialLicense, onLicenseCheckResultChanged, Telemetry } from "../../engine_license.js";
4
5
  import { isLocalNetwork } from "../../engine_networking_utils.js";
@@ -303,7 +304,7 @@ export class NeedleMenu {
303
304
  *
304
305
  * @element needle-menu
305
306
  */
306
- export class NeedleMenuElement extends HTMLElement {
307
+ export class NeedleMenuElement extends HTMLElementBase {
307
308
 
308
309
  static create() {
309
310
  // Ensure the element is registered before creating — guards against
@@ -1,4 +1,5 @@
1
1
  import { isDevEnvironment } from "../debug/index.js";
2
+ import { HTMLElementBase } from "../engine_ssr.js";
2
3
  import { ButtonsFactory } from "./buttons.js";
3
4
  import { iconFontUrl, loadFont } from "./fonts.js";
4
5
  import { WebXRButtonFactory } from "./WebXRButtons.js";
@@ -65,7 +66,7 @@ const isDev = isDevEnvironment();
65
66
  * @see {@link NeedleEngineWebComponent} for the main &lt;needle-engine&gt; element
66
67
  * @see {@link NeedleMenu} for the built-in menu component that can display similar buttons
67
68
  */
68
- export class NeedleButtonElement extends HTMLElement {
69
+ export class NeedleButtonElement extends HTMLElementBase {
69
70
 
70
71
  static observedAttributes = ["ar", "vr", "quicklook", "qrcode"];
71
72
 
@@ -1,4 +1,5 @@
1
1
  import { isDevEnvironment, showBalloonWarning } from "../debug/index.js";
2
+ import { HTMLElementBase } from "../engine_ssr.js";
2
3
  import { PUBLIC_KEY, VERSION } from "../engine_constants.js";
3
4
  import { ContextEvent, ContextRegistry } from "../engine_context_registry.js";
4
5
  import { hasCommercialLicense } from "../engine_license.js";
@@ -153,7 +154,7 @@ const observedAttributes = [
153
154
  * @see {@link NeedleButtonElement} for adding AR/VR/Quicklook buttons via &lt;needle-button&gt;
154
155
  * @see {@link NeedleMenu} for the built-in menu configuration component
155
156
  */
156
- export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngineComponent {
157
+ export class NeedleEngineWebComponent extends HTMLElementBase implements INeedleEngineComponent {
157
158
 
158
159
  static get observedAttributes() {
159
160
  return observedAttributes;
@@ -32,7 +32,7 @@ declare type NeedleXRFrame = XRFrame & { fillPoses?: FillPosesFunction };
32
32
  * Use `args.xr` to access the NeedleXRSession */
33
33
  export type NeedleXREventArgs = { readonly xr: NeedleXRSession }
34
34
  export type SessionChangedEvt = (args: NeedleXREventArgs) => void;
35
- export type SessionRequestedEvent = (args: { readonly mode: XRSessionMode, readonly init: XRSessionInit }) => void;
35
+ export type SessionRequestedEvent = (args: { readonly mode: XRSessionMode, readonly init: XRSessionInit, readonly context: Context }) => void;
36
36
  export type SessionRequestedEndEvent = (args: { readonly mode: XRSessionMode, readonly init: XRSessionInit, newSession: XRSession | null }) => void;
37
37
 
38
38
  /** Result of a XR hit-test
@@ -207,7 +207,7 @@ function waitForContextLoadingFinished(): Promise<void> {
207
207
  }
208
208
 
209
209
 
210
- if (DeviceUtilities.isDesktop() && isDevEnvironment()) {
210
+ if (typeof window !== "undefined" && DeviceUtilities.isDesktop() && isDevEnvironment()) {
211
211
  window.addEventListener("keydown", (evt) => {
212
212
  if (evt.key === "x" || evt.key === "Escape") {
213
213
  if (NeedleXRSession.active) {
@@ -492,7 +492,7 @@ export class NeedleXRSession implements INeedleXRSession {
492
492
 
493
493
  if (!arSupported && (mode === "immersive-ar" || mode === "ar")) {
494
494
 
495
- this.invokeSessionRequestStart("immersive-ar", init);
495
+ this.invokeSessionRequestStart("immersive-ar", init, context ?? Context.Current);
496
496
 
497
497
  // if we are in an iframe, we need to navigate the top window
498
498
  const topWindow = window.top || window;
@@ -662,7 +662,7 @@ export class NeedleXRSession implements INeedleXRSession {
662
662
  script.onBeforeXR(mode, init);
663
663
  }
664
664
  }
665
- this.invokeSessionRequestStart(mode, init);
665
+ this.invokeSessionRequestStart(mode, init, context ?? Context.Current);
666
666
  if (debug) showBalloonMessage("Requesting " + mode + " session (" + Date.now() + ")");
667
667
  Telemetry.sendEvent(Context.Current, "xr", {
668
668
  action: "session_request",
@@ -693,9 +693,9 @@ export class NeedleXRSession implements INeedleXRSession {
693
693
  return session;
694
694
  }
695
695
 
696
- private static invokeSessionRequestStart(mode: XRSessionMode, init: XRSessionInit) {
696
+ private static invokeSessionRequestStart(mode: XRSessionMode, init: XRSessionInit, context: Context) {
697
697
  for (const listener of this._sessionRequestStartListeners) {
698
- listener({ mode, init });
698
+ listener({ mode, init, context });
699
699
  }
700
700
  }
701
701
  private static invokeSessionRequestEnd(mode: XRSessionMode, init: XRSessionInit, session: XRSession | null | undefined | void) {
@@ -1,4 +1,5 @@
1
- import type { NeedleXRSession } from "./NeedleXRSession.js";
1
+ import { NeedleXRSession } from "./NeedleXRSession.js";
2
+ import type { Context } from "../engine_context.js";
2
3
 
3
4
  export declare type XRSessionEventArgs = { session: NeedleXRSession };
4
5
 
@@ -14,11 +15,13 @@ const onXRSessionStartListeners: ((evt: XRSessionEventArgs) => void)[] = [];
14
15
  * console.log("XR session started", evt);
15
16
  * });
16
17
  * ```
18
+ * @returns A function to remove the listener
17
19
  */
18
20
  export function onXRSessionStart(fn: (evt: XRSessionEventArgs) => void) {
19
21
  if (onXRSessionStartListeners.indexOf(fn) === -1) {
20
22
  onXRSessionStartListeners.push(fn);
21
23
  }
24
+ return () => offXRSessionStart(fn);
22
25
  }
23
26
  /**
24
27
  * Remove a listener for when an XR session starts
@@ -51,11 +54,13 @@ const onXRSessionEndListeners: ((evt: XRSessionEventArgs) => void)[] = [];
51
54
  * console.log("XR session ended", evt);
52
55
  * });
53
56
  * ```
57
+ * @returns A function to remove the listener
54
58
  */
55
59
  export function onXRSessionEnd(fn: (evt: XRSessionEventArgs) => void) {
56
60
  if (onXRSessionEndListeners.indexOf(fn) === -1) {
57
61
  onXRSessionEndListeners.push(fn);
58
62
  }
63
+ return () => offXRSessionEnd(fn);
59
64
  };
60
65
 
61
66
  /**
@@ -78,6 +83,44 @@ export function offXRSessionEnd(fn: (evt: XRSessionEventArgs) => void) {
78
83
  }
79
84
 
80
85
 
86
+ export declare type XRSessionRequestEventArgs = { readonly mode: XRSessionMode, readonly init: XRSessionInit, readonly context: Context };
87
+
88
+ /**
89
+ * Add a listener that fires before an XR session is requested.
90
+ * Use this to modify the session init options, e.g. to add optional features like `camera-access`.
91
+ * @param fn The function to call before the XR session is requested
92
+ * @example
93
+ * ```js
94
+ * onBeforeXRSession((args) => {
95
+ * args.init.optionalFeatures ??= [];
96
+ * args.init.optionalFeatures.push("camera-access");
97
+ * });
98
+ * ```
99
+ * @return A function to remove the listener
100
+ */
101
+ export function onBeforeXRSession(fn: (args: XRSessionRequestEventArgs) => void) {
102
+ if (onBeforeXRSessionListeners.indexOf(fn) === -1) {
103
+ onBeforeXRSessionListeners.push(fn);
104
+ // Delegate to NeedleXRSession which owns the actual invocation
105
+ NeedleXRSession.onSessionRequestStart(fn);
106
+ }
107
+ return () => offBeforeXRSession(fn);
108
+ }
109
+
110
+ /**
111
+ * Remove a listener for before an XR session is requested
112
+ * @param fn The function to remove from the listeners
113
+ */
114
+ export function offBeforeXRSession(fn: (args: XRSessionRequestEventArgs) => void) {
115
+ const index = onBeforeXRSessionListeners.indexOf(fn);
116
+ if (index !== -1) {
117
+ onBeforeXRSessionListeners.splice(index, 1);
118
+ NeedleXRSession.offSessionRequestStart(fn);
119
+ }
120
+ }
121
+
122
+ const onBeforeXRSessionListeners: ((args: XRSessionRequestEventArgs) => void)[] = [];
123
+
81
124
  /**
82
125
  * @internal
83
126
  * Invoke the XRSessionStart event
@@ -1,4 +1,3 @@
1
- import { isDevEnvironment, showBalloonMessage } from "../debug/index.js";
2
1
  import { DeviceUtilities, setParamWithoutReload } from "../engine_utils.js";
3
2
  import { NeedleXRSession } from "./NeedleXRSession.js";
4
3
 
@@ -13,12 +12,6 @@ export function initXR() {
13
12
 
14
13
  if (DeviceUtilities.isiOS()) {
15
14
 
16
- if (isDevEnvironment()) {
17
- const randomParameterValue = Date.now().toString();
18
- setParamWithoutReload("debug_appclip", randomParameterValue);
19
- showBalloonMessage("iOS appclip debug: " + randomParameterValue);
20
- }
21
-
22
15
  // Prefetch
23
16
  const url = new URL("https://appclip.apple.com/id?p=tools.needle.launch-app.Clip");
24
17
  url.searchParams.set("url", location.href);