@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.
- package/build/dive.cjs +224 -219
- package/build/dive.cjs.map +1 -1
- package/build/dive.js +233 -21947
- package/build/dive.js.map +1 -1
- package/build/dive.mjs +26111 -0
- package/build/dive.mjs.map +1 -0
- package/build/src/ar/AR.d.ts +37 -14
- package/build/src/ar/arquicklook/ARQuickLook.d.ts +5 -6
- package/build/src/ar/sceneviewer/SceneViewer.d.ts +42 -6
- package/build/src/com/actions/scene/launchar.d.ts +5 -2
- package/build/src/converter/Converter.d.ts +21 -0
- package/build/src/dive.d.ts +2 -2
- package/build/src/exporter/Exporter.d.ts +11 -0
- package/build/src/helper/applyMixins/applyMixins.d.ts +22 -6
- package/build/src/info/Info.d.ts +37 -13
- package/build/src/interface/Movable.d.ts +5 -5
- package/build/src/interface/Selectable.d.ts +4 -4
- package/build/src/loader/Loader.d.ts +11 -0
- package/build/src/model/Model.d.ts +2 -2
- package/build/src/node/Node.d.ts +3 -3
- package/build/src/scene/root/Root.d.ts +1 -1
- package/build/src/types/ExporterOptions.d.ts +15 -0
- package/build/src/types/FileTypes.d.ts +27 -0
- package/build/src/types/index.d.ts +4 -0
- package/build/src/types/info/index.d.ts +66 -0
- package/package.json +16 -1
- package/src/ar/AR.ts +72 -69
- package/src/ar/__test__/AR.test.ts +194 -105
- package/src/ar/arquicklook/ARQuickLook.ts +32 -72
- package/src/ar/arquicklook/__test__/ARQuickLook.test.ts +89 -38
- package/src/ar/sceneviewer/SceneViewer.ts +96 -51
- package/src/ar/sceneviewer/__test__/SceneViewer.test.ts +144 -47
- package/src/ar/webxr/WebXR.ts +5 -4
- package/src/com/Communication.ts +5 -7
- package/src/com/__test__/Communication.test.ts +9 -3
- package/src/com/actions/scene/launchar.ts +2 -2
- package/src/converter/Converter.ts +117 -0
- package/src/dive.ts +3 -3
- package/src/exporter/Exporter.ts +75 -0
- package/src/helper/applyMixins/applyMixins.ts +59 -7
- package/src/info/Info.ts +99 -75
- package/src/info/__test__/Info.test.ts +162 -154
- package/src/interface/Movable.ts +5 -5
- package/src/interface/Selectable.ts +4 -4
- package/src/loader/Loader.ts +48 -0
- package/src/model/Model.ts +10 -5
- package/src/model/__test__/Model.test.ts +4 -11
- package/src/node/Node.ts +7 -5
- package/src/scene/root/Root.ts +4 -4
- package/src/scene/root/__test__/Root.test.ts +4 -4
- package/src/types/ExporterOptions.ts +14 -0
- package/src/types/FileTypes.ts +37 -0
- package/src/types/index.ts +26 -0
- package/src/types/info/index.ts +76 -0
- package/build/src/exporters/usdz/USDZExporter.d.ts +0 -15
- package/build/src/loadingmanager/LoadingManager.d.ts +0 -14
- package/src/exporters/usdz/USDZExporter.ts +0 -21
- package/src/exporters/usdz/__test__/USDZExporter.test.ts +0 -57
- package/src/loadingmanager/LoadingManager.ts +0 -50
- 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 {
|
|
4
|
-
import {
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
}
|
|
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('
|
|
48
|
-
it('should
|
|
49
|
-
|
|
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
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
61
|
-
|
|
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
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
arPlacement: 'vertical',
|
|
73
|
-
arScale: 'fixed',
|
|
74
|
-
} as DIVEAROptions;
|
|
124
|
+
await quickLook.launch(mockUri);
|
|
75
125
|
|
|
76
|
-
expect((
|
|
77
|
-
|
|
78
|
-
|
|
126
|
+
expect(URL.createObjectURL).toHaveBeenCalledWith(
|
|
127
|
+
expect.objectContaining({
|
|
128
|
+
type: 'model/vnd.usdz+zip',
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
79
131
|
});
|
|
80
132
|
|
|
81
|
-
it('should
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
|
2
|
-
import { type DIVEAROptions } from '../AR';
|
|
1
|
+
import { type ARSystemOptions } from '../AR';
|
|
3
2
|
|
|
4
|
-
export class
|
|
5
|
-
public
|
|
6
|
-
|
|
7
|
-
const
|
|
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
|
-
|
|
10
|
-
|
|
10
|
+
anchor.setAttribute('href', intent);
|
|
11
|
+
anchor.click();
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
28
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
69
|
-
throw new Error('No model found in scene');
|
|
70
|
-
}
|
|
110
|
+
locationUrl.hash = noArViewerSigil;
|
|
71
111
|
|
|
72
|
-
|
|
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 {
|
|
4
|
-
import {
|
|
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
|
|
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
|
-
|
|
28
|
-
let mockOptions:
|
|
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
|
-
}
|
|
57
|
+
};
|
|
58
|
+
jest.clearAllMocks();
|
|
45
59
|
});
|
|
46
60
|
|
|
47
|
-
describe('
|
|
48
|
-
it('should
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
68
|
+
describe('launch', () => {
|
|
69
|
+
it('should launch with default options', () => {
|
|
70
|
+
const sceneViewer = new SceneViewer();
|
|
71
|
+
sceneViewer.launch(mockUri);
|
|
54
72
|
|
|
55
|
-
expect(()
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
});
|
package/src/ar/webxr/WebXR.ts
CHANGED
|
@@ -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
|
|
76
|
-
'immersive-ar',
|
|
77
|
-
|
|
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
|
});
|
package/src/com/Communication.ts
CHANGED
|
@@ -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
|
|
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<
|
|
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
|
-
|
|
1009
|
+
launch: jest.fn(),
|
|
1009
1010
|
});
|
|
1010
1011
|
const arModule = await testCom['_ar'].get();
|
|
1011
1012
|
const arLaunchSpy = jest
|
|
1012
|
-
.spyOn(arModule, '
|
|
1013
|
+
.spyOn(arModule, 'launch')
|
|
1013
1014
|
.mockResolvedValueOnce();
|
|
1014
1015
|
|
|
1015
|
-
const result = await testCom.PerformAction('LAUNCH_AR',
|
|
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
|
|
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?:
|
|
5
|
+
PAYLOAD: { uri: string; options?: ARSystemOptions };
|
|
6
6
|
RETURN: Promise<void>;
|
|
7
7
|
}
|