@needle-tools/engine 3.2.12-alpha → 3.2.14-alpha

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 (41) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/needle-engine.js +10130 -9995
  3. package/dist/needle-engine.min.js +279 -279
  4. package/dist/needle-engine.umd.cjs +273 -273
  5. package/lib/engine/codegen/register_types.js +4 -0
  6. package/lib/engine/codegen/register_types.js.map +1 -1
  7. package/lib/engine/engine_serialization_core.js +2 -0
  8. package/lib/engine/engine_serialization_core.js.map +1 -1
  9. package/lib/engine/engine_utils.d.ts +2 -2
  10. package/lib/engine/engine_utils.js +4 -4
  11. package/lib/engine/engine_utils.js.map +1 -1
  12. package/lib/engine-components/SceneSwitcher.d.ts +5 -0
  13. package/lib/engine-components/SceneSwitcher.js +32 -4
  14. package/lib/engine-components/SceneSwitcher.js.map +1 -1
  15. package/lib/engine-components/codegen/components.d.ts +2 -0
  16. package/lib/engine-components/codegen/components.js +2 -0
  17. package/lib/engine-components/codegen/components.js.map +1 -1
  18. package/lib/engine-components/export/usdz/USDZExporter.d.ts +9 -5
  19. package/lib/engine-components/export/usdz/USDZExporter.js +79 -28
  20. package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
  21. package/lib/engine-components/ui/EventSystem.js +11 -4
  22. package/lib/engine-components/ui/EventSystem.js.map +1 -1
  23. package/lib/engine-components/utils/OpenURL.d.ts +21 -0
  24. package/lib/engine-components/utils/OpenURL.js +125 -0
  25. package/lib/engine-components/utils/OpenURL.js.map +1 -0
  26. package/lib/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +2 -2
  28. package/plugins/vite/alias.js +1 -4
  29. package/plugins/vite/copyfiles.js +46 -40
  30. package/plugins/vite/index.js +2 -0
  31. package/plugins/vite/meta.js +5 -2
  32. package/plugins/vite/peer.js +28 -0
  33. package/plugins/vite/reload.js +17 -13
  34. package/src/engine/codegen/register_types.js +4 -0
  35. package/src/engine/engine_serialization_core.ts +1 -1
  36. package/src/engine/engine_utils.ts +7 -7
  37. package/src/engine-components/SceneSwitcher.ts +38 -5
  38. package/src/engine-components/codegen/components.ts +2 -0
  39. package/src/engine-components/export/usdz/USDZExporter.ts +75 -31
  40. package/src/engine-components/ui/EventSystem.ts +11 -5
  41. package/src/engine-components/utils/OpenURL.ts +119 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/engine",
3
- "version": "3.2.12-alpha",
3
+ "version": "3.2.14-alpha",
4
4
  "description": "Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in",
5
5
  "main": "dist/needle-engine.umd.cjs",
6
6
  "type": "module",
@@ -61,7 +61,7 @@
61
61
  "postprocessing": "^6.30.1",
62
62
  "simplex-noise": "^4.0.1",
63
63
  "stats.js": "^0.17.0",
64
- "three": "npm:@needle-tools/three@^0.146.6",
64
+ "three": "npm:@needle-tools/three@^0.146.8",
65
65
  "three-mesh-ui": "^6.4.5",
66
66
  "three.quarks": "^0.7.3",
67
67
  "uuid": "^9.0.0",
@@ -33,10 +33,7 @@ export const needleViteAlias = (command, config, userSettings) => {
33
33
  return {
34
34
  name: "needle-alias",
35
35
  config(config) {
36
- setTimeout(() => {
37
- console.log('[needle-alias] ProjectDirectory: ' + projectDir);
38
- }, 150);
39
-
36
+ console.log('[needle-alias] ProjectDirectory: ' + projectDir);
40
37
  if (!config.resolve) config.resolve = {};
41
38
  if (!config.resolve.alias) config.resolve.alias = {};
42
39
  const aliasDict = config.resolve.alias;
@@ -11,57 +11,63 @@ export const needleCopyFiles = (command, config, userSettings) => {
11
11
  return;
12
12
  }
13
13
 
14
- const copyIncludesFromEngine = config?.copyIncludesFromEngine ?? true;
15
-
16
14
  return {
17
15
  name: 'needle-copy-files',
18
- apply: 'build',
19
- async closeBundle() {
20
- const baseDir = process.cwd();
21
- const pluginName = "needle-copy-files";
16
+ buildStart() {
17
+ return run(false, config);
18
+ },
19
+ closeBundle() {
20
+ return run(true, config);
21
+ },
22
+ }
23
+ }
24
+
25
+ async function run(isBuild, config) {
26
+ const copyIncludesFromEngine = config?.copyIncludesFromEngine ?? true;
22
27
 
23
- let assetsDirName = "assets";
24
- let outdirName = "dist";
28
+ const baseDir = process.cwd();
29
+ const pluginName = "needle-copy-files";
25
30
 
26
- const needleConfig = tryLoadProjectConfig();
27
- if(needleConfig){
28
- assetsDirName = needleConfig.assetsDirectory;
29
- }
31
+ let assetsDirName = "assets";
32
+ let outdirName = "dist";
30
33
 
31
- const outDir = resolve(baseDir, outdirName);
32
- if (!existsSync(outDir)) {
33
- mkdirSync(outDir);
34
- }
34
+ const needleConfig = tryLoadProjectConfig();
35
+ if (needleConfig) {
36
+ assetsDirName = needleConfig.assetsDirectory;
37
+ }
35
38
 
36
- if (copyIncludesFromEngine !== false) {
37
- // copy include from engine
38
- const engineIncludeDir = resolve(baseDir, 'node_modules', '@needle-tools', 'engine', 'src', 'include');
39
- if (existsSync(engineIncludeDir)) {
40
- console.log(`[${pluginName}] - Copy engine include to ${baseDir}/include`)
41
- const targetDir = resolve(baseDir, 'include');
42
- copyRecursiveSync(engineIncludeDir, targetDir);
43
- }
44
- }
39
+ if (copyIncludesFromEngine !== false) {
40
+ // copy include from engine
41
+ const engineIncludeDir = resolve(baseDir, 'node_modules', '@needle-tools', 'engine', 'src', 'include');
42
+ if (existsSync(engineIncludeDir)) {
43
+ console.log(`[${pluginName}] - Copy engine include to ${baseDir}/include`)
44
+ const projectIncludeDir = resolve(baseDir, 'include');
45
+ copyRecursiveSync(engineIncludeDir, projectIncludeDir);
46
+ }
47
+ }
45
48
 
46
- // copy assets dir
47
- const assetsDir = resolve(baseDir, assetsDirName);
48
- if (existsSync(assetsDir)) {
49
- console.log(`[${pluginName}] - Copy assets to ${outdirName}/${builtAssetsDirectory()}`)
50
- const targetDir = resolve(outDir, 'assets');
51
- copyRecursiveSync(assetsDir, targetDir);
52
- }
53
- // copy include dir
54
- const includeDir = resolve(baseDir, 'include');
55
- if (existsSync(includeDir)) {
56
- console.log(`[${pluginName}] - Copy include to ${outdirName}/include`)
57
- const targetDir = resolve(outDir, 'include');
58
- copyRecursiveSync(includeDir, targetDir);
59
- }
49
+ if (isBuild) {
50
+ const outDir = resolve(baseDir, outdirName);
51
+ if (!existsSync(outDir)) {
52
+ mkdirSync(outDir);
53
+ }
54
+ // copy assets dir
55
+ const assetsDir = resolve(baseDir, assetsDirName);
56
+ if (existsSync(assetsDir)) {
57
+ console.log(`[${pluginName}] - Copy assets to ${outdirName}/${builtAssetsDirectory()}`)
58
+ const targetDir = resolve(outDir, 'assets');
59
+ copyRecursiveSync(assetsDir, targetDir);
60
+ }
61
+ // copy include dir
62
+ const includeDir = resolve(baseDir, 'include');
63
+ if (existsSync(includeDir)) {
64
+ console.log(`[${pluginName}] - Copy include to ${outdirName}/include`)
65
+ const targetDir = resolve(outDir, 'include');
66
+ copyRecursiveSync(includeDir, targetDir);
60
67
  }
61
68
  }
62
69
  }
63
70
 
64
-
65
71
  function copyRecursiveSync(src, dest) {
66
72
  var exists = existsSync(src);
67
73
  var stats = exists && statSync(src);
@@ -8,6 +8,7 @@ import { needleCopyFiles } from "./copyfiles.js";
8
8
  import { needleViteAlias } from "./alias.js";
9
9
  import { needleTransformCodegen } from "./transform-codegen.js";
10
10
  import { needleLicense } from "./license.js";
11
+ import { needlePeerjs } from "./peer.js";
11
12
 
12
13
  export * from "./gzip.js";
13
14
  export * from "./config.js";
@@ -30,6 +31,7 @@ export const needlePlugins = async (command, config, userSettings) => {
30
31
  needleCopyFiles(command, config, userSettings),
31
32
  needleTransformCodegen(command, config, userSettings),
32
33
  needleDrop(command, config, userSettings),
34
+ needlePeerjs(command, config, userSettings)
33
35
  ];
34
36
  array.push(await editorConnection(command, config, userSettings, array));
35
37
  return array;
@@ -91,6 +91,9 @@ export const needleMeta = (command, config, userSettings) => {
91
91
  }
92
92
  }
93
93
 
94
+ // if(!tags.filter(t => t.attrs?.name === "generator"))
95
+ tags.push({ tag: 'meta', attrs: { name: 'generator', content: 'Needle' } });
96
+
94
97
  return { html, tags }
95
98
  },
96
99
  }
@@ -124,8 +127,8 @@ function removeMetaTag(html, name) {
124
127
  function insertNeedleCredits(html) {
125
128
  const needleCredits = `<!-- 🌵 Made with Needle — https://needle.tools -->`;
126
129
  html = html.replace(
127
- /<head>/,
128
- needleCredits + "\n<head>",
130
+ /<head/,
131
+ needleCredits + "\n<head",
129
132
  );
130
133
  return html;
131
134
  }
@@ -0,0 +1,28 @@
1
+
2
+ const peerjsString = `/* needle: injected fix for peerjs */
3
+ window.global = window;
4
+ var parcelRequire;`
5
+
6
+ export const needlePeerjs = (command, config, userSettings) => {
7
+
8
+ if (userSettings.noPeer === true) return;
9
+
10
+ return {
11
+ name: 'needle-peerjs',
12
+ transformIndexHtml: {
13
+ enforce: 'pre',
14
+ transform(html, _ctx) {
15
+ return {
16
+ html,
17
+ tags: [
18
+ {
19
+ tag: 'script',
20
+ children: peerjsString,
21
+ injectTo: 'body-prepend',
22
+ },
23
+ ]
24
+ }
25
+ }
26
+ }
27
+ }
28
+ }
@@ -61,22 +61,26 @@ export const needleReload = (command, config, userSettings) => {
61
61
  transformIndexHtml: {
62
62
  enforce: 'pre',
63
63
  transform(html, _) {
64
- if (config?.allowHotReload === false) return [html];
65
- if (userSettings?.allowHotReload === false) return [html];
64
+ if (config?.allowHotReload === false) return html;
65
+ if (userSettings?.allowHotReload === false) return html;
66
66
  const file = path.join(__dirname, 'reload-client.js');
67
- return [
68
- {
69
- tag: 'script',
70
- attrs: {
71
- type: 'module',
67
+ return {
68
+ html,
69
+ tags: [
70
+ {
71
+ tag: 'script',
72
+ attrs: {
73
+ type: 'module',
74
+ },
75
+ children: readFileSync(file, 'utf8'),
76
+ injectTo: 'body',
72
77
  },
73
- children: readFileSync(file, 'utf8'),
74
- injectTo: 'body',
75
- },
76
- ];
78
+ ]
79
+ }
80
+
77
81
  },
78
- },
79
- };
82
+ }
83
+ }
80
84
  }
81
85
 
82
86
 
@@ -95,6 +95,7 @@ import { Networking } from "../../engine-components/Networking";
95
95
  import { NoiseModule } from "../../engine-components/ParticleSystemModules";
96
96
  import { ObjectRaycaster } from "../../engine-components/ui/Raycaster";
97
97
  import { OffsetConstraint } from "../../engine-components/OffsetConstraint";
98
+ import { OpenURL } from "../../engine-components/utils/OpenURL";
98
99
  import { OrbitControls } from "../../engine-components/OrbitControls";
99
100
  import { ParticleBurst } from "../../engine-components/ParticleSystemModules";
100
101
  import { ParticleSubEmitter } from "../../engine-components/ParticleSystemSubEmitter";
@@ -108,6 +109,7 @@ import { PlayerSync } from "../../engine-components-experimental/networking/Play
108
109
  import { PointerEventData } from "../../engine-components/ui/PointerEvents";
109
110
  import { PostProcessingHandler } from "../../engine-components/postprocessing/PostProcessingHandler";
110
111
  import { PresentationMode } from "../../engine-components-experimental/Presentation";
112
+ import { QuickLookOverlay } from "../../engine-components/export/usdz/USDZExporter";
111
113
  import { RawImage } from "../../engine-components/ui/Image";
112
114
  import { Raycaster } from "../../engine-components/ui/Raycaster";
113
115
  import { Rect } from "../../engine-components/ui/RectTransform";
@@ -283,6 +285,7 @@ TypeStore.add("Networking", Networking);
283
285
  TypeStore.add("NoiseModule", NoiseModule);
284
286
  TypeStore.add("ObjectRaycaster", ObjectRaycaster);
285
287
  TypeStore.add("OffsetConstraint", OffsetConstraint);
288
+ TypeStore.add("OpenURL", OpenURL);
286
289
  TypeStore.add("OrbitControls", OrbitControls);
287
290
  TypeStore.add("ParticleBurst", ParticleBurst);
288
291
  TypeStore.add("ParticleSubEmitter", ParticleSubEmitter);
@@ -296,6 +299,7 @@ TypeStore.add("PlayerSync", PlayerSync);
296
299
  TypeStore.add("PointerEventData", PointerEventData);
297
300
  TypeStore.add("PostProcessingHandler", PostProcessingHandler);
298
301
  TypeStore.add("PresentationMode", PresentationMode);
302
+ TypeStore.add("QuickLookOverlay", QuickLookOverlay);
299
303
  TypeStore.add("RawImage", RawImage);
300
304
  TypeStore.add("Raycaster", Raycaster);
301
305
  TypeStore.add("Rect", Rect);
@@ -279,7 +279,7 @@ export function deserializeObject(obj: ISerializable, serializedData: object, co
279
279
  for (const key in typeInfo) {
280
280
  const serializedEntryInfo = typeInfo[key];
281
281
  const data = serializedData[key];
282
-
282
+ if(debug) console.log(key, data, obj, serializedEntryInfo)
283
283
 
284
284
  if (obj[key] !== undefined && data === undefined) {
285
285
  // if a field is marked as serialized and has some default value
@@ -97,12 +97,12 @@ export function setOrAddParamsToUrl(url: URLSearchParams, paramName: string, par
97
97
  url.append(paramName, paramValue.toString());
98
98
  }
99
99
 
100
- export function pushState(title: string, urlParams: URLSearchParams) {
101
- window.history.pushState(null, title, "?" + urlParams.toString());
100
+ export function pushState(title: string, urlParams: URLSearchParams, state?: any) {
101
+ window.history.pushState(state, title, "?" + urlParams.toString());
102
102
  }
103
103
 
104
- export function setState(title: string, urlParams: URLSearchParams) {
105
- window.history.replaceState(null, title, "?" + urlParams.toString());
104
+ export function setState(title: string, urlParams: URLSearchParams, state?: any) {
105
+ window.history.replaceState(state, title, "?" + urlParams.toString());
106
106
  }
107
107
 
108
108
  // for room id
@@ -212,7 +212,7 @@ const debugGetPath = getParam("debugresolveurl");
212
212
  export const relativePathPrefix = "rel:";
213
213
 
214
214
  /** @deprecated use resolveUrl instead */
215
- export function getPath(source:SourceIdentifier|undefined, uri:string) : string {
215
+ export function getPath(source: SourceIdentifier | undefined, uri: string): string {
216
216
  return resolveUrl(source, uri);
217
217
  }
218
218
  /**
@@ -226,7 +226,7 @@ export function resolveUrl(source: SourceIdentifier | undefined, uri: string): s
226
226
  if (debugGetPath) console.warn("getPath: uri is undefined, returning uri", uri);
227
227
  return uri;
228
228
  }
229
- if(uri.startsWith("./")) {
229
+ if (uri.startsWith("./")) {
230
230
  return uri;
231
231
  }
232
232
  if (uri.startsWith("http")) {
@@ -237,7 +237,7 @@ export function resolveUrl(source: SourceIdentifier | undefined, uri: string): s
237
237
  if (debugGetPath) console.warn("getPath: source is undefined, returning uri", uri);
238
238
  return uri;
239
239
  }
240
- if(uri.startsWith(relativePathPrefix)){
240
+ if (uri.startsWith(relativePathPrefix)) {
241
241
  uri = uri.substring(4);
242
242
  }
243
243
  const pathIndex = source.lastIndexOf("/");
@@ -14,7 +14,7 @@ ContextRegistry.registerCallback(ContextEvent.ContextRegistered, async _ => {
14
14
  // We need to defer import to not get issues with circular dependencies
15
15
  import("../engine/engine_element").then(res => {
16
16
  const webcomponent = res.NeedleEngineHTMLElement;
17
- if(debug) console.log("SceneSwitcher: registering scene attribute", webcomponent.observedAttributes);
17
+ if (debug) console.log("SceneSwitcher: registering scene attribute", webcomponent.observedAttributes);
18
18
  if (!webcomponent.observedAttributes.includes(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME))
19
19
  webcomponent.observedAttributes.push(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME);
20
20
  });
@@ -22,6 +22,12 @@ ContextRegistry.registerCallback(ContextEvent.ContextRegistered, async _ => {
22
22
 
23
23
  const couldNotLoadScenePromise = Promise.resolve(false);
24
24
 
25
+ export type LoadSceneEvent = {
26
+ switcher: SceneSwitcher;
27
+ scene: AssetReference;
28
+ index: number;
29
+ }
30
+
25
31
  export class SceneSwitcher extends Behaviour {
26
32
 
27
33
  @serializable(AssetReference)
@@ -108,7 +114,17 @@ export class SceneSwitcher extends Behaviour {
108
114
  let wasUsingHistory = this.useHistory;
109
115
  try {
110
116
  this.useHistory = false;
111
- await this.tryLoadFromQueryParam();
117
+ let didResolve = false;
118
+ if (this.queryParameterName)
119
+ didResolve = await this.tryLoadFromQueryParam();
120
+ if (!didResolve) {
121
+ const state = _state.state;
122
+ if (state !== null && state.startsWith(this.guid)) {
123
+ const value = state.substr(this.guid.length + 2);
124
+ console.log(value);
125
+ await this.trySelectSceneFromValue(value);
126
+ }
127
+ }
112
128
  }
113
129
  finally {
114
130
  this.useHistory = wasUsingHistory;
@@ -192,7 +208,15 @@ export class SceneSwitcher extends Behaviour {
192
208
  const index = this._currentIndex = this.scenes?.indexOf(scene) ?? -1;
193
209
  this._currentScene = scene;
194
210
  try {
211
+ const loadStartEvt = new CustomEvent<LoadSceneEvent>("loadscene-start", { detail: { scene: scene, switcher: this, index: index } })
212
+ this.dispatchEvent(loadStartEvt);
195
213
  await scene.loadAssetAsync();
214
+ const finishedEvt = new CustomEvent<LoadSceneEvent>("loadscene-finished", { detail: { scene: scene, switcher: this, index: index } });
215
+ this.dispatchEvent(finishedEvt);
216
+ if (finishedEvt.defaultPrevented) {
217
+ if (debug) console.warn("Adding loaded scene prevented:", scene, finishedEvt);
218
+ return false;
219
+ }
196
220
  if (!scene.asset) {
197
221
  if (debug) console.warn("Failed loading scene:", scene);
198
222
  return false;
@@ -201,9 +225,18 @@ export class SceneSwitcher extends Behaviour {
201
225
  GameObject.add(scene.asset, this.gameObject);
202
226
  if (this.useSceneLighting)
203
227
  this.context.sceneLighting.enable(scene)
204
- // save the loaded scene as an url parameter
205
- if (this.queryParameterName?.length)
206
- setParamWithoutReload(this.queryParameterName, index.toString(), this.useHistory);
228
+ if (this.useHistory) {
229
+ // save the loaded scene as an url parameter
230
+ if (this.queryParameterName?.length)
231
+ setParamWithoutReload(this.queryParameterName, index.toString(), this.useHistory);
232
+ // or set the history state without updating the url parameter
233
+ else {
234
+ const lastState = history.state;
235
+ const stateName = this.guid + "::" + index;
236
+ if (lastState !== stateName)
237
+ history.pushState(stateName, "unused", location.href);
238
+ }
239
+ }
207
240
  return true;
208
241
  }
209
242
  }
@@ -93,6 +93,7 @@ export { Networking } from "../Networking";
93
93
  export { NoiseModule } from "../ParticleSystemModules";
94
94
  export { ObjectRaycaster } from "../ui/Raycaster";
95
95
  export { OffsetConstraint } from "../OffsetConstraint";
96
+ export { OpenURL } from "../utils/OpenURL";
96
97
  export { OrbitControls } from "../OrbitControls";
97
98
  export { ParticleBurst } from "../ParticleSystemModules";
98
99
  export { ParticleSubEmitter } from "../ParticleSystemSubEmitter";
@@ -103,6 +104,7 @@ export { PlayableDirector } from "../timeline/PlayableDirector";
103
104
  export { PlayerColor } from "../PlayerColor";
104
105
  export { PointerEventData } from "../ui/PointerEvents";
105
106
  export { PostProcessingHandler } from "../postprocessing/PostProcessingHandler";
107
+ export { QuickLookOverlay } from "../export/usdz/USDZExporter";
106
108
  export { RawImage } from "../ui/Image";
107
109
  export { Raycaster } from "../ui/Raycaster";
108
110
  export { Rect } from "../ui/RectTransform";
@@ -1,6 +1,5 @@
1
1
  import { delay, getParam, isiOS, isMobileDevice, isSafari } from "../../../engine/engine_utils";
2
- import { Object3D, Color } from "three";
3
- import * as THREE from "three";
2
+ import { Object3D, Color, Mesh, Matrix4 } from "three";
4
3
  import { USDZExporter as ThreeUSDZExporter } from "three/examples/jsm/exporters/USDZExporter";
5
4
  import { AnimationExtension } from "./extensions/Animation"
6
5
  import { ensureQuicklookLinkIsCreated } from "./utils/quicklook";
@@ -10,16 +9,23 @@ import { IUSDZExporterExtension } from "./Extension";
10
9
  import { Behaviour, GameObject } from "../../Component";
11
10
  import { WebXR } from "../../webxr/WebXR"
12
11
  import { serializable } from "../../../engine/engine_serialization";
13
- import { showBalloonWarning } from "../../../engine/debug/debug";
12
+ import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../../../engine/debug/debug";
14
13
  import { Context } from "../../../engine/engine_setup";
15
14
  import { WebARSessionRoot } from "../../webxr/WebARSessionRoot";
16
15
 
17
16
  const debug = getParam("debugusdz");
18
17
 
19
- export type QuickLookOverlay = {
18
+ export class QuickLookOverlay {
19
+ @serializable()
20
20
  callToAction?: string;
21
+ @serializable()
21
22
  checkoutTitle?: string;
23
+ @serializable()
22
24
  checkoutSubtitle?: string;
25
+
26
+ /** if assigned the call to action button in quicklook will open the URL. Otherwise it will just close quicklook. */
27
+ @serializable()
28
+ callToActionURL?: string;
23
29
  }
24
30
 
25
31
  export class USDZExporter extends Behaviour {
@@ -30,6 +36,9 @@ export class USDZExporter extends Behaviour {
30
36
  @serializable()
31
37
  autoExportAnimations: boolean = false;
32
38
 
39
+ @serializable(QuickLookOverlay)
40
+ overlay?: QuickLookOverlay;
41
+
33
42
  extensions: IUSDZExporterExtension[] = [];
34
43
 
35
44
  private link!: HTMLAnchorElement;
@@ -39,6 +48,8 @@ export class USDZExporter extends Behaviour {
39
48
 
40
49
  start() {
41
50
  if (debug) {
51
+ console.log(this);
52
+ console.log("Debug USDZ, press 't' to export")
42
53
  window.addEventListener("keydown", (evt) => {
43
54
  switch (evt.key) {
44
55
  case "t":
@@ -58,6 +69,12 @@ export class USDZExporter extends Behaviour {
58
69
  });
59
70
 
60
71
  if (!this.objectToExport) this.objectToExport = this.gameObject;
72
+
73
+
74
+ if (isDevEnvironment() && (!this.objectToExport?.children?.length && !(this.objectToExport as Mesh)?.isMesh)) {
75
+ showBalloonWarning("USDZ Exporter has nothing to export");
76
+ console.warn("USDZExporter has no objects to export assigned:", this)
77
+ }
61
78
  }
62
79
 
63
80
 
@@ -66,16 +83,25 @@ export class USDZExporter extends Behaviour {
66
83
  const ios = isiOS()
67
84
  const safari = isSafari();
68
85
  if (debug || (ios && safari)) {
69
- this.createQuicklookButton();
86
+ this.addQuicklookButton();
70
87
  this.webARSessionRoot = GameObject.findObjectOfType(WebARSessionRoot) ?? undefined;
71
88
  this.lastCallback = this.quicklookCallback.bind(this);
72
89
  this.link = ensureQuicklookLinkIsCreated(this.context);
73
90
  this.link.addEventListener('message', this.lastCallback);
74
91
  }
92
+ if (debug)
93
+ showBalloonMessage("USDZ Exporter enabled: " + this.name);
75
94
  }
76
95
 
77
96
  onDisable() {
78
97
  this.link?.removeEventListener('message', this.lastCallback);
98
+ const ios = isiOS()
99
+ const safari = isSafari();
100
+ if (debug || (ios && safari)) {
101
+ this.removeQuicklookButton();
102
+ }
103
+ if (debug)
104
+ showBalloonMessage("USDZ Exporter disabled: " + this.name);
79
105
  }
80
106
 
81
107
  async exportAsync() {
@@ -87,7 +113,7 @@ export class USDZExporter extends Behaviour {
87
113
  const scale = 1 / this.webARSessionRoot!.arScale;
88
114
  scene.matrix.makeScale(scale, scale, scale);
89
115
  if (this.webARSessionRoot.invertForward) {
90
- scene.matrix.multiply(new THREE.Matrix4().makeRotationY(Math.PI));
116
+ scene.matrix.multiply(new Matrix4().makeRotationY(Math.PI));
91
117
  }
92
118
  }
93
119
 
@@ -125,14 +151,15 @@ export class USDZExporter extends Behaviour {
125
151
 
126
152
  // see https://developer.apple.com/documentation/arkit/adding_an_apple_pay_button_or_a_custom_action_in_ar_quick_look
127
153
  const overlay = this.buildQuicklookOverlay();
154
+ console.log(overlay);
128
155
  const callToAction = overlay.callToAction ? encodeURIComponent(overlay.callToAction) : "";
129
156
  const checkoutTitle = overlay.checkoutTitle ? encodeURIComponent(overlay.checkoutTitle) : "";
130
157
  const checkoutSubtitle = overlay.checkoutSubtitle ? encodeURIComponent(overlay.checkoutSubtitle) : "";
131
- this.link.href = URL.createObjectURL(blob) + `#callToAction=${callToAction}&checkoutTitle=${checkoutTitle}&checkoutSubtitle=${checkoutSubtitle}&`;
158
+ this.link.href = URL.createObjectURL(blob) + `#callToAction=${callToAction}&checkoutTitle=${checkoutTitle}&checkoutSubtitle=${checkoutSubtitle}&callToActionURL=${overlay.callToActionURL}`;
132
159
 
133
160
 
134
161
  if (!this.lastCallback) {
135
- this.lastCallback = this.quicklookCallback.bind(this);
162
+ this.lastCallback = this.quicklookCallback.bind(this)
136
163
  this.link.addEventListener('message', this.lastCallback);
137
164
  }
138
165
 
@@ -146,24 +173,44 @@ export class USDZExporter extends Behaviour {
146
173
 
147
174
  private lastCallback?: any;
148
175
 
149
- private quicklookCallback(event) {
176
+ private quicklookCallback(event: Event) {
150
177
  if ((event as any)?.data == '_apple_ar_quicklook_button_tapped') {
151
178
  if (debug) showBalloonWarning("Quicklook closed via call to action button");
152
- this.dispatchEvent(new CustomEvent("quicklook-button-tapped", { detail: this }));
179
+ var evt = new CustomEvent("quicklook-button-tapped", { detail: this });
180
+ this.dispatchEvent(evt);
181
+ if (!evt.defaultPrevented) {
182
+ const url = new URLSearchParams(this.link.href);
183
+ if (url) {
184
+ const callToActionURL = url.get("callToActionURL");
185
+ if (debug)
186
+ showBalloonMessage("Quicklook url: " + callToActionURL);
187
+ if (callToActionURL) {
188
+ globalThis.open(callToActionURL, "_blank");
189
+ }
190
+ }
191
+ }
153
192
  }
154
193
  }
155
194
 
156
195
  private buildQuicklookOverlay(): QuickLookOverlay {
157
196
  const obj: QuickLookOverlay = {};
158
- obj.callToAction = "Close";
159
- obj.checkoutTitle = "🌵 Made with Needle";
160
- obj.checkoutSubtitle = "_";
197
+ if (this.overlay) Object.assign(obj, this.overlay);
198
+ if (!obj.callToAction?.length)
199
+ obj.callToAction = "Close";
200
+ if (!obj.checkoutTitle?.length)
201
+ obj.checkoutTitle = "🌵 Made with Needle";
202
+ if (!obj.checkoutSubtitle?.length)
203
+ obj.checkoutSubtitle = "_";
161
204
  // Use the quicklook-overlay event to customize the overlay
162
205
  this.dispatchEvent(new CustomEvent("quicklook-overlay", { detail: obj }));
163
206
  return obj;
164
207
  }
165
208
 
166
- private _arButton?: HTMLElement;
209
+
210
+
211
+
212
+ private _quicklookButton?: HTMLElement;
213
+
167
214
  private async createQuicklookButton() {
168
215
  if (!this.webxr) {
169
216
  await delay(1);
@@ -171,8 +218,8 @@ export class USDZExporter extends Behaviour {
171
218
  if (this.webxr) {
172
219
  if (this.webxr.VRButton) this.webxr.VRButton.parentElement?.removeChild(this.webxr.VRButton);
173
220
  // check if we have an AR button already and re-use that
174
- if (this.webxr.ARButton && this._arButton !== this.webxr.ARButton) {
175
- this._arButton = this.webxr.ARButton;
221
+ if (this.webxr.ARButton && this._quicklookButton !== this.webxr.ARButton) {
222
+ this._quicklookButton = this.webxr.ARButton;
176
223
  // Hack to remove the immersiveweb link
177
224
  const linkInButton = this.webxr.ARButton.parentElement?.querySelector("a");
178
225
  if (linkInButton) {
@@ -185,6 +232,7 @@ export class USDZExporter extends Behaviour {
185
232
  this.exportAsync();
186
233
  });
187
234
  this.webxr.ARButton.classList.add("quicklook-ar-button");
235
+ this._quicklookButtonContainer = this.webxr.ARButton.parentElement;
188
236
  this.dispatchEvent(new CustomEvent("created-button", { detail: this.webxr.ARButton }))
189
237
  }
190
238
  // create a button if WebXR didnt create one yet
@@ -205,6 +253,7 @@ export class USDZExporter extends Behaviour {
205
253
  button.classList.add('webxr-button');
206
254
  button.classList.add("quicklook-ar-button");
207
255
  container.appendChild(button);
256
+ this._quicklookButtonContainer = container;
208
257
  this.dispatchEvent(new CustomEvent("created-button", { detail: button }))
209
258
  }
210
259
  }
@@ -214,20 +263,15 @@ export class USDZExporter extends Behaviour {
214
263
  }
215
264
  }
216
265
 
217
- private resetStyles(el: HTMLElement) {
218
- el.style.position = "";
219
- el.style.top = "";
220
- el.style.left = "";
221
- el.style.width = "";
222
- el.style.height = "";
223
- el.style.margin = "";
224
- el.style.padding = "";
225
- el.style.border = "";
226
- el.style.background = "";
227
- el.style.color = "";
228
- el.style.font = "";
229
- el.style.textAlign = "";
230
- el.style.opacity = "";
231
- el.style.zIndex = "";
266
+
267
+ private _quicklookButtonContainer: Element | null = null;
268
+ private async addQuicklookButton() {
269
+ await this.createQuicklookButton();
270
+ if (this._quicklookButton && this._quicklookButtonContainer) {
271
+ this._quicklookButtonContainer.appendChild(this._quicklookButton);
272
+ }
273
+ }
274
+ private removeQuicklookButton() {
275
+ this._quicklookButton?.remove();
232
276
  }
233
277
  }