@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.
@@ -0,0 +1,268 @@
1
+ import { Box3, Color, Euler, Mesh, Object3D, Vector3 } from 'three';
2
+ import { DIVEScene } from '../../../scene/Scene';
3
+ import { DIVEAROptions } from '../../AR';
4
+ import { DIVEARQuickLook } from '../ARQuickLook';
5
+
6
+ jest.mock('three', () => {
7
+ return {
8
+ Vector3: jest.fn(function (
9
+ x: number = 0,
10
+ y: number = 0,
11
+ z: number = 0,
12
+ ) {
13
+ this.x = x;
14
+ this.y = y;
15
+ this.z = z;
16
+ this.copy = (vec3: Vector3) => {
17
+ this.x = vec3.x;
18
+ this.y = vec3.y;
19
+ this.z = vec3.z;
20
+ return this;
21
+ };
22
+ this.set = (x: number, y: number, z: number) => {
23
+ this.x = x;
24
+ this.y = y;
25
+ this.z = z;
26
+ return this;
27
+ };
28
+ this.multiply = (vec3: Vector3) => {
29
+ this.x *= vec3.x;
30
+ this.y *= vec3.y;
31
+ this.z *= vec3.z;
32
+ return this;
33
+ };
34
+ this.clone = () => {
35
+ return new Vector3(this.x, this.y, this.z);
36
+ };
37
+ this.setY = (y: number) => {
38
+ this.y = y;
39
+ return this;
40
+ };
41
+ this.add = (vec3: Vector3) => {
42
+ this.x += vec3.x;
43
+ this.y += vec3.y;
44
+ this.z += vec3.z;
45
+ return this;
46
+ };
47
+ this.sub = (vec3: Vector3) => {
48
+ this.x -= vec3.x;
49
+ this.y -= vec3.y;
50
+ this.z -= vec3.z;
51
+ return this;
52
+ };
53
+ return this;
54
+ }),
55
+ Euler: jest.fn(function () {
56
+ this.set = jest.fn();
57
+ return this;
58
+ }),
59
+ Object3D: jest.fn(function () {
60
+ this.clear = jest.fn();
61
+ this.color = {};
62
+ this.intensity = 0;
63
+ this.layers = {
64
+ mask: 0,
65
+ };
66
+ this.shadow = {
67
+ radius: 0,
68
+ mapSize: { width: 0, height: 0 },
69
+ bias: 0,
70
+ camera: {
71
+ near: 0,
72
+ far: 0,
73
+ fov: 0,
74
+ },
75
+ };
76
+ this.add = jest.fn();
77
+ this.sub = jest.fn();
78
+ this.children = [
79
+ {
80
+ visible: true,
81
+ material: {
82
+ color: {},
83
+ },
84
+ },
85
+ ];
86
+ this.userData = {};
87
+ this.position = new Vector3();
88
+ this.rotation = new Euler();
89
+ this.scale = new Vector3(1, 1, 1);
90
+ this.localToWorld = (vec3: Vector3) => {
91
+ return vec3;
92
+ };
93
+ this.mesh = new Mesh();
94
+ this.traverse = jest.fn((callback) => {
95
+ callback(this.children[0]);
96
+ });
97
+ this.getWorldPosition = jest.fn(() => {
98
+ return new Vector3();
99
+ });
100
+ return this;
101
+ }),
102
+ Box3: jest.fn(function () {
103
+ this.min = new Vector3(Infinity, Infinity, Infinity);
104
+ this.max = new Vector3(-Infinity, -Infinity, -Infinity);
105
+ this.getCenter = jest.fn(() => {
106
+ return new Vector3(0, 0, 0);
107
+ });
108
+ this.expandByObject = jest.fn();
109
+
110
+ return this;
111
+ }),
112
+ Mesh: jest.fn(function () {
113
+ this.geometry = {
114
+ computeBoundingBox: jest.fn(),
115
+ boundingBox: new Box3(),
116
+ };
117
+ this.material = {};
118
+ this.castShadow = true;
119
+ this.receiveShadow = true;
120
+ this.layers = {
121
+ mask: 0,
122
+ };
123
+ this.updateWorldMatrix = jest.fn();
124
+ this.traverse = jest.fn();
125
+ this.removeFromParent = jest.fn();
126
+ this.localToWorld = (vec3: Vector3) => {
127
+ return vec3;
128
+ };
129
+ return this;
130
+ }),
131
+ MeshStandardMaterial: jest.fn(function () {
132
+ this.color = new Color();
133
+ this.roughness = 1;
134
+ this.roughnessMap = undefined;
135
+ this.metalness = 0;
136
+ this.metalnessMap = undefined;
137
+ return this;
138
+ }),
139
+ Color: jest.fn(function () {
140
+ this.set = jest.fn();
141
+ return this;
142
+ }),
143
+ };
144
+ });
145
+
146
+ jest.mock('../../../exporters/usdz/USDZExporter', () => {
147
+ return {
148
+ DIVEUSDZExporter: jest.fn().mockImplementation(() => {
149
+ return {
150
+ parse: jest.fn(() => {
151
+ return Promise.resolve(new Uint8Array());
152
+ }),
153
+ };
154
+ }),
155
+ };
156
+ });
157
+
158
+ URL.createObjectURL = jest.fn(() => 'blob:http://localhost:8080/');
159
+
160
+ describe('DIVEARQuickLook', () => {
161
+ let mockScene: DIVEScene;
162
+ let mockOptions: DIVEAROptions;
163
+ let mockModels: Object3D[];
164
+
165
+ beforeEach(() => {
166
+ mockModels = [
167
+ new Object3D(),
168
+ new Object3D(),
169
+ ];
170
+ mockScene = {
171
+ Root: new Object3D(),
172
+ } as DIVEScene;
173
+ mockOptions = {
174
+ arPlacement: 'horizontal',
175
+ arScale: 'auto',
176
+ } as DIVEAROptions;
177
+ });
178
+
179
+ describe('Launch', () => {
180
+ it('should be a function', () => {
181
+ expect(DIVEARQuickLook.Launch).toBeInstanceOf(Function);
182
+ });
183
+
184
+ it('should return a promise', () => {
185
+ expect(
186
+ DIVEARQuickLook.Launch(mockScene, mockOptions),
187
+ ).toBeInstanceOf(Promise);
188
+ });
189
+
190
+ it('should not throw when called without options', () => {
191
+ const usdzParseSpy = jest.spyOn(
192
+ DIVEARQuickLook['_usdzExporter'],
193
+ 'parse',
194
+ );
195
+
196
+ expect(
197
+ async () => await DIVEARQuickLook.Launch(mockScene),
198
+ ).not.toThrow();
199
+
200
+ expect(usdzParseSpy).toHaveBeenCalled();
201
+ });
202
+
203
+ it('should not throw when called with empty scene', () => {
204
+ const usdzParseSpy = jest.spyOn(
205
+ DIVEARQuickLook['_usdzExporter'],
206
+ 'parse',
207
+ );
208
+
209
+ expect(
210
+ async () =>
211
+ await DIVEARQuickLook.Launch(mockScene, mockOptions),
212
+ ).not.toThrow();
213
+
214
+ expect(usdzParseSpy).toHaveBeenCalled();
215
+ });
216
+
217
+ it('should not throw when called with filled scene', () => {
218
+ const usdzParseSpy = jest.spyOn(
219
+ DIVEARQuickLook['_usdzExporter'],
220
+ 'parse',
221
+ );
222
+
223
+ mockScene.Root.children = mockModels;
224
+
225
+ expect(
226
+ async () =>
227
+ await DIVEARQuickLook.Launch(mockScene, mockOptions),
228
+ ).not.toThrow();
229
+
230
+ expect(usdzParseSpy).toHaveBeenCalled();
231
+ });
232
+
233
+ it('should pass options to exporter', async () => {
234
+ const usdzParseSpy = jest.spyOn(
235
+ DIVEARQuickLook['_usdzExporter'],
236
+ 'parse',
237
+ );
238
+
239
+ mockOptions.arPlacement = 'vertical';
240
+ mockOptions.arScale = 'fixed';
241
+
242
+ await DIVEARQuickLook.Launch(mockScene, mockOptions);
243
+
244
+ expect(usdzParseSpy).toHaveBeenCalledWith(expect.any(Object3D), {
245
+ quickLookCompatible: true,
246
+ ar: {
247
+ anchoring: { type: 'plane' },
248
+ planeAnchoring: {
249
+ alignment: 'vertical',
250
+ },
251
+ },
252
+ });
253
+ });
254
+
255
+ it('should reject when USDZExporter fails', async () => {
256
+ const usdzParseSpy = jest.spyOn(
257
+ DIVEARQuickLook['_usdzExporter'],
258
+ 'parse',
259
+ );
260
+
261
+ usdzParseSpy.mockReturnValueOnce(Promise.reject());
262
+
263
+ await expect(
264
+ DIVEARQuickLook.Launch(mockScene, mockOptions),
265
+ ).rejects.toBeUndefined();
266
+ });
267
+ });
268
+ });
@@ -0,0 +1,74 @@
1
+ import { type DIVEScene } from '../../scene/Scene';
2
+ import { type DIVEAROptions } from '../AR';
3
+
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);
8
+
9
+ // launch SceneViewer
10
+ this.launchSceneViewer(url, options);
11
+ }
12
+
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);
23
+ const params = new URLSearchParams(modelUrl.search);
24
+
25
+ locationUrl.hash = noArViewerSigil;
26
+
27
+ // modelUrl can contain title/link/sound etc.
28
+ params.set('mode', 'ar_only');
29
+
30
+ if (options?.arScale === 'fixed') {
31
+ params.set('resizable', 'false');
32
+ }
33
+
34
+ if (options?.arPlacement === 'vertical') {
35
+ params.set('enable_vertical_placement', 'true');
36
+ }
37
+
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();
56
+ }
57
+
58
+ private static findSceneViewerSrc(scene: DIVEScene): string {
59
+ let uri: string | null = null;
60
+
61
+ scene.traverse((object) => {
62
+ if (uri) return;
63
+ if (object.userData.uri) {
64
+ uri = object.userData.uri;
65
+ }
66
+ });
67
+
68
+ if (!uri) {
69
+ throw new Error('No model found in scene');
70
+ }
71
+
72
+ return uri;
73
+ }
74
+ }
@@ -0,0 +1,245 @@
1
+ import { Box3, Color, Euler, Mesh, Object3D, Vector3 } from 'three';
2
+ import { DIVEScene } from '../../../scene/Scene';
3
+ import { DIVEAROptions } from '../../AR';
4
+ import { DIVESceneViewer } from '../SceneViewer';
5
+
6
+ jest.mock('../../../scene/Scene', () => {
7
+ return {
8
+ DIVEScene: jest.fn(function () {
9
+ this.add = jest.fn();
10
+ this.children = [];
11
+ this.Root = {
12
+ children: [],
13
+ };
14
+ this.traverse = jest.fn((callback) => {
15
+ this.Root.children.forEach((child: Object3D) => {
16
+ callback(child);
17
+ });
18
+ });
19
+ return this;
20
+ }),
21
+ };
22
+ });
23
+
24
+ jest.mock('three', () => {
25
+ return {
26
+ Vector3: jest.fn(function (
27
+ x: number = 0,
28
+ y: number = 0,
29
+ z: number = 0,
30
+ ) {
31
+ this.x = x;
32
+ this.y = y;
33
+ this.z = z;
34
+ this.copy = (vec3: Vector3) => {
35
+ this.x = vec3.x;
36
+ this.y = vec3.y;
37
+ this.z = vec3.z;
38
+ return this;
39
+ };
40
+ this.set = (x: number, y: number, z: number) => {
41
+ this.x = x;
42
+ this.y = y;
43
+ this.z = z;
44
+ return this;
45
+ };
46
+ this.multiply = (vec3: Vector3) => {
47
+ this.x *= vec3.x;
48
+ this.y *= vec3.y;
49
+ this.z *= vec3.z;
50
+ return this;
51
+ };
52
+ this.clone = () => {
53
+ return new Vector3(this.x, this.y, this.z);
54
+ };
55
+ this.setY = (y: number) => {
56
+ this.y = y;
57
+ return this;
58
+ };
59
+ this.add = (vec3: Vector3) => {
60
+ this.x += vec3.x;
61
+ this.y += vec3.y;
62
+ this.z += vec3.z;
63
+ return this;
64
+ };
65
+ this.sub = (vec3: Vector3) => {
66
+ this.x -= vec3.x;
67
+ this.y -= vec3.y;
68
+ this.z -= vec3.z;
69
+ return this;
70
+ };
71
+ return this;
72
+ }),
73
+ Euler: jest.fn(function () {
74
+ this.set = jest.fn();
75
+ return this;
76
+ }),
77
+ Object3D: jest.fn(function () {
78
+ this.clear = jest.fn();
79
+ this.color = {};
80
+ this.intensity = 0;
81
+ this.layers = {
82
+ mask: 0,
83
+ };
84
+ this.shadow = {
85
+ radius: 0,
86
+ mapSize: { width: 0, height: 0 },
87
+ bias: 0,
88
+ camera: {
89
+ near: 0,
90
+ far: 0,
91
+ fov: 0,
92
+ },
93
+ };
94
+ this.add = jest.fn();
95
+ this.sub = jest.fn();
96
+ this.children = [
97
+ {
98
+ visible: true,
99
+ material: {
100
+ color: {},
101
+ },
102
+ },
103
+ ];
104
+ this.userData = {};
105
+ this.position = new Vector3();
106
+ this.rotation = new Euler();
107
+ this.scale = new Vector3(1, 1, 1);
108
+ this.localToWorld = (vec3: Vector3) => {
109
+ return vec3;
110
+ };
111
+ this.mesh = new Mesh();
112
+ this.traverse = jest.fn((callback) => {
113
+ callback(this.children[0]);
114
+ });
115
+ this.getWorldPosition = jest.fn(() => {
116
+ return new Vector3();
117
+ });
118
+ return this;
119
+ }),
120
+ Box3: jest.fn(function () {
121
+ this.min = new Vector3(Infinity, Infinity, Infinity);
122
+ this.max = new Vector3(-Infinity, -Infinity, -Infinity);
123
+ this.getCenter = jest.fn(() => {
124
+ return new Vector3(0, 0, 0);
125
+ });
126
+ this.expandByObject = jest.fn();
127
+
128
+ return this;
129
+ }),
130
+ Mesh: jest.fn(function () {
131
+ this.geometry = {
132
+ computeBoundingBox: jest.fn(),
133
+ boundingBox: new Box3(),
134
+ };
135
+ this.material = {};
136
+ this.castShadow = true;
137
+ this.receiveShadow = true;
138
+ this.layers = {
139
+ mask: 0,
140
+ };
141
+ this.updateWorldMatrix = jest.fn();
142
+ this.traverse = jest.fn();
143
+ this.removeFromParent = jest.fn();
144
+ this.localToWorld = (vec3: Vector3) => {
145
+ return vec3;
146
+ };
147
+ return this;
148
+ }),
149
+ MeshStandardMaterial: jest.fn(function () {
150
+ this.color = new Color();
151
+ this.roughness = 1;
152
+ this.roughnessMap = undefined;
153
+ this.metalness = 0;
154
+ this.metalnessMap = undefined;
155
+ return this;
156
+ }),
157
+ Color: jest.fn(function () {
158
+ this.set = jest.fn();
159
+ return this;
160
+ }),
161
+ };
162
+ });
163
+
164
+ jest.mock('../../../exporters/usdz/USDZExporter', () => {
165
+ return {
166
+ DIVEUSDZExporter: jest.fn().mockImplementation(() => {
167
+ return {
168
+ parse: jest.fn(() => {
169
+ return Promise.resolve(new Uint8Array());
170
+ }),
171
+ };
172
+ }),
173
+ };
174
+ });
175
+
176
+ URL.createObjectURL = jest.fn(() => 'blob:http://localhost:8080/');
177
+
178
+ describe('DIVESceneViewer', () => {
179
+ let mockScene: DIVEScene;
180
+ let mockOptions: DIVEAROptions;
181
+ let mockModels: Object3D[];
182
+
183
+ beforeEach(() => {
184
+ mockModels = [
185
+ new Object3D(),
186
+ new Object3D(),
187
+ new Object3D(),
188
+ ];
189
+ mockModels[1].userData = {
190
+ uri: 'https://example.com',
191
+ };
192
+ mockScene = new DIVEScene();
193
+ mockOptions = {
194
+ arPlacement: 'horizontal',
195
+ arScale: 'auto',
196
+ } as DIVEAROptions;
197
+ });
198
+
199
+ describe('Launch', () => {
200
+ it('should be a function', () => {
201
+ expect(DIVESceneViewer.Launch).toBeInstanceOf(Function);
202
+ });
203
+
204
+ it('should not throw without options', () => {
205
+ mockScene.Root.children = mockModels;
206
+
207
+ expect(() => {
208
+ DIVESceneViewer.Launch(mockScene);
209
+ }).not.toThrow();
210
+ });
211
+
212
+ it('should not throw with options', () => {
213
+ mockScene.Root.children = mockModels;
214
+
215
+ expect(() => {
216
+ DIVESceneViewer.Launch(mockScene, mockOptions);
217
+ }).not.toThrow();
218
+ });
219
+
220
+ it('should not throw with alternated options', () => {
221
+ mockScene.Root.children = mockModels;
222
+
223
+ mockOptions = {
224
+ arPlacement: 'vertical',
225
+ arScale: 'fixed',
226
+ } as DIVEAROptions;
227
+
228
+ expect(() => {
229
+ DIVESceneViewer.Launch(mockScene, mockOptions);
230
+ }).not.toThrow();
231
+ });
232
+
233
+ it('should throw if no url is found', () => {
234
+ mockScene.Root.children = [
235
+ new Object3D(),
236
+ new Object3D(),
237
+ new Object3D(),
238
+ ];
239
+
240
+ expect(() => {
241
+ DIVESceneViewer.Launch(mockScene, mockOptions);
242
+ }).toThrow();
243
+ });
244
+ });
245
+ });
@@ -133,15 +133,7 @@ export class DIVEWebXRController extends Object3D {
133
133
  this._frameBuffer = frame;
134
134
 
135
135
  if (!this._placed) {
136
- this._xrCamera.updateMatrixWorld();
137
- this._scene.XRRoot.XRHandNode.position.copy(
138
- this._handNodeInitialPosition
139
- .clone()
140
- .applyMatrix4(this._xrCamera.matrixWorld),
141
- );
142
- this._scene.XRRoot.XRHandNode.quaternion.setFromRotationMatrix(
143
- this._xrCamera.matrixWorld,
144
- );
136
+ this.updateHandNode();
145
137
 
146
138
  if (this._origin) {
147
139
  this._origin.Update(frame);
@@ -149,6 +141,18 @@ export class DIVEWebXRController extends Object3D {
149
141
  }
150
142
  }
151
143
 
144
+ private updateHandNode(): void {
145
+ this._xrCamera.updateMatrixWorld();
146
+ this._scene.XRRoot.XRHandNode.position.copy(
147
+ this._handNodeInitialPosition
148
+ .clone()
149
+ .applyMatrix4(this._xrCamera.matrixWorld),
150
+ );
151
+ this._scene.XRRoot.XRHandNode.quaternion.setFromRotationMatrix(
152
+ this._xrCamera.matrixWorld,
153
+ );
154
+ }
155
+
152
156
  // placement
153
157
  private async initOrigin(): Promise<void> {
154
158
  // initialize origin
@@ -162,6 +166,8 @@ export class DIVEWebXRController extends Object3D {
162
166
 
163
167
  private placeObjects(matrix: Matrix4): void {
164
168
  this._scene.XRRoot.XRModelRoot.matrix.copy(matrix);
169
+
170
+ // we are copying children to a new array to keep the original array intact
165
171
  [...this._scene.XRRoot.XRHandNode.children].forEach((child) => {
166
172
  this._scene.XRRoot.XRModelRoot.add(child);
167
173
  });
@@ -290,7 +296,7 @@ export class DIVEWebXRController extends Object3D {
290
296
  // initialize crosshair
291
297
  this._scene.add(this._crosshair);
292
298
 
293
- // hang current scene children to hang node
299
+ // hang current scene children to hand node
294
300
  const children: Object3D[] = [];
295
301
  this._scene.Root.children.forEach((child) => {
296
302
  const clone = child.clone();
@@ -310,7 +316,7 @@ export class DIVEWebXRController extends Object3D {
310
316
  private restoreScene(): void {
311
317
  this._scene.remove(this._crosshair);
312
318
 
313
- // clear hang node and remove attached models
319
+ // clear hand node and remove attached models
314
320
  this._scene.XRRoot.XRHandNode.clear();
315
321
  this._scene.XRRoot.XRModelRoot.clear();
316
322
 
@@ -180,6 +180,7 @@ export class DIVEWebXROrigin {
180
180
 
181
181
  this.matrix.fromArray(pose.transform.matrix);
182
182
 
183
+ // we have to wait for a certain amount of frames to make sure the origin is set
183
184
  if (this._raycastHitCounter > 50) {
184
185
  this._originSetResolve();
185
186
  }
@@ -305,8 +305,9 @@ export class DIVECommunication {
305
305
  break;
306
306
  }
307
307
  case 'LAUNCH_AR': {
308
- this.ar.Launch();
309
- returnValue = true;
308
+ returnValue = this.ar.Launch(
309
+ payload as Actions['LAUNCH_AR']['PAYLOAD'],
310
+ );
310
311
  break;
311
312
  }
312
313
  default: {
@@ -461,7 +462,7 @@ export class DIVECommunication {
461
462
 
462
463
  this.registered.delete(payload.id);
463
464
 
464
- // detach from parent
465
+ // detach all children from parent if we delete a group
465
466
  Array.from(this.registered.values()).forEach((object) => {
466
467
  if (!object.parentId) return;
467
468
  if (object.parentId !== payload.id) return;