@shopware-ag/dive 1.18.5 → 1.19.1-beta.0

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 (66) hide show
  1. package/README.md +8 -0
  2. package/build/dive.cjs +220 -215
  3. package/build/dive.cjs.map +1 -1
  4. package/build/dive.js +233 -21102
  5. package/build/dive.js.map +1 -1
  6. package/build/dive.mjs +26111 -0
  7. package/build/dive.mjs.map +1 -0
  8. package/build/src/ar/AR.d.ts +37 -14
  9. package/build/src/ar/arquicklook/ARQuickLook.d.ts +5 -6
  10. package/build/src/ar/sceneviewer/SceneViewer.d.ts +42 -6
  11. package/build/src/com/actions/index.d.ts +2 -0
  12. package/build/src/com/actions/renderer/startrender.d.ts +5 -0
  13. package/build/src/com/actions/scene/launchar.d.ts +5 -2
  14. package/build/src/converter/Converter.d.ts +21 -0
  15. package/build/src/dive.d.ts +3 -2
  16. package/build/src/exporter/Exporter.d.ts +11 -0
  17. package/build/src/helper/applyMixins/applyMixins.d.ts +22 -6
  18. package/build/src/info/Info.d.ts +37 -13
  19. package/build/src/interface/Movable.d.ts +5 -5
  20. package/build/src/interface/Selectable.d.ts +4 -4
  21. package/build/src/loader/Loader.d.ts +11 -0
  22. package/build/src/model/Model.d.ts +2 -2
  23. package/build/src/node/Node.d.ts +3 -3
  24. package/build/src/scene/root/Root.d.ts +1 -1
  25. package/build/src/types/ExporterOptions.d.ts +15 -0
  26. package/build/src/types/FileTypes.d.ts +27 -0
  27. package/build/src/types/index.d.ts +4 -0
  28. package/build/src/types/info/index.d.ts +66 -0
  29. package/package.json +16 -1
  30. package/src/ar/AR.ts +72 -69
  31. package/src/ar/__test__/AR.test.ts +194 -105
  32. package/src/ar/arquicklook/ARQuickLook.ts +32 -72
  33. package/src/ar/arquicklook/__test__/ARQuickLook.test.ts +89 -38
  34. package/src/ar/sceneviewer/SceneViewer.ts +96 -51
  35. package/src/ar/sceneviewer/__test__/SceneViewer.test.ts +144 -47
  36. package/src/ar/webxr/WebXR.ts +5 -4
  37. package/src/com/Communication.ts +10 -7
  38. package/src/com/__test__/Communication.test.ts +16 -3
  39. package/src/com/actions/index.ts +2 -0
  40. package/src/com/actions/renderer/startrender.ts +5 -0
  41. package/src/com/actions/scene/launchar.ts +2 -2
  42. package/src/converter/Converter.ts +117 -0
  43. package/src/dive.ts +10 -6
  44. package/src/exporter/Exporter.ts +75 -0
  45. package/src/helper/applyMixins/applyMixins.ts +59 -7
  46. package/src/info/Info.ts +99 -75
  47. package/src/info/__test__/Info.test.ts +162 -154
  48. package/src/interface/Movable.ts +5 -5
  49. package/src/interface/Selectable.ts +4 -4
  50. package/src/loader/Loader.ts +48 -0
  51. package/src/model/Model.ts +10 -5
  52. package/src/model/__test__/Model.test.ts +4 -11
  53. package/src/node/Node.ts +7 -5
  54. package/src/scene/root/Root.ts +4 -4
  55. package/src/scene/root/__test__/Root.test.ts +4 -4
  56. package/src/toolbox/Toolbox.ts +1 -3
  57. package/src/types/ExporterOptions.ts +14 -0
  58. package/src/types/FileTypes.ts +37 -0
  59. package/src/types/index.ts +26 -0
  60. package/src/types/info/index.ts +76 -0
  61. package/build/src/exporters/usdz/USDZExporter.d.ts +0 -15
  62. package/build/src/loadingmanager/LoadingManager.d.ts +0 -14
  63. package/src/exporters/usdz/USDZExporter.ts +0 -21
  64. package/src/exporters/usdz/__test__/USDZExporter.test.ts +0 -57
  65. package/src/loadingmanager/LoadingManager.ts +0 -50
  66. package/src/loadingmanager/__test__/LoadingManager.test.ts +0 -27
@@ -34,6 +34,7 @@ import {
34
34
  type COMPov,
35
35
  } from '../types';
36
36
  import { type DIVESceneObject } from '../../types';
37
+ import { type ARSystemOptions } from '../../ar/AR';
37
38
 
38
39
  const mockModule: Record<string, any> = {
39
40
  get: jest.fn().mockReturnValue(Promise.resolve({})),
@@ -92,6 +93,7 @@ jest.mock('../../toolbox/select/SelectTool', () => {
92
93
  const mockRenderer = {
93
94
  render: jest.fn(),
94
95
  OnResize: jest.fn(),
96
+ StartRenderer: jest.fn(),
95
97
  } as unknown as DIVERenderer;
96
98
 
97
99
  const mockScene = {
@@ -288,6 +290,12 @@ describe('dive/communication/DIVECommunication', () => {
288
290
  ).not.toThrow();
289
291
  });
290
292
 
293
+ it('should perform action START_RENDER', () => {
294
+ const success = testCom.PerformAction('START_RENDER');
295
+ expect(mockRenderer.StartRenderer).toHaveBeenCalledTimes(1);
296
+ expect(success).toBe(true);
297
+ });
298
+
291
299
  it('should perform action ADD_OBJECT', () => {
292
300
  const payload = {
293
301
  entityType: 'light',
@@ -998,14 +1006,19 @@ describe('dive/communication/DIVECommunication', () => {
998
1006
 
999
1007
  it('should perform action LAUNCH_AR', async () => {
1000
1008
  jest.spyOn(mockModule, 'get').mockResolvedValue({
1001
- Launch: jest.fn(),
1009
+ launch: jest.fn(),
1002
1010
  });
1003
1011
  const arModule = await testCom['_ar'].get();
1004
1012
  const arLaunchSpy = jest
1005
- .spyOn(arModule, 'Launch')
1013
+ .spyOn(arModule, 'launch')
1006
1014
  .mockResolvedValueOnce();
1007
1015
 
1008
- const result = await testCom.PerformAction('LAUNCH_AR', undefined);
1016
+ const result = await testCom.PerformAction('LAUNCH_AR', {
1017
+ uri: 'https://example.com',
1018
+ options: {
1019
+ arPlacement: 'horizontal',
1020
+ } as ARSystemOptions,
1021
+ });
1009
1022
  expect(arLaunchSpy).toHaveBeenCalledTimes(1);
1010
1023
  });
1011
1024
 
@@ -1,3 +1,4 @@
1
+ import START_RENDER from './renderer/startrender.ts';
1
2
  import SET_BACKGROUND from './scene/setbackground.ts';
2
3
  import RESET_CAMERA from './camera/resetcamera.ts';
3
4
  import SET_CAMERA_LAYER from './camera/setcameralayer.ts';
@@ -28,6 +29,7 @@ import EXPORT_SCENE from './scene/exportscene.ts';
28
29
  import LAUNCH_AR from './scene/launchar.ts';
29
30
 
30
31
  export interface Actions {
32
+ START_RENDER: START_RENDER;
31
33
  GET_ALL_SCENE_DATA: GET_ALL_SCENE_DATA;
32
34
  GET_ALL_OBJECTS: GET_ALL_OBJECTS;
33
35
  GET_OBJECTS: GET_OBJECTS;
@@ -0,0 +1,5 @@
1
+ export default interface START_RENDER {
2
+ DESCRIPTION: 'Starts the render process.';
3
+ PAYLOAD: undefined;
4
+ RETURN: boolean;
5
+ }
@@ -1,7 +1,7 @@
1
- import { type DIVEAROptions } from '../../../ar/AR';
1
+ import { type ARSystemOptions } from '../../../ar/AR';
2
2
 
3
3
  export default interface LAUNCH_AR {
4
4
  DESCRIPTION: 'Launches AR mode in native capabilities. (iOS: AR Quick Look, Android: Google Scene Viewer)';
5
- PAYLOAD?: DIVEAROptions;
5
+ PAYLOAD: { uri: string; options?: ARSystemOptions };
6
6
  RETURN: Promise<void>;
7
7
  }
@@ -0,0 +1,117 @@
1
+ import { Loader } from '../loader/Loader';
2
+ import { Exporter } from '../exporter/Exporter';
3
+ import {
4
+ type FileType,
5
+ SUPPORTED_FILE_TYPES,
6
+ type ExportOptions,
7
+ } from '../types';
8
+
9
+ export class ConversionError extends Error {
10
+ constructor(
11
+ message: string,
12
+ public readonly cause?: unknown,
13
+ ) {
14
+ super(message);
15
+ this.name = 'ConversionError';
16
+ }
17
+ }
18
+
19
+ export class FileTypeError extends ConversionError {
20
+ constructor(extension: string) {
21
+ super(
22
+ `Unsupported file type: ${extension}. Supported types are: ${SUPPORTED_FILE_TYPES.join(', ')}`,
23
+ );
24
+ this.name = 'FileTypeError';
25
+ }
26
+ }
27
+
28
+ export class NetworkError extends ConversionError {
29
+ constructor(uri: string, cause?: unknown) {
30
+ super(`Failed to fetch file from ${uri}`, cause);
31
+ this.name = 'NetworkError';
32
+ }
33
+ }
34
+
35
+ export class Converter {
36
+ private _loader: Loader;
37
+ private _exporter: Exporter;
38
+
39
+ constructor(private readonly _uri: string) {
40
+ this._loader = new Loader();
41
+ this._exporter = new Exporter();
42
+ }
43
+
44
+ public static convert(uri: string): Converter {
45
+ return new Converter(uri);
46
+ }
47
+
48
+ public async to<T extends FileType>(
49
+ type: T,
50
+ options?: ExportOptions<T>,
51
+ ): Promise<ArrayBuffer> {
52
+ try {
53
+ const sourceType = this._getFileTypeFromUri();
54
+
55
+ // If source and target types match, just return the file content
56
+ if (sourceType === type) {
57
+ return await this._loadFile();
58
+ }
59
+
60
+ // Otherwise, convert through Object3D
61
+ const object3D = await this._loader.load(this._uri);
62
+ return await this._exporter.export(object3D, type, options);
63
+ } catch (error) {
64
+ if (error instanceof ConversionError) {
65
+ throw error;
66
+ }
67
+ throw new ConversionError('Failed to convert file', error);
68
+ }
69
+ }
70
+
71
+ private _getFileTypeFromUri(): FileType {
72
+ const extension = this._uri.split('.').pop()?.toLowerCase();
73
+ if (!extension) {
74
+ throw new FileTypeError('no extension');
75
+ }
76
+ if (!SUPPORTED_FILE_TYPES.includes(extension as FileType)) {
77
+ throw new FileTypeError(extension);
78
+ }
79
+ return extension as FileType;
80
+ }
81
+
82
+ private async _loadFile(): Promise<ArrayBuffer> {
83
+ try {
84
+ const response = await fetch(this._uri);
85
+ if (!response.ok) {
86
+ throw new NetworkError(
87
+ this._uri,
88
+ `HTTP error! status: ${response.status}`,
89
+ );
90
+ }
91
+ return response.arrayBuffer();
92
+ } catch (error) {
93
+ if (error instanceof NetworkError) {
94
+ throw error;
95
+ }
96
+ throw new NetworkError(this._uri, error);
97
+ }
98
+ }
99
+ }
100
+
101
+ // Example usage:
102
+ // Converter.convert('https://example.com/model.glb').to('usdz', {
103
+ // usdz: {
104
+ // ar: {
105
+ // anchoring: { type: 'plane' },
106
+ // planeAnchoring: { alignment: 'horizontal' }
107
+ // }
108
+ // }
109
+ // });
110
+ //
111
+ // Converter.convert('https://example.com/model.usdz').to('gltf', {
112
+ // gltf: {
113
+ // onlyVisible: true,
114
+ // maxTextureSize: 2048,
115
+ // includeCustomExtensions: true
116
+ // }
117
+ // });
package/src/dive.ts CHANGED
@@ -18,11 +18,12 @@ import { DIVEAnimationSystem } from './animation/AnimationSystem.ts';
18
18
  import DIVEAxisCamera from './axiscamera/AxisCamera.ts';
19
19
  import { getObjectDelta } from './helper/getObjectDelta/getObjectDelta.ts';
20
20
  import { MathUtils } from 'three';
21
- import { DIVEInfo } from './info/Info.ts';
21
+ import { SystemInfo } from './info/Info.ts';
22
22
  import pkgjson from '../package.json';
23
23
 
24
24
  export type DIVESettings = {
25
25
  autoResize: boolean;
26
+ autoStart: boolean;
26
27
  displayAxes: boolean;
27
28
  renderer: Partial<DIVERendererSettings>;
28
29
  perspectiveCamera: Partial<DIVEPerspectiveCameraSettings>;
@@ -31,6 +32,7 @@ export type DIVESettings = {
31
32
 
32
33
  export const DIVEDefaultSettings: DIVESettings = {
33
34
  autoResize: true,
35
+ autoStart: true,
34
36
  displayAxes: false,
35
37
  renderer: DIVERendererDefaultSettings,
36
38
  perspectiveCamera: DIVEPerspectiveCameraDefaultSettings,
@@ -156,8 +158,8 @@ export default class DIVE {
156
158
  return this.renderer.domElement;
157
159
  }
158
160
 
159
- public get Info(): DIVEInfo {
160
- return DIVEInfo;
161
+ public get Info(): SystemInfo {
162
+ return SystemInfo;
161
163
  }
162
164
 
163
165
  // setters
@@ -266,9 +268,6 @@ export default class DIVE {
266
268
  this.addResizeObserver();
267
269
  }
268
270
 
269
- // whene everything is done, start the renderer
270
- this.renderer.StartRenderer(this.scene, this.perspectiveCamera);
271
-
272
271
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
273
272
  (window as any).DIVE = {
274
273
  PrintScene: () => {
@@ -307,6 +306,11 @@ export default class DIVE {
307
306
  @@@@@@@ @@@@@@
308
307
 
309
308
  `);
309
+
310
+ if (this._settings.autoStart) {
311
+ // when everything is done, start the renderer
312
+ this.renderer.StartRenderer(this.scene, this.perspectiveCamera);
313
+ }
310
314
  }
311
315
 
312
316
  public Dispose(): void {
@@ -0,0 +1,75 @@
1
+ import { Object3D } from 'three';
2
+ import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter';
3
+ import { USDZExporter } from 'three/examples/jsm/exporters/USDZExporter';
4
+ import {
5
+ type FileType,
6
+ type GLTFExporterOptions,
7
+ type USDZExporterOptions,
8
+ type ExportOptions,
9
+ } from '../types';
10
+
11
+ export class Exporter {
12
+ private _gltfExporter: GLTFExporter;
13
+ private _usdzExporter: USDZExporter;
14
+
15
+ constructor() {
16
+ this._gltfExporter = new GLTFExporter();
17
+ this._usdzExporter = new USDZExporter();
18
+ }
19
+
20
+ public async export<T extends FileType>(
21
+ object: Object3D,
22
+ type: T,
23
+ options?: ExportOptions<T>,
24
+ ): Promise<ArrayBuffer> {
25
+ switch (type) {
26
+ case 'glb': {
27
+ return this._exportGlb(object, options);
28
+ }
29
+ case 'gltf': {
30
+ return this._exportGltf(object, options);
31
+ }
32
+ case 'usdz': {
33
+ return this._exportUsdz(object, options);
34
+ }
35
+ default:
36
+ throw new Error(`Unsupported file type: ${type}`);
37
+ }
38
+ }
39
+
40
+ private async _exportGlb(
41
+ object: Object3D,
42
+ options?: GLTFExporterOptions,
43
+ ): Promise<ArrayBuffer> {
44
+ const result = await this._gltfExporter.parseAsync(object, {
45
+ ...options,
46
+ binary: true,
47
+ });
48
+ if (result instanceof ArrayBuffer) {
49
+ return result;
50
+ }
51
+ throw new Error('Failed to export GLB: expected ArrayBuffer');
52
+ }
53
+
54
+ private async _exportGltf(
55
+ object: Object3D,
56
+ options?: GLTFExporterOptions,
57
+ ): Promise<ArrayBuffer> {
58
+ const json = await this._gltfExporter.parseAsync(object, {
59
+ ...options,
60
+ binary: false,
61
+ });
62
+ const text = JSON.stringify(json);
63
+ const encoder = new TextEncoder();
64
+ const bytes = encoder.encode(text);
65
+ return bytes.buffer as ArrayBuffer;
66
+ }
67
+
68
+ private async _exportUsdz(
69
+ object: Object3D,
70
+ options?: USDZExporterOptions,
71
+ ): Promise<ArrayBuffer> {
72
+ const result = await this._usdzExporter.parse(object, options);
73
+ return result.buffer as ArrayBuffer;
74
+ }
75
+ }
@@ -1,11 +1,62 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ /* eslint-disable @typescript-eslint/ban-types */
3
+ // A generic constructor type.
4
+ type Constructor<T = {}> = new (...args: any[]) => T;
5
+
6
+ // Converts a union of types to an intersection of types.
7
+ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
8
+ k: infer I,
9
+ ) => void
10
+ ? I
11
+ : never;
12
+
13
+ // Merges the instance types of the base class and all mixin classes.
14
+ type MixedInstance<
15
+ T extends Constructor,
16
+ K extends readonly Constructor[],
17
+ > = InstanceType<T> & UnionToIntersection<InstanceType<K[number]>>;
18
+
19
+ // Recursively flatten the constructor parameter lists for a tuple of constructors.
20
+ type FlattenConstructorParams<T extends readonly Constructor[]> =
21
+ T extends readonly [infer First, ...infer Rest]
22
+ ? First extends Constructor
23
+ ? Rest extends readonly Constructor[]
24
+ ? [
25
+ ...ConstructorParameters<First>,
26
+ ...FlattenConstructorParams<Rest>,
27
+ ]
28
+ : ConstructorParameters<First>
29
+ : []
30
+ : [];
31
+
32
+ // Constructs the mixed constructor type.
33
+ // It accepts the parameters of T followed by all the parameters of K and
34
+ // produces an instance that is the intersection of the instance types.
35
+ type MixedConstructor<
36
+ T extends Constructor,
37
+ K extends readonly Constructor[],
38
+ > = new (
39
+ ...args: [...ConstructorParameters<T>, ...FlattenConstructorParams<K>]
40
+ ) => MixedInstance<T, K>;
41
+
1
42
  /**
2
- * Merges two class prototypes to a new one.
43
+ * Applies mixins to a base class.
44
+ *
45
+ * @param derivedCtor Base class constructor
46
+ * @param constructors Additional constructors that get mixed into the base class
47
+ * @returns A mixed constructor with the instance type of the base class and all mixin classes
48
+ * @example
49
+ * ```
50
+ * const SelectableMovableObject3D = applyMixins(Object3D, [DIVESelectable, DIVEMovable]);
51
+ * const instance = new SelectableMovableObject3D();
52
+ * instance.onMove();
53
+ * instance.onSelect();
54
+ * ```
3
55
  */
4
-
5
- export const applyMixins = (
6
- derivedCtor: { prototype: object },
7
- constructors: { prototype: object }[],
8
- ): void => {
56
+ export function applyMixins<
57
+ T extends Constructor,
58
+ K extends readonly Constructor[],
59
+ >(derivedCtor: T, constructors: K): MixedConstructor<T, K> {
9
60
  constructors.forEach((baseCtor) => {
10
61
  Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
11
62
  Object.defineProperty(
@@ -15,4 +66,5 @@ export const applyMixins = (
15
66
  );
16
67
  });
17
68
  });
18
- };
69
+ return derivedCtor as unknown as MixedConstructor<T, K>;
70
+ }
package/src/info/Info.ts CHANGED
@@ -1,137 +1,161 @@
1
- export enum WebXRUnsupportedReason {
2
- 'UNKNWON_ERROR' = 0,
3
- 'NO_HTTPS' = 1,
4
- 'IMMERSIVE_AR_NOT_SUPPORTED_BY_DEVICE' = 2,
5
- 'AR_SESSION_NOT_ALLOWED' = 3,
6
- }
1
+ import { ESystem, EWebXRUnsupportedReason } from '../types/info';
7
2
 
8
- export class DIVEInfo {
9
- private static _supportsWebXR: boolean | null = null;
10
- private static _webXRUnsupportedReason: WebXRUnsupportedReason | null =
3
+ export class SystemInfo {
4
+ private static _supportsWebXR: boolean = false;
5
+ private static _webXRUnsupportedReason: EWebXRUnsupportedReason | null =
11
6
  null;
12
7
 
13
8
  /**
14
- *
15
- * @returns The system the user is using. Possible values are "Android", "iOS", "Windows", "MacOS", "Linux" or "Unknown".
9
+ * Gets the current system (iOS, Android, Windows, etc.)
10
+ * @returns DIVESystem The current system
16
11
  */
17
- public static GetSystem(): string {
18
- const platform = navigator.platform;
19
- if (/Android/.test(navigator.userAgent)) {
20
- return 'Android';
21
- } else if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
22
- return 'iOS';
23
- } else if (platform.startsWith('Win')) {
24
- return 'Windows';
25
- } else if (platform.startsWith('Mac')) {
26
- return 'MacOS';
27
- } else if (platform.startsWith('Linux')) {
28
- return 'Linux';
29
- } else {
30
- return 'Unknown';
12
+ public static GetSystem(): ESystem {
13
+ if (typeof window === 'undefined' || !window.navigator) {
14
+ return ESystem.UNKNOWN;
15
+ }
16
+
17
+ const userAgent = window.navigator.userAgent.toLowerCase();
18
+ if (userAgent.includes('iphone') || userAgent.includes('ipad')) {
19
+ return ESystem.IOS;
20
+ }
21
+ if (userAgent.includes('android')) {
22
+ return ESystem.ANDROID;
31
23
  }
24
+ if (userAgent.includes('windows')) {
25
+ return ESystem.WINDOWS;
26
+ }
27
+ if (userAgent.includes('macintosh')) {
28
+ return ESystem.MACOS;
29
+ }
30
+ if (userAgent.includes('linux')) {
31
+ return ESystem.LINUX;
32
+ }
33
+ return ESystem.UNKNOWN;
32
34
  }
33
35
 
34
36
  /**
35
37
  * @returns A promise that resolves to a boolean indicating whether the user's device supports WebXR.
36
38
  */
37
39
  public static async GetSupportsWebXR(): Promise<boolean> {
38
- if (this._supportsWebXR !== null) {
40
+ if (this._supportsWebXR !== false) {
39
41
  return this._supportsWebXR;
40
42
  }
41
43
 
42
- // check if XRSystem is available && if https enabled
43
- if (!navigator.xr) {
44
+ // Check if we're in a secure context (HTTPS)
45
+ if (!window.isSecureContext) {
44
46
  this._supportsWebXR = false;
47
+ this._webXRUnsupportedReason = EWebXRUnsupportedReason.NO_HTTPS;
48
+ return this._supportsWebXR;
49
+ }
45
50
 
46
- if (window.isSecureContext === false) {
47
- this._webXRUnsupportedReason = WebXRUnsupportedReason.NO_HTTPS;
48
- } else {
49
- this._webXRUnsupportedReason =
50
- WebXRUnsupportedReason.UNKNWON_ERROR;
51
- }
52
-
51
+ // Check if XRSystem is available
52
+ if (!navigator.xr) {
53
+ this._supportsWebXR = false;
54
+ this._webXRUnsupportedReason = EWebXRUnsupportedReason.NO_WEBXR_API;
53
55
  return this._supportsWebXR;
54
56
  }
55
57
 
56
- // Check if immersive-vr session mode is supported
57
58
  try {
58
- const supported =
59
- await navigator.xr!.isSessionSupported('immersive-ar');
60
- if (!supported) {
59
+ // Check specifically for immersive-ar support
60
+ const arSupported =
61
+ await navigator.xr.isSessionSupported('immersive-ar');
62
+ this._supportsWebXR = arSupported;
63
+
64
+ if (!this._supportsWebXR) {
61
65
  this._webXRUnsupportedReason =
62
- WebXRUnsupportedReason.IMMERSIVE_AR_NOT_SUPPORTED_BY_DEVICE;
66
+ EWebXRUnsupportedReason.IMMERSIVE_AR_NOT_SUPPORTED_BY_DEVICE;
63
67
  }
64
- this._supportsWebXR = supported;
65
68
  } catch (error) {
66
69
  this._supportsWebXR = false;
67
70
  this._webXRUnsupportedReason =
68
- WebXRUnsupportedReason.AR_SESSION_NOT_ALLOWED;
71
+ EWebXRUnsupportedReason.AR_PERMISSION_DENIED;
69
72
  }
73
+
70
74
  return this._supportsWebXR;
71
75
  }
72
76
 
73
77
  /**
74
- * @returns The reason why WebXR is not supported on the user's device. Returns null if WebXR is supported nor not has been checked yet.
78
+ * @returns The reason why WebXR is not supported on the user's device. Returns null if WebXR is supported.
75
79
  */
76
- public static GetWebXRUnsupportedReason(): WebXRUnsupportedReason | null {
77
- if (this._supportsWebXR === null) {
78
- console.log('WebXR support has not been checked yet.');
80
+ public static GetWebXRUnsupportedReason(): EWebXRUnsupportedReason | null {
81
+ if (this._supportsWebXR) {
82
+ console.log('WebXR is supported.');
79
83
  return null;
80
84
  }
81
85
  return this._webXRUnsupportedReason;
82
86
  }
83
87
 
84
88
  /**
85
- * @returns A boolean indicating whether the user's device supports AR Quick Look.
89
+ * Checks if ARQuickLook is supported on the current device
90
+ * This checks for:
91
+ * 1. AR support via relList
92
+ *
93
+ * Requirements:
94
+ * - iOS 13.0 or later
95
+ * - Safari browser (ARQuickLook is only supported in Safari)
96
+ * - Device with AR capabilities (iPhone/iPad with LiDAR scanner or ARKit support)
97
+ *
98
+ * Note: ARQuickLook is only available in Safari on iOS. Other browsers
99
+ * (Chrome, Firefox, etc.) do not support ARQuickLook, even on iOS.
100
+ *
101
+ * @returns boolean indicating if ARQuickLook is supported
86
102
  */
87
103
  public static GetSupportsARQuickLook(): boolean {
88
104
  const a = document.createElement('a');
89
- if (a.relList.supports('ar')) {
90
- return true;
91
- }
92
-
93
- // fallback check
94
- const userAgent = navigator.userAgent;
105
+ return a.relList.supports('ar');
106
+ }
95
107
 
96
- // Check if the device is running iOS
97
- const isIOS =
98
- /iPad|iPhone|iPod/.test(userAgent) &&
99
- !(window as unknown as Window & { MSStream?: string }).MSStream;
100
- if (!isIOS) {
108
+ /**
109
+ * Checks if SceneViewer is supported on the current device
110
+ * This checks for:
111
+ * 1. Android device
112
+ * 2. Chrome browser (version 89 or later)
113
+ *
114
+ * Requirements:
115
+ * - Android 7.0 (API level 24) or later
116
+ * - Chrome for Android 89 or later
117
+ *
118
+ * Note: According to Google's documentation, if these requirements are met,
119
+ * SceneViewer will be available. If ARCore is not installed, SceneViewer will
120
+ * fall back to showing the model in 3D.
121
+ *
122
+ * @returns boolean indicating if SceneViewer is supported
123
+ */
124
+ public static GetSupportsSceneViewer(): boolean {
125
+ // Check if we're in a browser environment
126
+ if (typeof window === 'undefined' || !window.navigator) {
101
127
  return false;
102
128
  }
103
129
 
104
- // Extract iOS version
105
- const match = userAgent.match(/OS (\d+)_/);
106
- if (!match || match.length < 2) {
130
+ const userAgent = window.navigator.userAgent.toLowerCase();
131
+
132
+ // Check if we're on Android
133
+ if (!userAgent.includes('android')) {
107
134
  return false;
108
135
  }
109
- const iOSVersion = parseInt(match[1], 10);
110
-
111
- // Minimum iOS version for QuickLook support
112
- const minQuickLookVersion = 12;
113
136
 
114
- // Check if the iOS version is supported
115
- if (iOSVersion < minQuickLookVersion) {
137
+ // Check if we're using Chrome
138
+ if (!userAgent.includes('chrome')) {
116
139
  return false;
117
140
  }
118
141
 
119
- // Check for supported browser
120
- const isSupportedBrowser =
121
- /^((?!chrome|android).)*safari|CriOS|FxiOS/i.test(userAgent);
122
- if (isSupportedBrowser) {
123
- return true;
142
+ // Check Chrome version (89 or later)
143
+ const chromeVersion = userAgent.match(/chrome\/(\d+)/);
144
+ if (!chromeVersion || parseInt(chromeVersion[1]) < 89) {
145
+ return false;
124
146
  }
125
147
 
126
- // Default to false if none of the conditions are met
127
- return false;
148
+ return true;
128
149
  }
129
150
 
130
151
  /**
131
152
  * @returns A boolean indicating whether the user's device is a mobile device.
132
153
  */
133
154
  public static get isMobile(): boolean {
134
- return this.GetSystem() === 'Android' || this.GetSystem() === 'iOS';
155
+ return (
156
+ this.GetSystem() === ESystem.ANDROID ||
157
+ this.GetSystem() === ESystem.IOS
158
+ );
135
159
  }
136
160
 
137
161
  /**