@needle-tools/engine 4.14.0-next.b2e3b1a → 4.15.0-next.cecd8e7

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 (92) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/components.needle.json +1 -1
  3. package/dist/{needle-engine.bundle-Bhgt3W8p.min.js → needle-engine.bundle-CuAiLb-d.min.js} +134 -126
  4. package/dist/{needle-engine.bundle-BC1QDiuv.umd.cjs → needle-engine.bundle-JQGIFVRm.umd.cjs} +128 -120
  5. package/dist/{needle-engine.bundle-CeQXs7Hh.js → needle-engine.bundle-VZVrVbc3.js} +3521 -3315
  6. package/dist/needle-engine.d.ts +450 -107
  7. package/dist/needle-engine.js +570 -569
  8. package/dist/needle-engine.min.js +1 -1
  9. package/dist/needle-engine.umd.cjs +1 -1
  10. package/lib/engine/api.d.ts +202 -17
  11. package/lib/engine/api.js +270 -17
  12. package/lib/engine/api.js.map +1 -1
  13. package/lib/engine/engine_accessibility.d.ts +77 -0
  14. package/lib/engine/engine_accessibility.js +162 -0
  15. package/lib/engine/engine_accessibility.js.map +1 -0
  16. package/lib/engine/engine_components.d.ts +1 -1
  17. package/lib/engine/engine_components.js +3 -7
  18. package/lib/engine/engine_components.js.map +1 -1
  19. package/lib/engine/engine_context.d.ts +2 -12
  20. package/lib/engine/engine_context.js +7 -29
  21. package/lib/engine/engine_context.js.map +1 -1
  22. package/lib/engine/engine_gltf_builtin_components.js +1 -16
  23. package/lib/engine/engine_gltf_builtin_components.js.map +1 -1
  24. package/lib/engine/engine_materialpropertyblock.d.ts +90 -4
  25. package/lib/engine/engine_materialpropertyblock.js +97 -7
  26. package/lib/engine/engine_materialpropertyblock.js.map +1 -1
  27. package/lib/engine/engine_math.d.ts +34 -1
  28. package/lib/engine/engine_math.js +34 -1
  29. package/lib/engine/engine_math.js.map +1 -1
  30. package/lib/engine/engine_networking.js +1 -1
  31. package/lib/engine/engine_networking.js.map +1 -1
  32. package/lib/engine/engine_types.d.ts +2 -0
  33. package/lib/engine/engine_types.js +2 -0
  34. package/lib/engine/engine_types.js.map +1 -1
  35. package/lib/engine/webcomponents/icons.js +3 -0
  36. package/lib/engine/webcomponents/icons.js.map +1 -1
  37. package/lib/engine/webcomponents/logo-element.d.ts +1 -0
  38. package/lib/engine/webcomponents/logo-element.js +3 -1
  39. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  40. package/lib/engine/webcomponents/needle-button.d.ts +37 -11
  41. package/lib/engine/webcomponents/needle-button.js +42 -11
  42. package/lib/engine/webcomponents/needle-button.js.map +1 -1
  43. package/lib/engine/webcomponents/needle-engine.d.ts +10 -2
  44. package/lib/engine/webcomponents/needle-engine.js +13 -3
  45. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  46. package/lib/engine-components/Component.d.ts +1 -2
  47. package/lib/engine-components/Component.js +1 -2
  48. package/lib/engine-components/Component.js.map +1 -1
  49. package/lib/engine-components/DragControls.d.ts +1 -0
  50. package/lib/engine-components/DragControls.js +21 -0
  51. package/lib/engine-components/DragControls.js.map +1 -1
  52. package/lib/engine-components/NeedleMenu.d.ts +2 -0
  53. package/lib/engine-components/NeedleMenu.js +2 -0
  54. package/lib/engine-components/NeedleMenu.js.map +1 -1
  55. package/lib/engine-components/Networking.d.ts +28 -3
  56. package/lib/engine-components/Networking.js +28 -3
  57. package/lib/engine-components/Networking.js.map +1 -1
  58. package/lib/engine-components/ReflectionProbe.d.ts +1 -0
  59. package/lib/engine-components/ReflectionProbe.js +20 -2
  60. package/lib/engine-components/ReflectionProbe.js.map +1 -1
  61. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.d.ts +107 -13
  62. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js +167 -30
  63. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js.map +1 -1
  64. package/lib/engine-components/postprocessing/Effects/Tonemapping.utils.d.ts +1 -1
  65. package/lib/engine-components/ui/Button.d.ts +1 -0
  66. package/lib/engine-components/ui/Button.js +11 -0
  67. package/lib/engine-components/ui/Button.js.map +1 -1
  68. package/lib/engine-components/ui/Text.d.ts +1 -0
  69. package/lib/engine-components/ui/Text.js +11 -0
  70. package/lib/engine-components/ui/Text.js.map +1 -1
  71. package/package.json +1 -1
  72. package/src/engine/api.ts +370 -18
  73. package/src/engine/engine_accessibility.ts +198 -0
  74. package/src/engine/engine_components.ts +3 -7
  75. package/src/engine/engine_context.ts +11 -28
  76. package/src/engine/engine_gltf_builtin_components.ts +1 -17
  77. package/src/engine/engine_materialpropertyblock.ts +102 -11
  78. package/src/engine/engine_math.ts +34 -1
  79. package/src/engine/engine_networking.ts +1 -1
  80. package/src/engine/engine_types.ts +5 -0
  81. package/src/engine/webcomponents/icons.ts +3 -0
  82. package/src/engine/webcomponents/logo-element.ts +4 -1
  83. package/src/engine/webcomponents/needle-button.ts +44 -13
  84. package/src/engine/webcomponents/needle-engine.ts +18 -7
  85. package/src/engine-components/Component.ts +1 -3
  86. package/src/engine-components/DragControls.ts +29 -4
  87. package/src/engine-components/NeedleMenu.ts +5 -3
  88. package/src/engine-components/Networking.ts +29 -4
  89. package/src/engine-components/ReflectionProbe.ts +21 -2
  90. package/src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +198 -65
  91. package/src/engine-components/ui/Button.ts +12 -0
  92. package/src/engine-components/ui/Text.ts +13 -0
@@ -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
@@ -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
@@ -753,34 +760,8 @@ export class Context implements IContext {
753
760
  }
754
761
  }
755
762
 
756
- /**
757
- * Registers all uninitialized components found in the scene hierarchy with this context.
758
- * Components that have been added to objects (e.g. via `addComponent`) but are not yet registered
759
- * with a context will be collected and queued for initialization.
760
- * On the next frame, these components will go through the full lifecycle: `awake` → `onEnable` → `start` → `update`.
761
- *
762
- * This is useful when components are created outside of the normal glTF loading pipeline,
763
- * for example in an editor that adds components during edit mode and then needs to activate them for play mode.
764
- * @param root The root object to search for components. Defaults to the context's scene.
765
- * @returns The number of components that were newly registered.
766
- */
767
- registerSceneComponents(root?: Object3D): number {
768
- const searchRoot = root ?? this.scene;
769
- if (!searchRoot) return 0;
770
- let count = 0;
771
- foreachComponent(searchRoot, (comp) => {
772
- // Skip components that are already registered with this context
773
- if (this.scripts.includes(comp)) return;
774
- if (this.new_scripts.includes(comp)) return;
775
- if (this.new_script_start.includes(comp)) return;
776
- this.new_scripts.push(comp);
777
- count++;
778
- }, true);
779
- return count;
780
- }
781
-
782
- /** This will recreate the whole needle engine context and dispose the whole scene content
783
- * All content will be reloaded (loading times might be faster due to browser caches)
763
+ /** This will recreate the whole needle engine context and dispose the whole scene content
764
+ * All content will be reloaded (loading times might be faster due to browser caches)
784
765
  * All scripts will be recreated */
785
766
  recreate() {
786
767
  this.clear();
@@ -836,6 +817,7 @@ export class Context implements IContext {
836
817
  this.lightmaps?.clear();
837
818
  this.physics?.engine?.clearCaches();
838
819
  this.lodsManager.disable();
820
+ this.accessibility?.clear();
839
821
 
840
822
  this._onBeforeRenderListeners.clear();
841
823
  this._onAfterRenderListeners.clear();
@@ -857,6 +839,7 @@ export class Context implements IContext {
857
839
  */
858
840
  dispose() {
859
841
  this.internalOnDestroy();
842
+ this.accessibility.dispose();
860
843
  }
861
844
 
862
845
  /**@deprecated use dispose() */
@@ -113,20 +113,6 @@ export async function createBuiltinComponents(context: Context, gltfId: SourceId
113
113
  }
114
114
  }
115
115
  });
116
-
117
- // Batch-register all components that were created above with the context.
118
- // This uses the same codepath as Context.registerSceneComponents() which is also used
119
- // by editors to activate components that were added outside the glTF pipeline.
120
- if (gltf.scenes) {
121
- for (const scene of gltf.scenes) {
122
- context.registerSceneComponents(scene);
123
- }
124
- }
125
- if (gltf.children) {
126
- for (const ch of gltf.children) {
127
- context.registerSceneComponents(ch);
128
- }
129
- }
130
116
  }
131
117
 
132
118
  declare type IHasResolveGuids = {
@@ -250,10 +236,8 @@ async function onCreateBuiltinComponents(context: SerializationContext, obj: Obj
250
236
  // Object.assign(instance, compData);
251
237
  // dont call awake here because some references might not be resolved yet and components that access those fields in awake will throw
252
238
  // for example Duplicatable reference to object might still be { node: id }
253
- // dont register here - components are batch-registered via context.registerSceneComponents below
254
239
  const callAwake = false;
255
- const register = false;
256
- addNewComponent(obj, instance, callAwake, register);
240
+ addNewComponent(obj, instance, callAwake);
257
241
  deserialize.push({ instance, compData, obj });
258
242
 
259
243
  // if the component instance is a camera and we dont have a main camera yet
@@ -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;
@@ -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 {
@@ -84,8 +84,11 @@ export class NeedleLogoElement extends HTMLElement {
84
84
  globalThis.open("https://needle.tools", "_blank");
85
85
  });
86
86
 
87
- // set title
87
+ }
88
+
89
+ connectedCallback() {
88
90
  this.wrapper.setAttribute("title", "Made with Needle Engine");
91
+ this.setAttribute("aria-label", "Needle Engine logo. Click to open the Needle Engine website.");
89
92
  }
90
93
 
91
94
  private readonly _root: ShadowRoot;
@@ -13,32 +13,58 @@ const htmlTagName = "needle-button";
13
13
  const isDev = isDevEnvironment();
14
14
 
15
15
  /**
16
- * A <needle-button> can be used to simply add VR, AR or Quicklook buttons to your website without having to write any code.
17
- * @example
16
+ * [&lt;needle-button&gt;](https://engine.needle.tools/docs/api/NeedleButtonElement) is a web component for easily adding AR, VR, Quicklook, or QR code buttons to your website without writing JavaScript code.
17
+ *
18
+ * The button automatically handles session management and displays appropriate UI based on device capabilities.
19
+ * It comes with default styling (glassmorphism design) but can be fully customized with CSS.
20
+ *
21
+ * **Supported button types:**
22
+ * - `ar` - WebXR AR session button
23
+ * - `vr` - WebXR VR session button
24
+ * - `quicklook` - Apple AR Quick Look button (iOS only)
25
+ * - `qrcode` - QR code sharing button
26
+ *
27
+ * @example Basic AR/VR buttons
18
28
  * ```html
29
+ * <needle-engine src="scene.glb"></needle-engine>
19
30
  * <needle-button ar></needle-button>
20
31
  * <needle-button vr></needle-button>
21
32
  * <needle-button quicklook></needle-button>
22
33
  * ```
23
- *
24
- * @example custom label
34
+ *
35
+ * @example Custom button labels
25
36
  * ```html
26
- * <needle-button ar>Start AR</needle-button>
27
- * <needle-button vr>Start VR</needle-button>
37
+ * <needle-button ar>Start AR Experience</needle-button>
38
+ * <needle-button vr>Enter VR Mode</needle-button>
28
39
  * <needle-button quicklook>View in AR</needle-button>
29
40
  * ```
30
- *
31
- * @example custom styling
41
+ *
42
+ * @example Custom styling
32
43
  * ```html
33
- * <!-- You can either style the element directly or use a CSS stylesheet -->
34
44
  * <style>
35
- * needle-button {
36
- * background-color: red;
37
- * color: white;
38
- * }
45
+ * needle-button {
46
+ * background-color: #ff6b6b;
47
+ * color: white;
48
+ * border-radius: 8px;
49
+ * padding: 1rem 2rem;
50
+ * }
51
+ * needle-button:hover {
52
+ * background-color: #ff5252;
53
+ * }
39
54
  * </style>
40
55
  * <needle-button ar>Start AR</needle-button>
41
56
  * ```
57
+ *
58
+ * @example Unstyled button (for complete custom styling)
59
+ * ```html
60
+ * <needle-button ar unstyled>
61
+ * <span class="my-icon">🥽</span>
62
+ * Launch AR
63
+ * </needle-button>
64
+ * ```
65
+ *
66
+ * @see {@link NeedleEngineWebComponent} for the main &lt;needle-engine&gt; element
67
+ * @see {@link NeedleMenu} for the built-in menu component that can display similar buttons
42
68
  */
43
69
  export class NeedleButtonElement extends HTMLElement {
44
70
 
@@ -73,18 +99,22 @@ export class NeedleButtonElement extends HTMLElement {
73
99
  if (this.getAttribute("ar") != null) {
74
100
  this.#webxrfactory ??= new WebXRButtonFactory()
75
101
  this.#button = this.#webxrfactory.createARButton();
102
+ this.setAttribute("aria-label", "Enter augmented reality mode");
76
103
  }
77
104
  else if (this.getAttribute("vr") != null) {
78
105
  this.#webxrfactory ??= new WebXRButtonFactory()
79
106
  this.#button = this.#webxrfactory.createVRButton();
107
+ this.setAttribute("aria-label", "Enter virtual reality mode");
80
108
  }
81
109
  else if (this.getAttribute("quicklook") != null) {
82
110
  this.#webxrfactory ??= new WebXRButtonFactory()
83
111
  this.#button = this.#webxrfactory.createQuicklookButton();
112
+ this.setAttribute("aria-label", "View in AR with Apple Quick Look");
84
113
  }
85
114
  else if (this.getAttribute("qrcode") != null) {
86
115
  this.#buttonfactory ??= new ButtonsFactory();
87
116
  this.#button = this.#buttonfactory.createQRCode({ anchorElement: this });
117
+ this.setAttribute("aria-label", "Share application with QR code");
88
118
  }
89
119
  else {
90
120
  if (isDev) {
@@ -93,6 +123,7 @@ export class NeedleButtonElement extends HTMLElement {
93
123
  else {
94
124
  console.debug("No button type specified for <needle-button>. Use either ar, vr or quicklook attribute.")
95
125
  }
126
+ this.setAttribute("aria-label", "Needle Button with no specified type");
96
127
  return;
97
128
  }
98
129
 
@@ -87,17 +87,25 @@ const observedAttributes = [
87
87
 
88
88
  // https://developers.google.com/web/fundamentals/web-components/customelements
89
89
 
90
- /**
91
- * The `<needle-engine>` web component. See {@link NeedleEngineAttributes} attributes for supported attributes
92
- * The web component creates and manages a Needle Engine context, which is responsible for rendering a 3D scene using threejs.
93
- * The context is created when the `src` attribute is set, and disposed when the element is removed from the DOM. You can prevent cleanup by setting the `keep-alive` attribute to `true`.
90
+ /**
91
+ * The `<needle-engine>` web component. See {@link NeedleEngineAttributes} attributes for supported attributes
92
+ * The web component creates and manages a Needle Engine context, which is responsible for rendering a 3D scene using threejs.
93
+ * The context is created when the `src` attribute is set, and disposed when the element is removed from the DOM. You can prevent cleanup by setting the `keep-alive` attribute to `true`.
94
94
  * The context is accessible from the `<needle-engine>` element: `document.querySelector("needle-engine").context`.
95
95
  * See {@link https://engine.needle.tools/docs/reference/needle-engine-attributes}
96
96
  *
97
- * @example
97
+ * @example Basic usage
98
+ * ```html
98
99
  * <needle-engine src="https://example.com/scene.glb"></needle-engine>
99
- * @example
100
+ * ```
101
+ *
102
+ * @example With camera controls disabled
103
+ * ```html
100
104
  * <needle-engine src="https://example.com/scene.glb" camera-controls="false"></needle-engine>
105
+ * ```
106
+ *
107
+ * @see {@link NeedleButtonElement} for adding AR/VR/Quicklook buttons via &lt;needle-button&gt;
108
+ * @see {@link NeedleMenu} for the built-in menu configuration component
101
109
  */
102
110
  export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngineComponent {
103
111
 
@@ -176,7 +184,9 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
176
184
 
177
185
  ensureFonts();
178
186
 
179
- this.attachShadow({ mode: 'open' });
187
+ this.attachShadow({ mode: 'open', delegatesFocus: true });
188
+ this.setAttribute("role", "application");
189
+ this.setAttribute("aria-label", "Needle Engine 3D scene");
180
190
  const template = document.createElement('template');
181
191
  // #region CSS
182
192
  template.innerHTML = `<style>
@@ -282,6 +292,7 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
282
292
  if (this.getAttribute("tabindex") === null || this.getAttribute("tabindex") === undefined)
283
293
  this.setAttribute("tabindex", "0");
284
294
 
295
+
285
296
  this.addEventListener("xr-session-started", this.onXRSessionStarted);
286
297
  this.onSetupDesktop();
287
298
 
@@ -10,7 +10,7 @@ import * as main from "../engine/engine_mainloop_utils.js";
10
10
  import { syncDestroy, syncInstantiate, SyncInstantiateOptions } from "../engine/engine_networking_instantiate.js";
11
11
  import { Context, FrameEvent } from "../engine/engine_setup.js";
12
12
  import * as threeutils from "../engine/engine_three_utils.js";
13
- import type { Collision, ComponentInit, Constructor, ConstructorConcrete, GuidsMap, ICollider, IComponent, IGameObject, SourceIdentifier } from "../engine/engine_types.js";
13
+ import { $componentName, type Collision, type ComponentInit, type Constructor, type ConstructorConcrete, type GuidsMap, type ICollider, type IComponent, type IGameObject, type SourceIdentifier } from "../engine/engine_types.js";
14
14
  import { TypeStore } from "../engine/engine_typestore.js";
15
15
  import type { INeedleXRSessionEventReceiver, NeedleXRControllerEventArgs, NeedleXREventArgs } from "../engine/engine_xr.js";
16
16
  import { type IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
@@ -542,8 +542,6 @@ export abstract class GameObject extends Object3D implements Object3D, IGameObje
542
542
  }
543
543
  }
544
544
 
545
- // DO NOT CHANGE THE SYMBOL NAME
546
- const $componentName = Symbol("component-name");
547
545
 
548
546
  /**
549
547
  * Needle Engine component's are the main building blocks of the Needle Engine.
@@ -193,7 +193,7 @@ export class DragControls extends Behaviour implements IPointerEventHandler {
193
193
  }
194
194
  }
195
195
  }
196
-
196
+
197
197
  private _rigidbody: Rigidbody | null = null;
198
198
 
199
199
  // future:
@@ -235,12 +235,22 @@ export class DragControls extends Behaviour implements IPointerEventHandler {
235
235
  /** @internal */
236
236
  onEnable(): void {
237
237
  DragControls._instances.push(this);
238
+ this.context.accessibility.updateElement(this, {
239
+ role: "button",
240
+ label: "Drag " + (this.gameObject.name || "object"),
241
+ hidden: false,
242
+ });
238
243
  }
239
244
  /** @internal */
240
245
  onDisable(): void {
246
+ this.context.accessibility.updateElement(this, { hidden: true });
241
247
  DragControls._instances = DragControls._instances.filter(i => i !== this);
242
248
  }
243
249
 
250
+ onDestroy(): void {
251
+ this.context.accessibility.removeElement(this);
252
+ }
253
+
244
254
  /**
245
255
  * Checks if editing is allowed for the current networking connection.
246
256
  * @param _obj Optional object to check edit permissions for
@@ -268,6 +278,8 @@ export class DragControls extends Behaviour implements IPointerEventHandler {
268
278
  if (!dc || dc !== this) return;
269
279
  DragControls.lastHovered = evt.object;
270
280
  this.context.domElement.style.cursor = 'pointer';
281
+
282
+ this.context.accessibility.hover(this, `Draggable ${evt.object?.name}`);
271
283
  }
272
284
 
273
285
  /**
@@ -339,6 +351,14 @@ export class DragControls extends Behaviour implements IPointerEventHandler {
339
351
  }
340
352
 
341
353
  args.use();
354
+
355
+ this.context.accessibility.updateElement(this, {
356
+ role: "button",
357
+ label: "Dragging " + (this.gameObject.name || "object"),
358
+ hidden: false,
359
+ busy: true,
360
+ });
361
+ this.context.accessibility.focus(this);
342
362
  }
343
363
  }
344
364
 
@@ -375,6 +395,11 @@ export class DragControls extends Behaviour implements IPointerEventHandler {
375
395
  }
376
396
  args.use();
377
397
  }
398
+
399
+ this.context.accessibility.unfocus(this);
400
+ this.context.accessibility.updateElement(this, {
401
+ busy: false,
402
+ });
378
403
  }
379
404
 
380
405
  /**
@@ -779,12 +804,12 @@ class DragPointerHandler implements IDragHandler {
779
804
  * Used for determining if enough motion has occurred to start a drag.
780
805
  */
781
806
  getTotalMovement(): Vector3 { return this._totalMovement; }
782
-
807
+
783
808
  /**
784
809
  * Returns the object that follows the pointer during dragging operations.
785
810
  */
786
811
  get followObject(): GameObject { return this._followObject; }
787
-
812
+
788
813
  /**
789
814
  * Returns the point where the pointer initially hit the object in local space.
790
815
  */
@@ -1377,7 +1402,7 @@ class LegacyDragVisualsHelper {
1377
1402
 
1378
1403
  /** Controls whether visual helpers like lines and markers are displayed */
1379
1404
  showGizmo: boolean = true;
1380
-
1405
+
1381
1406
  /** When true, drag plane alignment changes based on view angle */
1382
1407
  useViewAngle: boolean = true;
1383
1408