@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
@@ -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
 
@@ -120,6 +120,11 @@ export class DIVECommunication {
120
120
  let returnValue: Actions[Action]['RETURN'] = false;
121
121
 
122
122
  switch (action) {
123
+ case 'START_RENDER': {
124
+ this.renderer.StartRenderer(this.scene, this.controller.object);
125
+ returnValue = true;
126
+ break;
127
+ }
123
128
  case 'GET_ALL_SCENE_DATA': {
124
129
  returnValue = this.getAllSceneData(
125
130
  payload as Actions['GET_ALL_SCENE_DATA']['PAYLOAD'],
@@ -283,15 +288,13 @@ export class DIVECommunication {
283
288
  break;
284
289
  }
285
290
  case 'LAUNCH_AR': {
291
+ const { uri, options } =
292
+ payload as Actions['LAUNCH_AR']['PAYLOAD'];
286
293
  returnValue = new Promise<void>((resolve, reject) => {
287
294
  this._ar
288
295
  .get()
289
296
  .then((ar) => {
290
- resolve(
291
- ar.Launch(
292
- payload as Actions['LAUNCH_AR']['PAYLOAD'],
293
- ),
294
- );
297
+ resolve(ar.launch(uri, options));
295
298
  })
296
299
  .catch(reject);
297
300
  });