@needle-tools/engine 4.14.0 → 4.15.0-next.f391a30

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 (198) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/components.needle.json +1 -1
  3. package/dist/{gltf-progressive-BttGBXw6.umd.cjs → gltf-progressive-CMwJPwEt.umd.cjs} +1 -1
  4. package/dist/{gltf-progressive-Bm_6aEi4.js → gltf-progressive-CTlvpS3A.js} +1 -1
  5. package/dist/{gltf-progressive-T5WKTux5.min.js → gltf-progressive-DYL3SLVb.min.js} +1 -1
  6. package/dist/materialx-4jJLLe9Q.js +4174 -0
  7. package/dist/materialx-Bt9FHwco.min.js +158 -0
  8. package/dist/materialx-NDD0y4JY.umd.cjs +158 -0
  9. package/dist/{needle-engine.bundle-COL2Bar3.umd.cjs → needle-engine.bundle-C1BFRZDF.umd.cjs} +150 -140
  10. package/dist/{needle-engine.bundle-Z_gAD7Kg.js → needle-engine.bundle-DB4kLWO_.js} +6651 -6400
  11. package/dist/{needle-engine.bundle-NolzHLqO.min.js → needle-engine.bundle-DsTdfmeb.min.js} +151 -141
  12. package/dist/needle-engine.d.ts +345 -88
  13. package/dist/needle-engine.js +322 -322
  14. package/dist/needle-engine.min.js +1 -1
  15. package/dist/needle-engine.umd.cjs +1 -1
  16. package/dist/{postprocessing-06AXuvdv.min.js → postprocessing-BN-f4viE.min.js} +1 -1
  17. package/dist/{postprocessing-CPDcA21P.umd.cjs → postprocessing-DYmYOVm4.umd.cjs} +1 -1
  18. package/dist/{postprocessing-CI2x8Cln.js → postprocessing-De9ZpJrk.js} +1 -1
  19. package/dist/{three-examples-BMmNgNCN.umd.cjs → three-examples-BHqRVpO_.umd.cjs} +12 -12
  20. package/dist/{three-examples-CMYCd5nH.js → three-examples-C0ZCCA_K.js} +182 -192
  21. package/dist/{three-examples-CQl1fFZp.min.js → three-examples-DmTY8tGr.min.js} +14 -14
  22. package/lib/engine/api.d.ts +0 -2
  23. package/lib/engine/api.js +0 -2
  24. package/lib/engine/api.js.map +1 -1
  25. package/lib/engine/debug/debug.js +1 -1
  26. package/lib/engine/debug/debug.js.map +1 -1
  27. package/lib/engine/debug/debug_spatial_console.js +1 -1
  28. package/lib/engine/debug/debug_spatial_console.js.map +1 -1
  29. package/lib/engine/engine_accessibility.d.ts +77 -0
  30. package/lib/engine/engine_accessibility.js +162 -0
  31. package/lib/engine/engine_accessibility.js.map +1 -0
  32. package/lib/engine/engine_context.d.ts +2 -0
  33. package/lib/engine/engine_context.js +8 -1
  34. package/lib/engine/engine_context.js.map +1 -1
  35. package/lib/engine/engine_create_objects.js +1 -1
  36. package/lib/engine/engine_create_objects.js.map +1 -1
  37. package/lib/engine/engine_gizmos.js +1 -1
  38. package/lib/engine/engine_gizmos.js.map +1 -1
  39. package/lib/engine/engine_license.js +7 -2
  40. package/lib/engine/engine_license.js.map +1 -1
  41. package/lib/engine/engine_materialpropertyblock.d.ts +90 -4
  42. package/lib/engine/engine_materialpropertyblock.js +97 -7
  43. package/lib/engine/engine_materialpropertyblock.js.map +1 -1
  44. package/lib/engine/engine_math.d.ts +34 -1
  45. package/lib/engine/engine_math.js +34 -1
  46. package/lib/engine/engine_math.js.map +1 -1
  47. package/lib/engine/engine_networking.js +1 -1
  48. package/lib/engine/engine_networking.js.map +1 -1
  49. package/lib/engine/engine_types.d.ts +2 -0
  50. package/lib/engine/engine_types.js +2 -0
  51. package/lib/engine/engine_types.js.map +1 -1
  52. package/lib/engine/engine_utils.js +2 -2
  53. package/lib/engine/engine_utils.js.map +1 -1
  54. package/lib/engine/export/gltf/EXT_mesh_gpu_instancing_exporter.js.map +1 -0
  55. package/lib/engine/export/gltf/index.js +1 -1
  56. package/lib/engine/export/gltf/index.js.map +1 -1
  57. package/lib/engine/webcomponents/icons.js +3 -0
  58. package/lib/engine/webcomponents/icons.js.map +1 -1
  59. package/lib/engine/webcomponents/logo-element.d.ts +7 -3
  60. package/lib/engine/webcomponents/logo-element.js +21 -1
  61. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  62. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
  63. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
  64. package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +10 -7
  65. package/lib/engine/webcomponents/needle menu/needle-menu.js +14 -4
  66. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  67. package/lib/engine/webcomponents/needle-button.d.ts +37 -11
  68. package/lib/engine/webcomponents/needle-button.js +42 -11
  69. package/lib/engine/webcomponents/needle-button.js.map +1 -1
  70. package/lib/engine/webcomponents/needle-engine.ar-overlay.js +10 -1
  71. package/lib/engine/webcomponents/needle-engine.ar-overlay.js.map +1 -1
  72. package/lib/engine/webcomponents/needle-engine.d.ts +13 -2
  73. package/lib/engine/webcomponents/needle-engine.js +23 -3
  74. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  75. package/lib/engine-components/Component.d.ts +1 -2
  76. package/lib/engine-components/Component.js +1 -3
  77. package/lib/engine-components/Component.js.map +1 -1
  78. package/lib/engine-components/DragControls.d.ts +1 -0
  79. package/lib/engine-components/DragControls.js +21 -0
  80. package/lib/engine-components/DragControls.js.map +1 -1
  81. package/lib/engine-components/NeedleMenu.d.ts +2 -0
  82. package/lib/engine-components/NeedleMenu.js +2 -0
  83. package/lib/engine-components/NeedleMenu.js.map +1 -1
  84. package/lib/engine-components/Networking.d.ts +28 -3
  85. package/lib/engine-components/Networking.js +28 -3
  86. package/lib/engine-components/Networking.js.map +1 -1
  87. package/lib/engine-components/ReflectionProbe.d.ts +25 -2
  88. package/lib/engine-components/ReflectionProbe.js +46 -2
  89. package/lib/engine-components/ReflectionProbe.js.map +1 -1
  90. package/lib/engine-components/Skybox.js +4 -2
  91. package/lib/engine-components/Skybox.js.map +1 -1
  92. package/lib/engine-components/export/gltf/GltfExport.js +1 -1
  93. package/lib/engine-components/export/gltf/GltfExport.js.map +1 -1
  94. package/lib/engine-components/export/usdz/ThreeUSDZExporter.js +2 -2
  95. package/lib/engine-components/export/usdz/USDZExporter.js +1 -1
  96. package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
  97. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.d.ts +15 -0
  98. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js +77 -0
  99. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js.map +1 -1
  100. package/lib/engine-components/export/usdz/extensions/behavior/PhysicsExtension.js +2 -2
  101. package/lib/engine-components/export/usdz/extensions/behavior/PhysicsExtension.js.map +1 -1
  102. package/lib/engine-components/postprocessing/Effects/Tonemapping.utils.d.ts +1 -1
  103. package/lib/engine-components/ui/Button.d.ts +1 -0
  104. package/lib/engine-components/ui/Button.js +11 -0
  105. package/lib/engine-components/ui/Button.js.map +1 -1
  106. package/lib/engine-components/ui/Text.d.ts +1 -0
  107. package/lib/engine-components/ui/Text.js +11 -0
  108. package/lib/engine-components/ui/Text.js.map +1 -1
  109. package/package.json +18 -14
  110. package/plugins/common/buildinfo.js +46 -10
  111. package/plugins/common/files.js +2 -1
  112. package/plugins/common/license.js +144 -69
  113. package/plugins/common/logger.js +172 -11
  114. package/plugins/common/needle-engine-skill.md +175 -0
  115. package/plugins/common/worker.js +5 -4
  116. package/plugins/types/userconfig.d.ts +40 -2
  117. package/plugins/vite/ai.js +71 -0
  118. package/plugins/vite/alias.js +6 -5
  119. package/plugins/vite/asap.js +6 -5
  120. package/plugins/vite/build-pipeline.js +224 -41
  121. package/plugins/vite/buildinfo.js +66 -6
  122. package/plugins/vite/copyfiles.js +41 -12
  123. package/plugins/vite/custom-element-data.js +26 -16
  124. package/plugins/vite/defines.js +8 -5
  125. package/plugins/vite/dependencies.js +16 -10
  126. package/plugins/vite/dependency-watcher.js +35 -7
  127. package/plugins/vite/drop-client.js +7 -5
  128. package/plugins/vite/drop.js +16 -14
  129. package/plugins/vite/editor-connection.js +18 -16
  130. package/plugins/vite/imports-logger.js +12 -2
  131. package/plugins/vite/index.js +8 -3
  132. package/plugins/vite/local-files-analysis.js +789 -0
  133. package/plugins/vite/local-files-core.js +992 -0
  134. package/plugins/vite/local-files-internals.js +28 -0
  135. package/plugins/vite/local-files-types.d.ts +111 -0
  136. package/plugins/vite/local-files-utils.js +359 -0
  137. package/plugins/vite/local-files.js +2 -441
  138. package/plugins/vite/logger.client.js +45 -35
  139. package/plugins/vite/logger.js +6 -3
  140. package/plugins/vite/logging.js +129 -0
  141. package/plugins/vite/meta.js +18 -4
  142. package/plugins/vite/needle-app.js +4 -3
  143. package/plugins/vite/peer.js +2 -1
  144. package/plugins/vite/pwa.js +33 -17
  145. package/plugins/vite/reload.js +24 -2
  146. package/src/engine/api.ts +0 -3
  147. package/src/engine/debug/debug.ts +1 -1
  148. package/src/engine/debug/debug_spatial_console.ts +5 -1
  149. package/src/engine/engine_accessibility.ts +198 -0
  150. package/src/engine/engine_context.ts +10 -1
  151. package/src/engine/engine_create_objects.ts +1 -1
  152. package/src/engine/engine_gizmos.ts +9 -5
  153. package/src/engine/engine_license.ts +7 -2
  154. package/src/engine/engine_materialpropertyblock.ts +102 -11
  155. package/src/engine/engine_math.ts +34 -1
  156. package/src/engine/engine_networking.ts +1 -1
  157. package/src/engine/engine_types.ts +5 -0
  158. package/src/engine/engine_utils.ts +2 -2
  159. package/src/engine/export/gltf/index.ts +1 -1
  160. package/src/engine/webcomponents/icons.ts +3 -0
  161. package/src/engine/webcomponents/logo-element.ts +24 -4
  162. package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +6 -2
  163. package/src/engine/webcomponents/needle menu/needle-menu.ts +23 -11
  164. package/src/engine/webcomponents/needle-button.ts +44 -13
  165. package/src/engine/webcomponents/needle-engine.ar-overlay.ts +13 -2
  166. package/src/engine/webcomponents/needle-engine.ts +31 -8
  167. package/src/engine-components/Component.ts +2 -5
  168. package/src/engine-components/DragControls.ts +29 -4
  169. package/src/engine-components/NeedleMenu.ts +5 -3
  170. package/src/engine-components/Networking.ts +29 -4
  171. package/src/engine-components/ReflectionProbe.ts +52 -9
  172. package/src/engine-components/Skybox.ts +4 -2
  173. package/src/engine-components/export/gltf/GltfExport.ts +1 -1
  174. package/src/engine-components/export/usdz/ThreeUSDZExporter.ts +2 -2
  175. package/src/engine-components/export/usdz/USDZExporter.ts +1 -1
  176. package/src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +108 -32
  177. package/src/engine-components/export/usdz/extensions/behavior/PhysicsExtension.ts +2 -2
  178. package/src/engine-components/ui/Button.ts +12 -0
  179. package/src/engine-components/ui/Text.ts +13 -0
  180. package/dist/materialx-CJyQZtjt.min.js +0 -90
  181. package/dist/materialx-DMs1E08Z.js +0 -4636
  182. package/dist/materialx-DaKKOoVk.umd.cjs +0 -90
  183. package/lib/engine/engine_test_utils.d.ts +0 -39
  184. package/lib/engine/engine_test_utils.js +0 -84
  185. package/lib/engine/engine_test_utils.js.map +0 -1
  186. package/lib/include/three/EXT_mesh_gpu_instancing_exporter.js.map +0 -1
  187. package/src/engine/engine_test_utils.ts +0 -109
  188. package/src/include/draco/draco_decoder.js +0 -34
  189. package/src/include/draco/draco_decoder.wasm +0 -0
  190. package/src/include/draco/draco_wasm_wrapper.js +0 -117
  191. package/src/include/ktx2/basis_transcoder.js +0 -19
  192. package/src/include/ktx2/basis_transcoder.wasm +0 -0
  193. package/src/include/needle/arial-msdf.json +0 -1472
  194. package/src/include/needle/arial.png +0 -0
  195. package/src/include/needle/poweredbyneedle.webp +0 -0
  196. /package/lib/{include/three → engine/export/gltf}/EXT_mesh_gpu_instancing_exporter.d.ts +0 -0
  197. /package/lib/{include/three → engine/export/gltf}/EXT_mesh_gpu_instancing_exporter.js +0 -0
  198. /package/src/{include/three → engine/export/gltf}/EXT_mesh_gpu_instancing_exporter.js +0 -0
@@ -0,0 +1,198 @@
1
+ import { Object3D } from "three";
2
+
3
+ import type { Context } from "./engine_setup";
4
+ import { IComponent, isComponent } from "./engine_types.js";
5
+
6
+
7
+
8
+ /** Data describing the accessible semantics for a 3D object or component. */
9
+ type AccessibilityData = {
10
+ /** ARIA role (e.g. `"button"`, `"img"`, `"region"`). */
11
+ role: string;
12
+ /** Human-readable label announced by screen readers. */
13
+ label: string;
14
+ /** When `true`, the element is hidden from the accessibility tree. */
15
+ hidden?: boolean;
16
+ /** When `true`, indicates the element's content is being updated. */
17
+ busy?: boolean;
18
+ }
19
+
20
+ /**
21
+ * Manages an accessible, screen-reader-friendly overlay for a Needle Engine {@link Context}.
22
+ *
23
+ * The manager maintains a visually-hidden DOM tree that mirrors relevant 3D scene objects
24
+ * with appropriate ARIA roles and labels. It also provides a live region so that hover
25
+ * events in the 3D scene can be announced to assistive technology without stealing focus.
26
+ *
27
+ * ## Automatic integration
28
+ * Several built-in components register accessible elements automatically:
29
+ * - {@link DragControls} — announces draggable objects and drag state
30
+ * - {@link Button} — exposes UI buttons to the accessibility tree
31
+ * - {@link Text} — exposes UI text content to screen readers
32
+ * - {@link ChangeTransformOnClick} — announces clickable transform actions
33
+ * - {@link ChangeMaterialOnClick} — announces clickable material changes
34
+ * - {@link EmphasizeOnClick} — announces clickable emphasis effects
35
+ * - {@link PlayAudioOnClick} — announces clickable audio playback
36
+ * - {@link PlayAnimationOnClick} — announces clickable animation triggers
37
+ *
38
+ * ## What this unlocks
39
+ * - Hovering over buttons and interactive objects with the cursor announces them to screen readers via an ARIA live region — no focus steal required
40
+ * - Screen readers can discover and navigate interactive 3D objects in the scene
41
+ * - Drag operations update the accessibility state (busy, label changes) in real time
42
+ * - Custom components can participate by calling {@link updateElement}, {@link focus}, and {@link hover}
43
+ *
44
+ * Access the manager via `this.context.accessibility` from any component.
45
+ */
46
+ export class AccessibilityManager {
47
+
48
+ private static readonly _managers: WeakMap<object, AccessibilityManager> = new WeakMap();
49
+
50
+ /** Returns the {@link AccessibilityManager} associated with the given context or component. */
51
+ static get(obj: Context | IComponent) {
52
+ if (isComponent(obj)) {
53
+ return this._managers.get(obj.context);
54
+ }
55
+ else {
56
+ return this._managers.get(obj);
57
+ }
58
+ }
59
+
60
+ constructor(
61
+ private readonly context: Context
62
+ ) {
63
+ this.root.style.cssText = `
64
+ position: absolute;
65
+ width: 1px; height: 1px;
66
+ padding: 0; margin: -1px;
67
+ overflow: hidden;
68
+ clip: rect(0, 0, 0, 0);
69
+ white-space: nowrap;
70
+ border: 0;
71
+ `
72
+ this.root.setAttribute("role", "region");
73
+ this.root.setAttribute("aria-label", "3D Needle Engine scene");
74
+
75
+ // Live region for announcing hovered 3D elements without stealing focus
76
+ this.liveRegion.setAttribute("aria-live", "polite");
77
+ this.liveRegion.setAttribute("aria-atomic", "true");
78
+ this.liveRegion.setAttribute("role", "status");
79
+ this.root.appendChild(this.liveRegion);
80
+ this.enabled = true;
81
+ }
82
+
83
+ private _enabled!: boolean;
84
+ /** Enables or disables the accessibility overlay. When disabled, the overlay DOM is removed. */
85
+ set enabled(value: boolean) {
86
+ if (value === this._enabled) return;
87
+ this._enabled = value;
88
+ if (!value) {
89
+ this.root.remove();
90
+ }
91
+ else {
92
+ AccessibilityManager._managers.set(this.context, this);
93
+ const target = this.context.domElement.shadowRoot || this.context.domElement;
94
+ target.prepend(this.root);
95
+ }
96
+ }
97
+
98
+ /** Removes all tracked accessibility elements, keeping only the live region. */
99
+ clear() {
100
+ this.root.childNodes.forEach(child => child.remove());
101
+ this.root.appendChild(this.liveRegion);
102
+ }
103
+
104
+ /** Removes the overlay from the DOM and unregisters this manager from the context. */
105
+ dispose() {
106
+ this.root.remove();
107
+ AccessibilityManager._managers.delete(this.context);
108
+ }
109
+
110
+ private readonly root: HTMLElement = document.createElement("div");
111
+ private readonly liveRegion: HTMLElement = document.createElement("div");
112
+ private readonly treeElements = new WeakMap<object, HTMLElement>();
113
+
114
+ /**
115
+ * Creates or updates the accessible DOM element for a 3D object or component.
116
+ * @param obj - The scene object or component to represent.
117
+ * @param data - Partial accessibility data (role, label, hidden, busy) to apply.
118
+ */
119
+ updateElement<T extends Object3D | IComponent>(obj: T, data: Partial<AccessibilityData>) {
120
+ let el = this.treeElements.get(obj);
121
+ if (!el) {
122
+ el = document.createElement("div");
123
+ this.treeElements.set(obj, el);
124
+ this.root.appendChild(el);
125
+
126
+
127
+ let didSetRole = false;
128
+
129
+ if (typeof data === "object") {
130
+ if (data.role) {
131
+ didSetRole = true;
132
+ el.setAttribute("role", data.role);
133
+ }
134
+ if (data.label) {
135
+ el.setAttribute("aria-label", data.label);
136
+ }
137
+ if (data.hidden !== undefined) {
138
+ el.setAttribute("aria-hidden", String(data.hidden));
139
+ }
140
+ if (data.busy !== undefined) {
141
+ el.setAttribute("aria-busy", String(data.busy));
142
+ }
143
+ }
144
+
145
+
146
+ // if (didSetRole) {
147
+ // const role = el.getAttribute("role");
148
+ // if (role) {
149
+ // el.setAttribute("tabindex", "0");
150
+ // } else {
151
+ // el.removeAttribute("tabindex");
152
+ // }
153
+ // }
154
+
155
+ }
156
+ }
157
+
158
+ /** Moves keyboard focus to the accessible element representing the given object. */
159
+ focus<T extends Object3D | IComponent>(obj: T) {
160
+ const el = this.treeElements.get(obj);
161
+ if (el) {
162
+ // if (!el.hasAttribute("tabindex")) {
163
+ // el.setAttribute("tabindex", "0");
164
+ // }
165
+ el.focus();
166
+ }
167
+ }
168
+ /** Removes keyboard focus from the accessible element representing the given object. */
169
+ unfocus<T extends Object3D | IComponent>(obj: T) {
170
+ const el = this.treeElements.get(obj);
171
+ if (el) {
172
+ el.blur();
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Announces a hover event to screen readers via the ARIA live region.
178
+ * @param obj - The hovered object (used to look up its label if `text` is not provided).
179
+ * @param text - Optional text to announce. Falls back to the element's `aria-label`.
180
+ */
181
+ hover<T extends Object3D | IComponent>(obj: T, text?: string) {
182
+ const el = this.treeElements.get(obj);
183
+ // Update the live region text — screen reader announces this without stealing focus
184
+ this.liveRegion.textContent = text || el?.getAttribute("aria-label") || "";
185
+ }
186
+
187
+ /** Removes the accessible DOM element for the given object and stops tracking it. */
188
+ removeElement(obj: Object3D | IComponent) {
189
+ const el = this.treeElements.get(obj);
190
+ el?.remove();
191
+ this.treeElements.delete(obj);
192
+ }
193
+
194
+ private set liveRegionMode(mode: "polite" | "assertive") {
195
+ this.liveRegion.setAttribute("aria-live", mode);
196
+ }
197
+
198
+ }
@@ -15,6 +15,7 @@ import { nodeFrame } from "three/examples/jsm/renderers/webgl-legacy/nodes/WebGL
15
15
 
16
16
  import { initSpectorIfAvailable } from './debug/debug_spector.js';
17
17
  import { isDevEnvironment, LogType, showBalloonError, showBalloonMessage } from './debug/index.js';
18
+ import { AccessibilityManager } from './engine_accessibility.js';
18
19
  import { Addressables } from './engine_addressables.js';
19
20
  import { AnimationsRegistry } from './engine_animation.js';
20
21
  import { Application } from './engine_application.js';
@@ -522,6 +523,8 @@ export class Context implements IContext {
522
523
  /** Access the needle menu to add or remove buttons to the menu element */
523
524
  readonly menu: NeedleMenu;
524
525
 
526
+ readonly accessibility: AccessibilityManager;
527
+
525
528
  /**
526
529
  * Checks if the context is fully created and ready
527
530
  * @returns true if the context is fully created and ready
@@ -562,7 +565,7 @@ export class Context implements IContext {
562
565
  this.input = new Input(this);
563
566
  this.physics = new Physics(this);
564
567
  this.connection = new NetworkConnection(this);
565
- // eslint-disable-next-line deprecation/deprecation
568
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
566
569
  this.assets = new AssetDatabase();
567
570
  this.sceneLighting = new SceneLighting(this);
568
571
  this.addressables = new Addressables(this);
@@ -571,6 +574,7 @@ export class Context implements IContext {
571
574
  this.menu = new NeedleMenu(this);
572
575
  this.lodsManager = new LODsManager(this);
573
576
  this.animations = new AnimationsRegistry(this);
577
+ this.accessibility = new AccessibilityManager(this);
574
578
 
575
579
 
576
580
  const resizeCallback = () => this._needsUpdateSize = true;
@@ -614,6 +618,9 @@ export class Context implements IContext {
614
618
 
615
619
  this.renderer = new WebGLRenderer(params);
616
620
 
621
+ this.renderer.domElement.setAttribute("aria-label", "3D rendering");
622
+ this.renderer.domElement.setAttribute("role", "img");
623
+
617
624
  this.renderer.debug.checkShaderErrors = isDevEnvironment() || getParam("checkshadererrors") === true;
618
625
 
619
626
  // some tonemapping other than "NONE" is required for adjusting exposure with EXR environments
@@ -810,6 +817,7 @@ export class Context implements IContext {
810
817
  this.lightmaps?.clear();
811
818
  this.physics?.engine?.clearCaches();
812
819
  this.lodsManager.disable();
820
+ this.accessibility?.clear();
813
821
 
814
822
  this._onBeforeRenderListeners.clear();
815
823
  this._onAfterRenderListeners.clear();
@@ -831,6 +839,7 @@ export class Context implements IContext {
831
839
  */
832
840
  dispose() {
833
841
  this.internalOnDestroy();
842
+ this.accessibility.dispose();
834
843
  }
835
844
 
836
845
  /**@deprecated use dispose() */
@@ -338,7 +338,7 @@ function loadFont(family: string | null): Font | Promise<Font> {
338
338
  url = "https://cdn.needle.tools/static/fonts/facetype/Open Sans_Regular_ascii.json";
339
339
  break;
340
340
  case "Helvetiker":
341
- url = "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/fonts/helvetiker_regular.typeface.json";
341
+ url = "https://cdn.needle.tools/static/fonts/facetype/Helvetiker_Regular_ascii.json";
342
342
  break;
343
343
  }
344
344
  if (fontsDict.has(url)) {
@@ -1,10 +1,10 @@
1
- import { AxesHelper, Box3, BoxGeometry, BufferAttribute, BufferGeometry, Color, type ColorRepresentation, CylinderGeometry, EdgesGeometry, Line, LineBasicMaterial, LineSegments, Material, Matrix4, Mesh, MeshBasicMaterial, Object3D, Quaternion, SphereGeometry, Vector3 } from 'three';
2
- import ThreeMeshUI, { Inline, Text } from "three-mesh-ui"
1
+ import { Box3, BoxGeometry, BufferAttribute, BufferGeometry, Color, type ColorRepresentation, CylinderGeometry, EdgesGeometry, Line, LineBasicMaterial, LineSegments, Material, Matrix4, Mesh, MeshBasicMaterial, Object3D, Quaternion, SphereGeometry, Vector3 } from 'three';
2
+ import ThreeMeshUI, { Text } from "three-mesh-ui"
3
3
  import { type Options } from 'three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js';
4
4
 
5
5
  import { isDestroyed } from './engine_gameobject.js';
6
- import { Context, FrameEvent } from './engine_setup.js';
7
- import { getTempVector, getWorldPosition, lookAtObject, setWorldPositionXYZ } from './engine_three_utils.js';
6
+ import { Context } from './engine_setup.js';
7
+ import { getTempVector, lookAtObject, setWorldPositionXYZ } from './engine_three_utils.js';
8
8
  import type { Vec3, Vec4 } from './engine_types.js';
9
9
  import { getParam } from './engine_utils.js';
10
10
  import { NeedleXRSession } from './engine_xr.js';
@@ -341,7 +341,11 @@ class Internal {
341
341
 
342
342
  if (!fontFamily) {
343
343
  fontFamily = ThreeMeshUI.FontLibrary.addFontFamily(this.familyName);
344
- const variant = fontFamily.addVariant("normal", "normal", "https://uploads.needle.tools/include/font-msdf.json", "https://uploads.needle.tools/include/font.png") as any as ThreeMeshUI.FontVariant;
344
+ const variant = fontFamily.addVariant(
345
+ "normal",
346
+ "normal",
347
+ "https://cdn.needle.tools/static/fonts/msdf/arial/arial-msdf.json",
348
+ "https://cdn.needle.tools/static/fonts/msdf/arial/arial.png");
345
349
  /** @ts-ignore */
346
350
  variant?.addEventListener('ready', () => {
347
351
  ThreeMeshUI.update();
@@ -407,7 +407,12 @@ function insertNonCommercialUseHint(ctx: IContext) {
407
407
  padding-left: 30px;
408
408
  `;
409
409
  if (NEEDLE_ENGINE_LICENSE_TYPE === "edu") {
410
- console.log("%c " + "This project is supported by Needle for Education – https://needle.tools", style);
410
+ if (navigator.webdriver) {
411
+ console.log("This project is supported by Needle for Education – https://needle.tools");
412
+ }
413
+ else {
414
+ console.log("%c " + "This project is supported by Needle for Education – https://needle.tools", style);
415
+ }
411
416
  }
412
417
  else {
413
418
  // if the user has a basic license we already show the logo in the menu and log a license message
@@ -502,7 +507,7 @@ async function logNonCommercialUse(_logo?: string) {
502
507
  // url must contain https for firefox to make it clickable
503
508
  const version = VERSION;
504
509
  const licenseText = `Needle Engine — No license active, commercial use is not allowed. Visit https://needle.tools/pricing for more information and licensing options! v${version}`;
505
- if (Context.Current?.xr) {
510
+ if (Context.Current?.xr || navigator.webdriver) {
506
511
  console.log(licenseText);
507
512
  }
508
513
  else {
@@ -122,17 +122,26 @@ class PropertyBlockRegistry {
122
122
  const registry = new PropertyBlockRegistry();
123
123
 
124
124
  /**
125
- * MaterialPropertyBlock allows per-object material property overrides without creating new material instances.
126
- * This is useful for rendering multiple objects with the same base material but different properties
127
- * (e.g., different colors, textures, or shader parameters).
125
+ * MaterialPropertyBlock allows per-object material property overrides without creating new material instances.
126
+ * This is useful for rendering multiple objects with the same base material but different properties
127
+ * (e.g., different colors, textures, or shader parameters).
128
128
  *
129
- * The property block system works by:
129
+ * ## How Property Blocks Work
130
+ *
131
+ * **Important**: Overrides are registered on the **Object3D**, not on the material.
132
+ * This means:
133
+ * - If you change the object's material, the overrides will still be applied to the new material
134
+ * - Multiple objects can share the same material but have different property overrides
135
+ * - If you don't want overrides applied after changing a material, you must remove them using {@link removeOveride}, {@link clearAllOverrides}, or {@link dispose}
136
+ *
137
+ * The property block system works by:
130
138
  * - Temporarily applying overrides in onBeforeRender
131
139
  * - Restoring original values in onAfterRender
132
140
  * - Managing shader defines and program cache keys for correct shader compilation
133
141
  * - Supporting texture coordinate transforms per object
134
142
  *
135
- * Common use cases:
143
+ * ## Common Use Cases
144
+ *
136
145
  * - **Lightmaps**: Apply unique lightmap textures to individual objects sharing the same material
137
146
  * - **Reflection Probes**: Apply different environment maps per object for localized reflections
138
147
  * - **See-through effects**: Temporarily override transparency/transmission properties for X-ray effects
@@ -170,6 +179,26 @@ const registry = new PropertyBlockRegistry();
170
179
  * block.setDefine("USE_CUSTOM_FEATURE", 1);
171
180
  * ```
172
181
  *
182
+ * @example Material swapping behavior
183
+ * ```typescript
184
+ * const mesh = new Mesh(geometry, materialA);
185
+ * const block = MaterialPropertyBlock.get(mesh);
186
+ * block.setOverride("color", new Color(1, 0, 0));
187
+ *
188
+ * // The color override is red for materialA
189
+ *
190
+ * // Swap the material - overrides persist and apply to the new material!
191
+ * mesh.material = materialB;
192
+ * // The color override is now red for materialB too
193
+ *
194
+ * // If you don't want overrides on the new material, remove them:
195
+ * block.clearAllOverrides(); // Remove all overrides
196
+ * // or
197
+ * block.removeOveride("color"); // Remove specific override
198
+ * // or
199
+ * block.dispose(); // Remove the entire property block
200
+ * ```
201
+ *
173
202
  * @example Lightmap usage
174
203
  * ```typescript
175
204
  * const block = MaterialPropertyBlock.get(mesh);
@@ -317,8 +346,25 @@ export class MaterialPropertyBlock<T extends Material = Material> {
317
346
  }
318
347
 
319
348
  /**
320
- * Removes a specific property override
321
- * @param name The property name to clear
349
+ * Removes a specific property override.
350
+ * After removal, the material will use its original property value for this property.
351
+ *
352
+ * @param name The property name to remove the override for
353
+ *
354
+ * @example
355
+ * ```typescript
356
+ * const block = MaterialPropertyBlock.get(mesh);
357
+ *
358
+ * // Set some overrides
359
+ * block.setOverride("color", new Color(1, 0, 0));
360
+ * block.setOverride("roughness", 0.5);
361
+ * block.setOverride("lightMap", lightmapTexture);
362
+ *
363
+ * // Remove a specific override - the material will now use its original color
364
+ * block.removeOveride("color");
365
+ *
366
+ * // Other overrides (roughness, lightMap) remain active
367
+ * ```
322
368
  */
323
369
  removeOveride<K extends NonFunctionPropertyNames<T>>(name: K | ({} & string)): void {
324
370
  const index = this._overrides.findIndex(o => o.name === name);
@@ -328,7 +374,47 @@ export class MaterialPropertyBlock<T extends Material = Material> {
328
374
  }
329
375
 
330
376
  /**
331
- * Removes all property overrides from this block
377
+ * Removes all property overrides from this block.
378
+ * After calling this, the material will use its original values for all properties.
379
+ *
380
+ * **Note**: This does NOT remove shader defines. Use {@link clearDefine} or {@link dispose} for that.
381
+ *
382
+ * @example Remove all overrides but keep the property block
383
+ * ```typescript
384
+ * const block = MaterialPropertyBlock.get(mesh);
385
+ *
386
+ * // Set multiple overrides
387
+ * block.setOverride("color", new Color(1, 0, 0));
388
+ * block.setOverride("roughness", 0.5);
389
+ * block.setOverride("lightMap", lightmapTexture);
390
+ *
391
+ * // Later, remove all overrides at once
392
+ * block.clearAllOverrides();
393
+ *
394
+ * // The material now uses its original values
395
+ * // The property block still exists and can be reused with new overrides
396
+ * ```
397
+ *
398
+ * @example Temporarily disable all overrides
399
+ * ```typescript
400
+ * const block = MaterialPropertyBlock.get(mesh);
401
+ *
402
+ * // Save current overrides if you want to restore them later
403
+ * const savedOverrides = [...block.overrides];
404
+ *
405
+ * // Clear all overrides temporarily
406
+ * block.clearAllOverrides();
407
+ *
408
+ * // Do some rendering without overrides...
409
+ *
410
+ * // Restore overrides
411
+ * savedOverrides.forEach(override => {
412
+ * block.setOverride(override.name, override.value, override.textureTransform);
413
+ * });
414
+ * ```
415
+ *
416
+ * @see {@link removeOveride} - To remove a single override
417
+ * @see {@link dispose} - To completely remove the property block and clean up resources
332
418
  */
333
419
  clearAllOverrides(): void {
334
420
  this._overrides = [];
@@ -797,11 +883,11 @@ function attachPropertyBlockToObject(object: Object3D, _propertyBlock: MaterialP
797
883
  if (object.type === "Group") {
798
884
  object.children.forEach(child => {
799
885
  if (child.type === "Mesh" || child.type === "SkinnedMesh") {
800
- attachCallbacksToMesh(child, object);
886
+ attachCallbacksToMesh(child, object, _propertyBlock);
801
887
  }
802
888
  });
803
889
  } else if (object.type === "Mesh" || object.type === "SkinnedMesh") {
804
- attachCallbacksToMesh(object, object);
890
+ attachCallbacksToMesh(object, object, _propertyBlock);
805
891
  }
806
892
  }
807
893
  /**
@@ -811,7 +897,7 @@ function attachPropertyBlockToObject(object: Object3D, _propertyBlock: MaterialP
811
897
  * @param propertyBlockOwner The object that owns the property block (may be the mesh itself or its parent Group)
812
898
  * @internal
813
899
  */
814
- function attachCallbacksToMesh(mesh: Object3D, propertyBlockOwner: Object3D): void {
900
+ function attachCallbacksToMesh(mesh: Object3D, propertyBlockOwner: Object3D, _propertyBlock: MaterialPropertyBlock): void {
815
901
  // Check if this specific mesh already has our callbacks attached for this property block owner
816
902
  if (registry.isHooked(mesh, propertyBlockOwner)) {
817
903
  // Already hooked for this property block owner
@@ -820,6 +906,11 @@ function attachCallbacksToMesh(mesh: Object3D, propertyBlockOwner: Object3D): vo
820
906
 
821
907
  registry.addHook(mesh, propertyBlockOwner);
822
908
 
909
+ /**
910
+ * Expose the property block for e.g. Needle Inspector
911
+ */
912
+ mesh["needle:materialPropertyBlock"] = _propertyBlock;
913
+
823
914
  if (!mesh.onBeforeRender) {
824
915
  mesh.onBeforeRender = onBeforeRender_MaterialBlock;
825
916
  } else {
@@ -254,7 +254,40 @@ class LowPassFilter {
254
254
  }
255
255
 
256
256
  /**
257
- * OneEuroFilter is a simple low-pass filter for noisy signals. It uses a one-euro filter to smooth the signal.
257
+ * [OneEuroFilter](https://engine.needle.tools/docs/api/OneEuroFilter) is a low-pass filter designed to reduce jitter in noisy signals while maintaining low latency.
258
+ * It's particularly useful for smoothing tracking data from XR controllers, hand tracking, or other input devices where the signal contains noise but responsiveness is important.
259
+ *
260
+ * The filter automatically adapts its smoothing strength based on the signal's velocity:
261
+ * - When the signal moves slowly, it applies strong smoothing to reduce jitter
262
+ * - When the signal moves quickly, it reduces smoothing to maintain responsiveness
263
+ *
264
+ * Based on the research paper: [1€ Filter: A Simple Speed-based Low-pass Filter for Noisy Input](http://cristal.univ-lille.fr/~casiez/1euro/)
265
+ *
266
+ * @example Basic usage with timestamp
267
+ * ```ts
268
+ * const filter = new OneEuroFilter(120, 1.0, 0.0);
269
+ *
270
+ * // In your update loop:
271
+ * const smoothedValue = filter.filter(noisyValue, this.context.time.time);
272
+ * ```
273
+ *
274
+ * @example Without timestamps (using frequency estimate)
275
+ * ```ts
276
+ * // Assuming 60 FPS update rate
277
+ * const filter = new OneEuroFilter(60, 1.0, 0.5);
278
+ *
279
+ * // Call without timestamp - uses the frequency estimate
280
+ * const smoothedValue = filter.filter(noisyValue);
281
+ * ```
282
+ *
283
+ * @example Smoothing 3D positions
284
+ * ```ts
285
+ * const posFilter = new OneEuroFilterXYZ(90, 0.5, 0.0);
286
+ *
287
+ * posFilter.filter(trackedPosition, smoothedPosition, this.context.time.time);
288
+ * ```
289
+ *
290
+ * @see {@link OneEuroFilterXYZ} for filtering 3D vectors
258
291
  */
259
292
  export class OneEuroFilter {
260
293
  /**
@@ -1,5 +1,5 @@
1
1
  const defaultNetworkingBackendUrlProvider = "https://urls.needle.tools/default-networking-backend/index";
2
- let networkingServerUrl: string | undefined = "wss://networking.needle.tools/socket";
2
+ let networkingServerUrl: string | undefined = "wss://networking-2.needle.tools/socket";
3
3
 
4
4
  import * as flatbuffers from 'flatbuffers';
5
5
  import { type Websocket } from 'websocket-ts';
@@ -164,10 +164,15 @@ export interface IHasGuid {
164
164
  guid: string;
165
165
  }
166
166
 
167
+ // DO NOT CHANGE THE SYMBOL NAME
168
+ export const $componentName = Symbol("component-name");
169
+
167
170
  // TODO: we might want to separate the IComponent and IBehaviour where the Behaviour is the one with custom methods and the component only has e.g. the gameobject reference
168
171
  export interface IComponent extends IHasGuid {
169
172
  get isComponent(): boolean;
170
173
 
174
+ get [$componentName](): string | undefined;
175
+
171
176
  /** the object this component is attached to */
172
177
  gameObject: IGameObject;
173
178
  // guid: string;
@@ -607,7 +607,7 @@ export namespace DeviceUtilities {
607
607
  /** @returns `true` if it's a phone or tablet */
608
608
  export function isMobileDevice() {
609
609
  if (_ismobile !== undefined) return _ismobile;
610
- // eslint-disable-next-line deprecation/deprecation
610
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
611
611
  if ((typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1)) {
612
612
  return _ismobile = true;
613
613
  }
@@ -677,7 +677,7 @@ export namespace DeviceUtilities {
677
677
  /** @returns `true` for mobile Apple devices like iPad, iPhone, iPod, Vision Pro, ... */
678
678
  export function isiOS() {
679
679
  if (__isiOS !== undefined) return __isiOS;
680
- // eslint-disable-next-line deprecation/deprecation
680
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
681
681
  return __isiOS = iosDevices.includes(navigator.platform)
682
682
  // iPad on iOS 13 detection
683
683
  || (navigator.userAgent.includes("Mac") && "ontouchend" in document)
@@ -1,12 +1,12 @@
1
1
  import { AnimationClip, Object3D } from "three";
2
2
  import { GLTFExporter, GLTFExporterOptions } from "three/examples/jsm/exporters/GLTFExporter.js";
3
3
 
4
- import GLTFMeshGPUInstancingExtension from "../../../include/three/EXT_mesh_gpu_instancing_exporter.js";
5
4
  import { AnimationUtils } from "../../engine_animation.js";
6
5
  import type { Context } from "../../engine_setup.js";
7
6
  import { registerExportExtensions } from "../../extensions/index.js";
8
7
  import { __isExporting } from "../state.js";
9
8
  import { shouldExport_HideFlags } from "../utils.js";
9
+ import GLTFMeshGPUInstancingExtension from "./EXT_mesh_gpu_instancing_exporter.js";
10
10
  import { GizmoWriter as GLTFGizmoWriter, RenderTextureWriter as GLTFRenderTextureWriter } from "./Writers.js";
11
11
 
12
12
  declare type ExportOptions = {
@@ -17,6 +17,9 @@ export function getIconElement(str: string): HTMLElement {
17
17
  span.innerText = str;
18
18
  span.style.visibility = "hidden";
19
19
  span.style.userSelect = "none";
20
+ span.setAttribute("role", "img");
21
+ span.setAttribute("aria-label", str + " icon");
22
+ span.setAttribute("aria-hidden", "true");
20
23
  fontReady(fontname).then(res => {
21
24
  if (res) span.style.visibility = "";
22
25
  else {
@@ -20,8 +20,13 @@ export class NeedleLogoElement extends HTMLElement {
20
20
  return document.createElement(elementName) as NeedleLogoElement;
21
21
  }
22
22
 
23
+ private _didInitialize = false;
24
+
23
25
  constructor() {
24
26
  super();
27
+ }
28
+
29
+ private initializeDom() {
25
30
  this._root = this.attachShadow({ mode: 'closed' });
26
31
  const template = document.createElement('template');
27
32
  template.innerHTML = `<style>
@@ -83,22 +88,37 @@ export class NeedleLogoElement extends HTMLElement {
83
88
  this.addEventListener("click", () => {
84
89
  globalThis.open("https://needle.tools", "_blank");
85
90
  });
91
+ }
92
+
93
+ ensureInitialized() {
94
+ if (!this._didInitialize) {
95
+ this._didInitialize = true;
96
+ this.initializeDom();
97
+ }
98
+ }
86
99
 
87
- // set title
100
+ connectedCallback() {
101
+ this.ensureInitialized();
102
+ if (!this.wrapper) return;
88
103
  this.wrapper.setAttribute("title", "Made with Needle Engine");
104
+ this.setAttribute("aria-label", "Needle Engine logo. Click to open the Needle Engine website.");
89
105
  }
90
106
 
91
- private readonly _root: ShadowRoot;
92
- private readonly wrapper: HTMLDivElement;
93
- private readonly logoElement: HTMLImageElement;
107
+ private _root!: ShadowRoot;
108
+ private wrapper!: HTMLDivElement;
109
+ private logoElement!: HTMLImageElement;
94
110
 
95
111
  /** Show or hide the logo element (used by the menu) */
96
112
  setLogoVisible(val: boolean) {
113
+ this.ensureInitialized();
114
+ if (!this.logoElement) return;
97
115
  this.logoElement.style.display = val ? "block" : "none";
98
116
  }
99
117
 
100
118
  /** Switch the logo between full and compact versions */
101
119
  setType(type: "full" | "compact") {
120
+ this.ensureInitialized();
121
+ if (!this.logoElement) return;
102
122
  if (type === "full") {
103
123
  this.logoElement.src = needleLogoSVG;
104
124
  this.logoElement.classList.remove("with-text");