@shopware-ag/dive 1.19.0 → 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 (60) hide show
  1. package/build/dive.cjs +224 -219
  2. package/build/dive.cjs.map +1 -1
  3. package/build/dive.js +233 -21947
  4. package/build/dive.js.map +1 -1
  5. package/build/dive.mjs +26111 -0
  6. package/build/dive.mjs.map +1 -0
  7. package/build/src/ar/AR.d.ts +37 -14
  8. package/build/src/ar/arquicklook/ARQuickLook.d.ts +5 -6
  9. package/build/src/ar/sceneviewer/SceneViewer.d.ts +42 -6
  10. package/build/src/com/actions/scene/launchar.d.ts +5 -2
  11. package/build/src/converter/Converter.d.ts +21 -0
  12. package/build/src/dive.d.ts +2 -2
  13. package/build/src/exporter/Exporter.d.ts +11 -0
  14. package/build/src/helper/applyMixins/applyMixins.d.ts +22 -6
  15. package/build/src/info/Info.d.ts +37 -13
  16. package/build/src/interface/Movable.d.ts +5 -5
  17. package/build/src/interface/Selectable.d.ts +4 -4
  18. package/build/src/loader/Loader.d.ts +11 -0
  19. package/build/src/model/Model.d.ts +2 -2
  20. package/build/src/node/Node.d.ts +3 -3
  21. package/build/src/scene/root/Root.d.ts +1 -1
  22. package/build/src/types/ExporterOptions.d.ts +15 -0
  23. package/build/src/types/FileTypes.d.ts +27 -0
  24. package/build/src/types/index.d.ts +4 -0
  25. package/build/src/types/info/index.d.ts +66 -0
  26. package/package.json +16 -1
  27. package/src/ar/AR.ts +72 -69
  28. package/src/ar/__test__/AR.test.ts +194 -105
  29. package/src/ar/arquicklook/ARQuickLook.ts +32 -72
  30. package/src/ar/arquicklook/__test__/ARQuickLook.test.ts +89 -38
  31. package/src/ar/sceneviewer/SceneViewer.ts +96 -51
  32. package/src/ar/sceneviewer/__test__/SceneViewer.test.ts +144 -47
  33. package/src/ar/webxr/WebXR.ts +5 -4
  34. package/src/com/Communication.ts +5 -7
  35. package/src/com/__test__/Communication.test.ts +9 -3
  36. package/src/com/actions/scene/launchar.ts +2 -2
  37. package/src/converter/Converter.ts +117 -0
  38. package/src/dive.ts +3 -3
  39. package/src/exporter/Exporter.ts +75 -0
  40. package/src/helper/applyMixins/applyMixins.ts +59 -7
  41. package/src/info/Info.ts +99 -75
  42. package/src/info/__test__/Info.test.ts +162 -154
  43. package/src/interface/Movable.ts +5 -5
  44. package/src/interface/Selectable.ts +4 -4
  45. package/src/loader/Loader.ts +48 -0
  46. package/src/model/Model.ts +10 -5
  47. package/src/model/__test__/Model.test.ts +4 -11
  48. package/src/node/Node.ts +7 -5
  49. package/src/scene/root/Root.ts +4 -4
  50. package/src/scene/root/__test__/Root.test.ts +4 -4
  51. package/src/types/ExporterOptions.ts +14 -0
  52. package/src/types/FileTypes.ts +37 -0
  53. package/src/types/index.ts +26 -0
  54. package/src/types/info/index.ts +76 -0
  55. package/build/src/exporters/usdz/USDZExporter.d.ts +0 -15
  56. package/build/src/loadingmanager/LoadingManager.d.ts +0 -14
  57. package/src/exporters/usdz/USDZExporter.ts +0 -21
  58. package/src/exporters/usdz/__test__/USDZExporter.test.ts +0 -57
  59. package/src/loadingmanager/LoadingManager.ts +0 -50
  60. package/src/loadingmanager/__test__/LoadingManager.test.ts +0 -27
@@ -1,7 +1,8 @@
1
1
  import { Box3, Color, Euler, Mesh, Object3D, Vector3 } from 'three';
2
2
  import { DIVEScene } from '../../../scene/Scene';
3
- import { DIVEAROptions } from '../../AR';
4
- import { DIVEARQuickLook } from '../ARQuickLook';
3
+ import { ARSystemOptions } from '../../AR';
4
+ import { ARQuickLook } from '../ARQuickLook';
5
+ import { Converter } from '../../../converter/Converter';
5
6
 
6
7
  jest.mock('../../../scene/Scene', () => {
7
8
  return {
@@ -21,12 +22,33 @@ jest.mock('../../../scene/Scene', () => {
21
22
  };
22
23
  });
23
24
 
25
+ // Mock the Converter class
26
+ jest.mock('../../../converter/Converter', () => {
27
+ return {
28
+ Converter: {
29
+ convert: jest.fn().mockReturnThis(),
30
+ to: jest.fn().mockResolvedValue(new ArrayBuffer(0)),
31
+ },
32
+ };
33
+ });
34
+
35
+ // Mock URL.createObjectURL
24
36
  URL.createObjectURL = jest.fn(() => 'blob:http://localhost:8080/');
25
37
 
26
- describe('DIVEARQuickLook', () => {
27
- let mockScene: DIVEScene;
28
- let mockOptions: DIVEAROptions;
38
+ // Mock document.createElement
39
+ document.createElement = jest.fn().mockReturnValue({
40
+ innerHTML: '',
41
+ rel: '',
42
+ href: '',
43
+ download: '',
44
+ click: jest.fn(),
45
+ });
46
+
47
+ describe('ARQuickLook', () => {
48
+ let mockOptions: ARSystemOptions;
29
49
  let mockModels: Object3D[];
50
+ let quickLook: ARQuickLook;
51
+ const mockUri = 'https://example.com/model.glb';
30
52
 
31
53
  beforeEach(() => {
32
54
  mockModels = [
@@ -37,57 +59,86 @@ describe('DIVEARQuickLook', () => {
37
59
  mockModels[1].userData = {
38
60
  uri: 'https://example.com',
39
61
  };
40
- mockScene = new DIVEScene();
41
62
  mockOptions = {
42
63
  arPlacement: 'horizontal',
43
64
  arScale: 'auto',
44
- } as DIVEAROptions;
65
+ };
66
+ quickLook = new ARQuickLook();
67
+ jest.clearAllMocks();
68
+ });
69
+
70
+ describe('constructor', () => {
71
+ it('should create an instance', () => {
72
+ expect(quickLook).toBeInstanceOf(ARQuickLook);
73
+ });
45
74
  });
46
75
 
47
- describe('Launch', () => {
48
- it('should be a function', () => {
49
- expect(DIVEARQuickLook.Launch).toBeInstanceOf(Function);
76
+ describe('launch', () => {
77
+ it('should convert and launch with default options', async () => {
78
+ await quickLook.launch(mockUri);
79
+
80
+ expect(Converter.convert).toHaveBeenCalledWith(mockUri);
81
+ expect((Converter as any).to).toHaveBeenCalledWith('usdz', {
82
+ quickLookCompatible: true,
83
+ ar: {
84
+ anchoring: { type: 'plane' },
85
+ planeAnchoring: { alignment: 'horizontal' },
86
+ },
87
+ });
88
+ expect(URL.createObjectURL).toHaveBeenCalled();
89
+ expect(document.createElement).toHaveBeenCalledWith('a');
50
90
  });
51
91
 
52
- it('should not throw without options', () => {
53
- mockScene.Root.children = mockModels;
92
+ it('should convert and launch with custom options', async () => {
93
+ const options: ARSystemOptions = {
94
+ arPlacement: 'vertical',
95
+ arScale: 'fixed',
96
+ };
97
+ await quickLook.launch(mockUri, options);
54
98
 
55
- expect(() => {
56
- DIVEARQuickLook.Launch(mockScene);
57
- }).not.toThrow();
99
+ expect(Converter.convert).toHaveBeenCalledWith(mockUri);
100
+ expect((Converter as any).to).toHaveBeenCalledWith('usdz', {
101
+ quickLookCompatible: true,
102
+ ar: {
103
+ anchoring: { type: 'plane' },
104
+ planeAnchoring: { alignment: 'vertical' },
105
+ },
106
+ });
107
+ expect(URL.createObjectURL).toHaveBeenCalled();
108
+ expect(document.createElement).toHaveBeenCalledWith('a');
58
109
  });
59
110
 
60
- it('should not throw with options', () => {
61
- mockScene.Root.children = mockModels;
111
+ it('should handle conversion errors', async () => {
112
+ const error = new Error('Conversion failed');
113
+ ((Converter as any).to as jest.Mock).mockRejectedValueOnce(error);
62
114
 
63
- expect(() => {
64
- DIVEARQuickLook.Launch(mockScene, mockOptions);
65
- }).not.toThrow();
115
+ await expect(quickLook.launch(mockUri)).rejects.toThrow(error);
66
116
  });
67
117
 
68
- it('should not throw with alternated options', () => {
69
- mockScene.Root.children = mockModels;
118
+ it('should create a blob with correct MIME type', async () => {
119
+ const mockBuffer = new ArrayBuffer(100);
120
+ ((Converter as any).to as jest.Mock).mockResolvedValueOnce(
121
+ mockBuffer,
122
+ );
70
123
 
71
- mockOptions = {
72
- arPlacement: 'vertical',
73
- arScale: 'fixed',
74
- } as DIVEAROptions;
124
+ await quickLook.launch(mockUri);
75
125
 
76
- expect(() => {
77
- DIVEARQuickLook.Launch(mockScene, mockOptions);
78
- }).not.toThrow();
126
+ expect(URL.createObjectURL).toHaveBeenCalledWith(
127
+ expect.objectContaining({
128
+ type: 'model/vnd.usdz+zip',
129
+ }),
130
+ );
79
131
  });
80
132
 
81
- it('should throw if no url is found', () => {
82
- mockScene.Root.children = [
83
- new Object3D(),
84
- new Object3D(),
85
- new Object3D(),
86
- ];
133
+ it('should add scale parameter when arScale is fixed', async () => {
134
+ const options: ARSystemOptions = {
135
+ arPlacement: 'horizontal',
136
+ arScale: 'fixed',
137
+ };
138
+ await quickLook.launch(mockUri, options);
87
139
 
88
- expect(() => {
89
- DIVEARQuickLook.Launch(mockScene, mockOptions);
90
- }).toThrow();
140
+ const anchor = document.createElement('a');
141
+ expect(anchor.href).toContain('#allowsContentScaling=0');
91
142
  });
92
143
  });
93
144
  });
@@ -1,74 +1,119 @@
1
- import { type DIVEScene } from '../../scene/Scene';
2
- import { type DIVEAROptions } from '../AR';
1
+ import { type ARSystemOptions } from '../AR';
3
2
 
4
- export class DIVESceneViewer {
5
- public static Launch(scene: DIVEScene, options?: DIVEAROptions): void {
6
- // find url in scene (first object found that has a set uri)
7
- const url = this.findSceneViewerSrc(scene);
3
+ export class SceneViewer {
4
+ public launch(uri: string, options?: ARSystemOptions): void {
5
+ const location = self.location.toString();
6
+ const anchor = document.createElement('a');
7
+ const params = this._createParams(location, uri, options);
8
+ const intent = this._createIntent(location, uri, params);
8
9
 
9
- // launch SceneViewer
10
- this.launchSceneViewer(url, options);
10
+ anchor.setAttribute('href', intent);
11
+ anchor.click();
11
12
  }
12
13
 
13
- private static launchSceneViewer(
14
- url: string,
15
- options?: DIVEAROptions,
16
- ): void {
17
- const anchor = document.createElement('a');
18
- const noArViewerSigil = '#model-viewer-no-ar-fallback';
19
-
20
- const location = self.location.toString();
21
- const locationUrl = new URL(location);
22
- const modelUrl = new URL(url, location);
14
+ /**
15
+ * Creates the base URL parameters for SceneViewer
16
+ * @param location Current page location URL
17
+ * @returns URLSearchParams with base configuration
18
+ */
19
+ private _createParams(
20
+ location: string,
21
+ uri: string,
22
+ options?: ARSystemOptions,
23
+ ): URLSearchParams {
24
+ const modelUrl = new URL(uri, location);
23
25
  const params = new URLSearchParams(modelUrl.search);
24
26
 
25
- locationUrl.hash = noArViewerSigil;
27
+ // Set AR mode as preferred
28
+ params.set('mode', 'ar_preferred');
29
+
30
+ // Apply any custom options
31
+ this._applyScaleOption(params, options);
32
+ this._applyPlacementOption(params, options);
26
33
 
27
- // modelUrl can contain title/link/sound etc.
28
- params.set('mode', 'ar_only');
34
+ // Apply additional parameters if present
35
+ this._applySoundOption(params, location);
36
+ this._applyLinkOption(params, location);
29
37
 
38
+ return params;
39
+ }
40
+
41
+ /**
42
+ * Applies the scale option to the parameters
43
+ * If scale is set to 'fixed', the model will not be resizable in AR
44
+ * @param params URLSearchParams to modify
45
+ */
46
+ private _applyScaleOption(
47
+ params: URLSearchParams,
48
+ options?: ARSystemOptions,
49
+ ): void {
30
50
  if (options?.arScale === 'fixed') {
31
51
  params.set('resizable', 'false');
32
52
  }
53
+ }
33
54
 
55
+ /**
56
+ * Applies the placement option to the parameters
57
+ * If placement is set to 'vertical', vertical placement will be enabled
58
+ * @param params URLSearchParams to modify
59
+ */
60
+ private _applyPlacementOption(
61
+ params: URLSearchParams,
62
+ options?: ARSystemOptions,
63
+ ): void {
34
64
  if (options?.arPlacement === 'vertical') {
35
65
  params.set('enable_vertical_placement', 'true');
36
66
  }
67
+ }
37
68
 
38
- // will be added later if needed
39
- // if (params.has('sound')) {
40
- // const soundUrl = new URL(params.get('sound')!, location);
41
- // params.set('sound', soundUrl.toString());
42
- // }
43
- // if (params.has('link')) {
44
- // const linkUrl = new URL(params.get('link')!, location);
45
- // params.set('link', linkUrl.toString());
46
- // }
47
-
48
- const intent = `intent://arvr.google.com/scene-viewer/1.2?${
49
- params.toString() + '&file=' + modelUrl.toString()
50
- }#Intent;scheme=https;package=com.google.android.googlequicksearchbox;action=android.intent.action.VIEW;S.browser_fallback_url=${encodeURIComponent(
51
- locationUrl.toString(),
52
- )};end;`;
53
-
54
- anchor.setAttribute('href', intent);
55
- anchor.click();
69
+ /**
70
+ * Applies the sound option to the parameters if present
71
+ * This will resolve any relative sound URLs to absolute URLs
72
+ * @param params URLSearchParams to modify
73
+ * @param location Current page location URL
74
+ */
75
+ private _applySoundOption(params: URLSearchParams, location: string): void {
76
+ if (params.has('sound')) {
77
+ const soundUrl = new URL(params.get('sound')!, location);
78
+ params.set('sound', soundUrl.toString());
79
+ }
56
80
  }
57
81
 
58
- private static findSceneViewerSrc(scene: DIVEScene): string {
59
- let uri: string | null = null;
82
+ /**
83
+ * Applies the link option to the parameters if present
84
+ * This will resolve any relative link URLs to absolute URLs
85
+ * @param params URLSearchParams to modify
86
+ * @param location Current page location URL
87
+ */
88
+ private _applyLinkOption(params: URLSearchParams, location: string): void {
89
+ if (params.has('link')) {
90
+ const linkUrl = new URL(params.get('link')!, location);
91
+ params.set('link', linkUrl.toString());
92
+ }
93
+ }
60
94
 
61
- scene.traverse((object) => {
62
- if (uri) return;
63
- if (object.userData.uri) {
64
- uri = object.userData.uri;
65
- }
66
- });
95
+ /**
96
+ * Creates the Android Intent URL for SceneViewer
97
+ * @param params URLSearchParams containing all configuration
98
+ * @param location Current page location URL
99
+ * @returns The complete Intent URL
100
+ */
101
+ private _createIntent(
102
+ location: string,
103
+ uri: string,
104
+ params: URLSearchParams,
105
+ ): string {
106
+ const locationUrl = new URL(location);
107
+ const modelUrl = new URL(uri, location);
108
+ const noArViewerSigil = '#model-viewer-no-ar-fallback';
67
109
 
68
- if (!uri) {
69
- throw new Error('No model found in scene');
70
- }
110
+ locationUrl.hash = noArViewerSigil;
71
111
 
72
- return uri;
112
+ // Construct the intent URL with all parameters
113
+ return `intent://arvr.google.com/scene-viewer/1.2?${
114
+ params.toString() + '&file=' + modelUrl.toString()
115
+ }#Intent;scheme=https;package=com.google.android.googlequicksearchbox;action=android.intent.action.VIEW;S.browser_fallback_url=${encodeURIComponent(
116
+ locationUrl.toString(),
117
+ )};end;`;
73
118
  }
74
119
  }
@@ -1,7 +1,16 @@
1
1
  import { Box3, Color, Euler, Mesh, Object3D, Vector3 } from 'three';
2
2
  import { DIVEScene } from '../../../scene/Scene';
3
- import { DIVEAROptions } from '../../AR';
4
- import { DIVESceneViewer } from '../SceneViewer';
3
+ import { ARSystemOptions } from '../../AR';
4
+ import { SceneViewer } from '../SceneViewer';
5
+ import { SystemInfo } from '../../../info/Info';
6
+
7
+ // Mock DIVEInfo
8
+ jest.mock('../../../info/Info', () => ({
9
+ DIVEInfo: {
10
+ GetSystem: jest.fn().mockReturnValue('Android'),
11
+ GetSupportsARQuickLook: jest.fn().mockReturnValue(false),
12
+ },
13
+ }));
5
14
 
6
15
  jest.mock('../../../scene/Scene', () => {
7
16
  return {
@@ -21,73 +30,161 @@ jest.mock('../../../scene/Scene', () => {
21
30
  };
22
31
  });
23
32
 
24
- URL.createObjectURL = jest.fn(() => 'blob:http://localhost:8080/');
33
+ // Mock URL and document APIs
34
+ const mockLocation = new URL('https://example.com');
35
+ const mockCreateElement = jest.fn();
36
+ const mockSetAttribute = jest.fn();
37
+ const mockClick = jest.fn();
38
+
39
+ Object.defineProperty(window, 'location', {
40
+ value: mockLocation,
41
+ writable: true,
42
+ });
43
+
44
+ document.createElement = mockCreateElement.mockReturnValue({
45
+ setAttribute: mockSetAttribute,
46
+ click: mockClick,
47
+ });
25
48
 
26
49
  describe('DIVESceneViewer', () => {
27
- let mockScene: DIVEScene;
28
- let mockOptions: DIVEAROptions;
29
- let mockModels: Object3D[];
50
+ const mockUri = 'https://example.com/model.glb';
51
+ let mockOptions: ARSystemOptions;
30
52
 
31
53
  beforeEach(() => {
32
- mockModels = [
33
- new Object3D(),
34
- new Object3D(),
35
- new Object3D(),
36
- ];
37
- mockModels[1].userData = {
38
- uri: 'https://example.com',
39
- };
40
- mockScene = new DIVEScene();
41
54
  mockOptions = {
42
55
  arPlacement: 'horizontal',
43
56
  arScale: 'auto',
44
- } as DIVEAROptions;
57
+ };
58
+ jest.clearAllMocks();
45
59
  });
46
60
 
47
- describe('Launch', () => {
48
- it('should be a function', () => {
49
- expect(DIVESceneViewer.Launch).toBeInstanceOf(Function);
61
+ describe('constructor', () => {
62
+ it('should create an instance', () => {
63
+ const sceneViewer = new SceneViewer();
64
+ expect(sceneViewer).toBeInstanceOf(SceneViewer);
50
65
  });
66
+ });
51
67
 
52
- it('should not throw without options', () => {
53
- mockScene.Root.children = mockModels;
68
+ describe('launch', () => {
69
+ it('should launch with default options', () => {
70
+ const sceneViewer = new SceneViewer();
71
+ sceneViewer.launch(mockUri);
54
72
 
55
- expect(() => {
56
- DIVESceneViewer.Launch(mockScene);
57
- }).not.toThrow();
73
+ expect(mockCreateElement).toHaveBeenCalledWith('a');
74
+ expect(mockSetAttribute).toHaveBeenCalledWith(
75
+ 'href',
76
+ expect.stringContaining('mode=ar_preferred'),
77
+ );
78
+ expect(mockClick).toHaveBeenCalled();
58
79
  });
59
80
 
60
- it('should not throw with options', () => {
61
- mockScene.Root.children = mockModels;
81
+ it('should launch with custom options', () => {
82
+ const options: ARSystemOptions = {
83
+ arPlacement: 'vertical',
84
+ arScale: 'fixed',
85
+ };
86
+ const sceneViewer = new SceneViewer();
87
+ sceneViewer.launch(mockUri, options);
62
88
 
63
- expect(() => {
64
- DIVESceneViewer.Launch(mockScene, mockOptions);
65
- }).not.toThrow();
89
+ expect(mockCreateElement).toHaveBeenCalledWith('a');
90
+ expect(mockSetAttribute).toHaveBeenCalledWith(
91
+ 'href',
92
+ expect.stringContaining('enable_vertical_placement=true'),
93
+ );
94
+ expect(mockSetAttribute).toHaveBeenCalledWith(
95
+ 'href',
96
+ expect.stringContaining('resizable=false'),
97
+ );
98
+ expect(mockClick).toHaveBeenCalled();
66
99
  });
67
100
 
68
- it('should not throw with alternated options', () => {
69
- mockScene.Root.children = mockModels;
101
+ it('should handle sound parameter in URL', () => {
102
+ const sceneViewer = new SceneViewer();
103
+ const params = new URLSearchParams();
104
+ params.set('sound', 'sound.mp3');
70
105
 
71
- mockOptions = {
72
- arPlacement: 'vertical',
73
- arScale: 'fixed',
74
- } as DIVEAROptions;
106
+ // Access private method for testing
107
+ const applySoundOption = (sceneViewer as any)._applySoundOption;
108
+ applySoundOption(params, mockLocation.toString());
109
+
110
+ expect(params.get('sound')).toBe('https://example.com/sound.mp3');
111
+ });
112
+
113
+ it('should handle link parameter in URL', () => {
114
+ const sceneViewer = new SceneViewer();
115
+ const params = new URLSearchParams();
116
+ params.set('link', 'details.html');
117
+
118
+ // Access private method for testing
119
+ const applyLinkOption = (sceneViewer as any)._applyLinkOption;
120
+ applyLinkOption(params, mockLocation.toString());
121
+
122
+ expect(params.get('link')).toBe('https://example.com/details.html');
123
+ });
124
+
125
+ it('should create intent URL with correct parameters', () => {
126
+ const sceneViewer = new SceneViewer();
127
+ const params = new URLSearchParams();
128
+ params.set('mode', 'ar_preferred');
129
+
130
+ // Access private method for testing and bind it to the instance
131
+ const createIntent = (sceneViewer as any)._createIntent.bind(
132
+ sceneViewer,
133
+ );
134
+ const intentUrl = createIntent(
135
+ mockLocation.toString(),
136
+ mockUri,
137
+ params,
138
+ );
139
+
140
+ expect(intentUrl).toContain(
141
+ 'intent://arvr.google.com/scene-viewer/1.2',
142
+ );
143
+ expect(intentUrl).toContain('mode=ar_preferred');
144
+ expect(intentUrl).toContain('file=' + mockUri);
145
+ expect(intentUrl).toContain('scheme=https');
146
+ expect(intentUrl).toContain(
147
+ 'package=com.google.android.googlequicksearchbox',
148
+ );
149
+ });
150
+
151
+ it('should handle relative model URLs', () => {
152
+ const relativeUri = '/models/model.glb';
153
+ const sceneViewer = new SceneViewer();
154
+ sceneViewer.launch(relativeUri);
155
+
156
+ expect(mockSetAttribute).toHaveBeenCalledWith(
157
+ 'href',
158
+ expect.stringContaining(
159
+ 'file=https://example.com/models/model.glb',
160
+ ),
161
+ );
162
+ });
163
+
164
+ it('should handle absolute model URLs', () => {
165
+ const absoluteUri = 'https://cdn.example.com/model.glb';
166
+ const sceneViewer = new SceneViewer();
167
+ sceneViewer.launch(absoluteUri);
75
168
 
76
- expect(() => {
77
- DIVESceneViewer.Launch(mockScene, mockOptions);
78
- }).not.toThrow();
169
+ expect(mockSetAttribute).toHaveBeenCalledWith(
170
+ 'href',
171
+ expect.stringContaining(
172
+ 'file=https://cdn.example.com/model.glb',
173
+ ),
174
+ );
79
175
  });
80
176
 
81
- it('should throw if no url is found', () => {
82
- mockScene.Root.children = [
83
- new Object3D(),
84
- new Object3D(),
85
- new Object3D(),
86
- ];
177
+ it('should handle special characters in model URL', () => {
178
+ const specialUri = 'https://example.com/model with spaces.glb';
179
+ const sceneViewer = new SceneViewer();
180
+ sceneViewer.launch(specialUri);
87
181
 
88
- expect(() => {
89
- DIVESceneViewer.Launch(mockScene, mockOptions);
90
- }).toThrow();
182
+ expect(mockSetAttribute).toHaveBeenCalledWith(
183
+ 'href',
184
+ expect.stringContaining(
185
+ 'file=https://example.com/model%20with%20spaces.glb',
186
+ ),
187
+ );
91
188
  });
92
189
  });
93
190
  });
@@ -72,10 +72,11 @@ export class DIVEWebXR {
72
72
  DIVEWebXR._options.domOverlay = { root: DIVEWebXR._overlay.Element };
73
73
 
74
74
  // request session
75
- const session = await navigator.xr.requestSession(
76
- 'immersive-ar',
77
- this._options,
78
- );
75
+ const session = await navigator.xr
76
+ .requestSession('immersive-ar', this._options)
77
+ .catch((reason) => {
78
+ return Promise.reject(reason);
79
+ });
79
80
  session.addEventListener('end', () => {
80
81
  this._onSessionEnded();
81
82
  });
@@ -22,7 +22,7 @@ import { type DIVEMediaCreator } from '../mediacreator/MediaCreator.ts';
22
22
  import { type DIVERenderer } from '../renderer/Renderer.ts';
23
23
  import { type DIVESelectable } from '../interface/Selectable.ts';
24
24
  import { type DIVEIO } from '../io/IO.ts';
25
- import { type DIVEAR } from '../ar/AR.ts';
25
+ import { type ARSystem } from '../ar/AR.ts';
26
26
 
27
27
  type EventListener<Action extends keyof Actions> = (
28
28
  payload: Actions[Action]['PAYLOAD'],
@@ -81,7 +81,7 @@ export class DIVECommunication {
81
81
  );
82
82
  private _io: DIVEModule<DIVEIO> = new DIVEModule('../io/IO.ts', 'DIVEIO');
83
83
 
84
- private _ar: DIVEModule<DIVEAR> = new DIVEModule('../ar/AR.ts', 'DIVEAR');
84
+ private _ar: DIVEModule<ARSystem> = new DIVEModule('../ar/AR.ts', 'DIVEAR');
85
85
 
86
86
  private registered: Map<string, COMEntity> = new Map();
87
87
 
@@ -288,15 +288,13 @@ export class DIVECommunication {
288
288
  break;
289
289
  }
290
290
  case 'LAUNCH_AR': {
291
+ const { uri, options } =
292
+ payload as Actions['LAUNCH_AR']['PAYLOAD'];
291
293
  returnValue = new Promise<void>((resolve, reject) => {
292
294
  this._ar
293
295
  .get()
294
296
  .then((ar) => {
295
- resolve(
296
- ar.Launch(
297
- payload as Actions['LAUNCH_AR']['PAYLOAD'],
298
- ),
299
- );
297
+ resolve(ar.launch(uri, options));
300
298
  })
301
299
  .catch(reject);
302
300
  });
@@ -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({})),
@@ -1005,14 +1006,19 @@ describe('dive/communication/DIVECommunication', () => {
1005
1006
 
1006
1007
  it('should perform action LAUNCH_AR', async () => {
1007
1008
  jest.spyOn(mockModule, 'get').mockResolvedValue({
1008
- Launch: jest.fn(),
1009
+ launch: jest.fn(),
1009
1010
  });
1010
1011
  const arModule = await testCom['_ar'].get();
1011
1012
  const arLaunchSpy = jest
1012
- .spyOn(arModule, 'Launch')
1013
+ .spyOn(arModule, 'launch')
1013
1014
  .mockResolvedValueOnce();
1014
1015
 
1015
- 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
+ });
1016
1022
  expect(arLaunchSpy).toHaveBeenCalledTimes(1);
1017
1023
  });
1018
1024
 
@@ -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
  }