@shopware-ag/dive 1.16.26-beta.2 → 1.17.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopware-ag/dive",
3
- "version": "1.16.26-beta.2",
3
+ "version": "1.17.0",
4
4
  "description": "Shopware Spatial Framework",
5
5
  "type": "module",
6
6
  "main": "./build/dive.cjs",
@@ -69,6 +69,7 @@
69
69
  "generate-readme": "yarn generate-readme:transpile && yarn generate-readme:write && yarn generate-readme:cleanup",
70
70
  "generate-readme:transpile": "yarn tsc --resolveJsonModule --esModuleInterop ci/readme/generate-readme.ts && mv ci/readme/generate-readme.js ci/readme/generate-readme.cjs",
71
71
  "generate-readme:write": "node ci/readme/generate-readme.cjs",
72
- "generate-readme:cleanup": "node -e \"require('fs').unlinkSync('ci/readme/generate-readme.cjs')\""
72
+ "generate-readme:cleanup": "node -e \"require('fs').unlinkSync('ci/readme/generate-readme.cjs')\"",
73
+ "ci": "yarn lint && yarn coverage && yarn prettier:check && yarn build && bash ci/lint/lint-actions.sh"
73
74
  }
74
75
  }
@@ -208,8 +208,8 @@ describe('dive/DIVE', () => {
208
208
  expect(() => (window as any).DIVE.PrintScene()).not.toThrow();
209
209
  });
210
210
 
211
- it('should instantiate in development NODE_ENV', () => {
212
- process.env.NODE_ENV = 'development';
211
+ it('should instantiate in development DIVE_NODE_ENV', () => {
212
+ process.env.DIVE_NODE_ENV = 'development';
213
213
  const dive = new DIVE();
214
214
  expect(dive).toBeDefined();
215
215
  expect((window as any).DIVE.PrintScene).toBeDefined();
package/src/ar/AR.ts CHANGED
@@ -4,15 +4,22 @@ import { DIVEWebXR } from './webxr/WebXR';
4
4
  import { type DIVEScene } from '../scene/Scene';
5
5
  import { type DIVERenderer } from '../renderer/Renderer';
6
6
  import DIVEOrbitControls from '../controls/OrbitControls';
7
+ import { DIVESceneViewer } from './sceneviewer/SceneViewer';
8
+
9
+ export type DIVEAROptions = {
10
+ arPlacement: 'horizontal' | 'vertical';
11
+ arScale: 'auto' | 'fixed';
12
+ /**
13
+ * experimental, currently deactivated
14
+ */
15
+ useWebXR: false;
16
+ };
7
17
 
8
18
  export class DIVEAR {
9
19
  private _renderer: DIVERenderer;
10
20
  private _scene: DIVEScene;
11
21
  private _controller: DIVEOrbitControls;
12
22
 
13
- private arPlacement: string = 'floor';
14
- private arScale: string = 'auto';
15
-
16
23
  constructor(
17
24
  renderer: DIVERenderer,
18
25
  scene: DIVEScene,
@@ -23,150 +30,69 @@ export class DIVEAR {
23
30
  this._controller = controller;
24
31
  }
25
32
 
26
- public async Launch(): Promise<void> {
33
+ public async Launch(options?: DIVEAROptions): Promise<void> {
27
34
  const system = DIVEInfo.GetSystem();
28
35
 
29
36
  if (system === 'iOS') {
30
- const support = DIVEInfo.GetSupportsARQuickLook();
31
- if (!support) {
32
- console.log('ARQuickLook not supported');
33
- return Promise.reject();
34
- }
35
-
36
- console.log('Launching AR on iOS');
37
-
38
- // Launch ARQuickLook
39
- await DIVEARQuickLook.Launch(this._scene);
40
- return Promise.resolve();
37
+ return this.tryARQuickLook();
41
38
  }
42
39
 
43
40
  if (system === 'Android') {
44
- this.openSceneViewer();
45
- return;
46
-
47
- const support = await DIVEInfo.GetSupportsWebXR();
48
- if (!support) {
49
- console.log(
50
- 'WebXR not supported. Reason: ' +
51
- WebXRUnsupportedReason[
52
- DIVEInfo.GetWebXRUnsupportedReason()!
53
- ],
54
- );
55
- return Promise.reject();
41
+ if (options?.useWebXR) {
42
+ console.warn('DIVE: WebXR is experimental on Android.');
43
+ return this.tryWebXR();
56
44
  }
57
45
 
58
- console.log('Launching AR on Android');
59
- // Launch WebXR
60
- await DIVEWebXR.Launch(
61
- this._renderer,
62
- this._scene,
63
- this._controller,
64
- );
65
- return Promise.resolve();
46
+ return this.trySceneViewer();
66
47
  }
67
48
 
68
49
  console.log(
69
- 'AR not supported. Not a mobile system. (System is ' + system + ')',
50
+ 'DIVE: AR not supported. Not a mobile system. (System is ' +
51
+ system +
52
+ ')',
70
53
  );
71
54
  }
72
55
 
73
- private openSceneViewer(): void {
74
- const src = this.createSceneViewerSrc();
75
- const anchor = document.createElement('a');
76
- const noArViewerSigil = '#model-viewer-no-ar-fallback';
77
- // let isSceneViewerBlocked = false;
78
-
79
- const location = self.location.toString();
80
- const locationUrl = new URL(location);
81
- const modelUrl = new URL(src, location);
82
- if (modelUrl.hash) modelUrl.hash = '';
83
- const params = new URLSearchParams(modelUrl.search);
84
-
85
- locationUrl.hash = noArViewerSigil;
86
-
87
- // modelUrl can contain title/link/sound etc.
88
- params.set('mode', 'ar_preferred');
89
- if (!params.has('disable_occlusion')) {
90
- params.set('disable_occlusion', 'true');
91
- }
92
- if (this.arScale === 'fixed') {
93
- params.set('resizable', 'false');
94
- }
95
- if (this.arPlacement === 'wall') {
96
- params.set('enable_vertical_placement', 'true');
97
- }
98
- if (params.has('sound')) {
99
- const soundUrl = new URL(params.get('sound')!, location);
100
- params.set('sound', soundUrl.toString());
101
- }
102
- if (params.has('link')) {
103
- const linkUrl = new URL(params.get('link')!, location);
104
- params.set('link', linkUrl.toString());
56
+ private async tryARQuickLook(options?: DIVEAROptions): Promise<void> {
57
+ const support = DIVEInfo.GetSupportsARQuickLook();
58
+ if (!support) {
59
+ console.log('ARQuickLook not supported');
60
+ return Promise.reject();
105
61
  }
106
62
 
107
- console.log('modelUrl.toString()', modelUrl.toString());
108
- console.log(
109
- 'encodeURIComponent(modelUrl.toString())',
110
- encodeURIComponent(modelUrl.toString()),
111
- );
63
+ console.log('DIVE: Launching AR with ARQuickLook ...');
112
64
 
113
- const version = '1.0';
114
-
115
- console.log('USING SCENE VIEWER');
116
- console.log('version:', version);
117
- console.log('params:', params.toString());
118
- console.log('modelUrl:', modelUrl.toString());
119
- console.log('locationUrl:', locationUrl.toString());
120
-
121
- const intent = `intent://arvr.google.com/scene-viewer/${version}?${
122
- params.toString() + '&file=' + modelUrl.toString()
123
- }#Intent;scheme=https;package=com.google.android.googlequicksearchbox;action=android.intent.action.VIEW;S.browser_fallback_url=${encodeURIComponent(
124
- locationUrl.toString(),
125
- )};end;`;
126
- // intent =
127
- // 'intent://arvr.google.com/scene-viewer/1.0?file=https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/Avocado/glTF/Avocado.gltf#Intent;scheme=https;package=com.google.android.googlequicksearchbox;action=android.intent.action.VIEW;S.browser_fallback_url=https://developers.google.com/ar;end;';
128
- console.log({ intent });
129
-
130
- const undoHashChange = (): void => {
131
- if (self.location.hash === noArViewerSigil) {
132
- // isSceneViewerBlocked = true;
133
- // The new history will be the current URL with a new hash.
134
- // Go back one step so that we reset to the expected URL.
135
- // NOTE(cdata): this should not invoke any browser-level navigation
136
- // because hash-only changes modify the URL in-place without
137
- // navigating:
138
- self.history.back();
139
- console.warn(
140
- 'Error while trying to present in AR with Scene Viewer',
141
- );
142
- console.warn('Falling back to next ar-mode');
143
- // this[$selectARMode]();
144
- // Would be nice to activateAR() here, but webXR fails due to not
145
- // seeing a user activation.
146
- }
147
- };
65
+ // Launch ARQuickLook
66
+ await DIVEARQuickLook.Launch(this._scene, options);
67
+ return Promise.resolve();
68
+ }
148
69
 
149
- self.addEventListener('hashchange', undoHashChange, { once: true });
70
+ private async tryWebXR(): Promise<void> {
71
+ const support = await DIVEInfo.GetSupportsWebXR();
72
+ if (!support) {
73
+ console.log(
74
+ 'WebXR not supported. Reason: ' +
75
+ WebXRUnsupportedReason[
76
+ DIVEInfo.GetWebXRUnsupportedReason()!
77
+ ],
78
+ );
79
+ return Promise.reject();
80
+ }
150
81
 
151
- anchor.setAttribute('href', intent);
152
- console.log('Attempting to present in AR with Scene Viewer...');
153
- anchor.click();
82
+ console.log('DIVE: Launching AR with WebXR ...');
83
+ // Launch WebXR
84
+ await DIVEWebXR.Launch(this._renderer, this._scene, this._controller);
85
+ return Promise.resolve();
154
86
  }
155
87
 
156
- private createSceneViewerSrc(): string {
157
- let uri: string | null = null;
88
+ private async trySceneViewer(options?: DIVEAROptions): Promise<void> {
89
+ // actually we don't have to try here, because SceneViewer is supported on all devices by now.
90
+ // if there are no AR services (ARCore) installed on the device, SceneViewer will only show the model in 3D.
91
+ // we also have no options to detect if SceneViewer is supported.
158
92
 
159
- this._scene.traverse((object) => {
160
- if (uri) return;
161
- if (object.userData.uri) {
162
- uri = object.userData.uri;
163
- }
164
- });
165
-
166
- if (!uri) {
167
- throw new Error('No model found in scene');
168
- }
93
+ console.log('DIVE: Launching AR with SceneViewer ...');
169
94
 
170
- return uri;
95
+ DIVESceneViewer.Launch(this._scene, options);
96
+ return Promise.resolve();
171
97
  }
172
98
  }
@@ -0,0 +1,187 @@
1
+ import { DIVEAR, type DIVEAROptions } from '../AR';
2
+ import { DIVEInfo } from '../../info/Info';
3
+ import { DIVERenderer } from '../../renderer/Renderer';
4
+ import { DIVEScene } from '../../scene/Scene';
5
+ import DIVEOrbitControls from '../../controls/OrbitControls';
6
+ import { DIVEARQuickLook } from '../arquicklook/ARQuickLook';
7
+ import { DIVESceneViewer } from '../sceneviewer/SceneViewer';
8
+ import { DIVEWebXR } from '../webxr/WebXR';
9
+
10
+ jest.mock('../arquicklook/ARQuickLook', () => ({
11
+ DIVEARQuickLook: {
12
+ Launch: jest.fn(),
13
+ },
14
+ }));
15
+
16
+ jest.mock('../webxr/WebXR', () => ({
17
+ DIVEWebXR: {
18
+ Launch: jest.fn(),
19
+ },
20
+ }));
21
+
22
+ jest.mock('../sceneviewer/SceneViewer', () => ({
23
+ DIVESceneViewer: {
24
+ Launch: jest.fn(),
25
+ },
26
+ }));
27
+
28
+ describe('DIVEAR', () => {
29
+ let renderer: DIVERenderer;
30
+ let scene: DIVEScene;
31
+ let controller: DIVEOrbitControls;
32
+ let diveAR: DIVEAR;
33
+
34
+ beforeEach(() => {
35
+ renderer = {} as DIVERenderer;
36
+ scene = {} as DIVEScene;
37
+ controller = {} as DIVEOrbitControls;
38
+ diveAR = new DIVEAR(renderer, scene, controller);
39
+ });
40
+
41
+ describe('Launch', () => {
42
+ describe('AR Quick Look', () => {
43
+ it('should launch ARQuickLook on iOS', async () => {
44
+ jest.spyOn(DIVEInfo, 'GetSystem').mockReturnValue('iOS');
45
+ jest.spyOn(DIVEInfo, 'GetSupportsARQuickLook').mockReturnValue(
46
+ true,
47
+ );
48
+ const arQuickLookLaunchSpy = jest.spyOn(
49
+ DIVEARQuickLook,
50
+ 'Launch',
51
+ );
52
+
53
+ const consoleLogSpy = jest
54
+ .spyOn(console, 'log')
55
+ .mockImplementation();
56
+
57
+ await diveAR.Launch();
58
+
59
+ expect(arQuickLookLaunchSpy).toHaveBeenCalledWith(
60
+ scene,
61
+ undefined,
62
+ );
63
+ arQuickLookLaunchSpy.mockRestore();
64
+
65
+ expect(consoleLogSpy).toHaveBeenCalled();
66
+ consoleLogSpy.mockRestore();
67
+ });
68
+
69
+ it('should not launch ARQuickLook on iOS if not supported', async () => {
70
+ jest.spyOn(DIVEInfo, 'GetSystem').mockReturnValue('iOS');
71
+ jest.spyOn(DIVEInfo, 'GetSupportsARQuickLook').mockReturnValue(
72
+ false,
73
+ );
74
+ const arQuickLookLaunchSpy = jest.spyOn(
75
+ DIVEARQuickLook,
76
+ 'Launch',
77
+ );
78
+
79
+ const consoleLogSpy = jest
80
+ .spyOn(console, 'log')
81
+ .mockImplementation();
82
+
83
+ await diveAR.Launch().catch(() => {});
84
+
85
+ expect(arQuickLookLaunchSpy).not.toHaveBeenCalled();
86
+ arQuickLookLaunchSpy.mockRestore();
87
+
88
+ expect(consoleLogSpy).toHaveBeenCalled();
89
+ consoleLogSpy.mockRestore();
90
+ });
91
+ });
92
+
93
+ describe('Scene Viewer', () => {
94
+ it('should launch SceneViewer on Android', async () => {
95
+ jest.spyOn(DIVEInfo, 'GetSystem').mockReturnValue('Android');
96
+
97
+ const consoleLogSpy = jest
98
+ .spyOn(console, 'log')
99
+ .mockImplementation();
100
+
101
+ await diveAR.Launch();
102
+
103
+ expect(DIVESceneViewer.Launch).toHaveBeenCalledWith(
104
+ scene,
105
+ undefined,
106
+ );
107
+
108
+ expect(consoleLogSpy).toHaveBeenCalled();
109
+ consoleLogSpy.mockRestore();
110
+ });
111
+ });
112
+
113
+ describe('WebXR', () => {
114
+ it('should launch WebXR on Android with useWebXR option', async () => {
115
+ jest.spyOn(DIVEInfo, 'GetSystem').mockReturnValue('Android');
116
+ jest.spyOn(DIVEInfo, 'GetSupportsWebXR').mockResolvedValue(
117
+ true,
118
+ );
119
+
120
+ const consoleLogSpy = jest
121
+ .spyOn(console, 'log')
122
+ .mockImplementation();
123
+ const consoleWarnSpy = jest
124
+ .spyOn(console, 'warn')
125
+ .mockImplementation();
126
+
127
+ await diveAR.Launch({
128
+ useWebXR: true,
129
+ } as unknown as DIVEAROptions);
130
+
131
+ expect(DIVEWebXR.Launch).toHaveBeenCalledWith(
132
+ renderer,
133
+ scene,
134
+ controller,
135
+ );
136
+ expect(consoleWarnSpy).toHaveBeenCalled();
137
+ consoleWarnSpy.mockRestore();
138
+
139
+ expect(consoleLogSpy).toHaveBeenCalled();
140
+ consoleLogSpy.mockRestore();
141
+ });
142
+
143
+ it('should not launch WebXR on Android with useWebXR option', async () => {
144
+ jest.spyOn(DIVEInfo, 'GetSystem').mockReturnValue('Android');
145
+ jest.spyOn(DIVEInfo, 'GetSupportsWebXR').mockResolvedValue(
146
+ false,
147
+ );
148
+
149
+ const consoleLogSpy = jest
150
+ .spyOn(console, 'log')
151
+ .mockImplementation();
152
+ const consoleWarnSpy = jest
153
+ .spyOn(console, 'warn')
154
+ .mockImplementation();
155
+
156
+ await diveAR
157
+ .Launch({
158
+ useWebXR: true,
159
+ } as unknown as DIVEAROptions)
160
+ .catch(() => {});
161
+
162
+ expect(DIVEWebXR.Launch).toHaveBeenCalledWith(
163
+ renderer,
164
+ scene,
165
+ controller,
166
+ );
167
+ expect(consoleWarnSpy).toHaveBeenCalled();
168
+ consoleWarnSpy.mockRestore();
169
+
170
+ expect(consoleLogSpy).toHaveBeenCalled();
171
+ consoleLogSpy.mockRestore();
172
+ });
173
+ });
174
+
175
+ it('should log AR not supported on non-mobile systems', async () => {
176
+ const consoleLogSpy = jest
177
+ .spyOn(console, 'log')
178
+ .mockImplementation();
179
+
180
+ jest.spyOn(DIVEInfo, 'GetSystem').mockReturnValue('Windows');
181
+
182
+ await diveAR.Launch();
183
+
184
+ expect(consoleLogSpy).toHaveBeenCalled();
185
+ });
186
+ });
187
+ });
@@ -1,11 +1,15 @@
1
1
  import { Object3D } from 'three';
2
+ import { DIVEUSDZExporter } from '../../exporters/usdz/USDZExporter';
2
3
  import { type DIVEScene } from '../../scene/Scene';
3
- import { USDZExporter } from 'three/examples/jsm/exporters/USDZExporter';
4
+ import { type DIVEAROptions } from '../AR';
4
5
 
5
6
  export class DIVEARQuickLook {
6
- private static _usdzExporter: USDZExporter = new USDZExporter();
7
+ private static _usdzExporter: DIVEUSDZExporter = new DIVEUSDZExporter();
7
8
 
8
- public static Launch(scene: DIVEScene): Promise<void> {
9
+ public static Launch(
10
+ scene: DIVEScene,
11
+ options?: DIVEAROptions,
12
+ ): Promise<void> {
9
13
  // create node to build usdz from
10
14
  const quickLookScene = new Object3D();
11
15
 
@@ -13,7 +17,7 @@ export class DIVEARQuickLook {
13
17
  quickLookScene.add(...this.extractModels(scene));
14
18
 
15
19
  // launch ARQuickLook
16
- return this.launchARFromNode(quickLookScene);
20
+ return this.launchARFromNode(quickLookScene, options);
17
21
  }
18
22
 
19
23
  private static extractModels(scene: DIVEScene): Object3D[] {
@@ -21,14 +25,32 @@ export class DIVEARQuickLook {
21
25
  return scene.Root.children;
22
26
  }
23
27
 
24
- private static launchARFromNode(node: Object3D): Promise<void> {
28
+ private static launchARFromNode(
29
+ node: Object3D,
30
+ options?: DIVEAROptions,
31
+ ): Promise<void> {
25
32
  // bundle USDZ
26
33
  return this._usdzExporter
27
- .parse(node, { quickLookCompatible: true })
34
+ .parse(node, {
35
+ quickLookCompatible: true,
36
+ ar: {
37
+ anchoring: { type: 'plane' },
38
+ planeAnchoring: {
39
+ alignment:
40
+ options?.arPlacement === 'vertical'
41
+ ? 'vertical'
42
+ : 'horizontal',
43
+ },
44
+ },
45
+ })
28
46
  .then((usdz: Uint8Array) => {
29
47
  // create blob
30
48
  const blob = new Blob([usdz], { type: 'model/vnd.usdz+zip' });
31
- const url = URL.createObjectURL(blob);
49
+ let url = URL.createObjectURL(blob);
50
+
51
+ if (options?.arScale === 'fixed') {
52
+ url = url.concat('#allowsContentScaling=0');
53
+ }
32
54
 
33
55
  // launch ARQuickLook
34
56
  const a = document.createElement('a');