@needle-tools/engine 3.2.12-alpha → 3.2.13-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 (34) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/needle-engine.js +9265 -9197
  3. package/dist/needle-engine.min.js +273 -273
  4. package/dist/needle-engine.umd.cjs +271 -271
  5. package/lib/engine/codegen/register_types.js +2 -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 +1 -0
  16. package/lib/engine-components/codegen/components.js +1 -0
  17. package/lib/engine-components/codegen/components.js.map +1 -1
  18. package/lib/engine-components/export/usdz/USDZExporter.d.ts +9 -4
  19. package/lib/engine-components/export/usdz/USDZExporter.js +77 -25
  20. package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
  21. package/lib/tsconfig.tsbuildinfo +1 -1
  22. package/package.json +1 -1
  23. package/plugins/vite/alias.js +1 -4
  24. package/plugins/vite/copyfiles.js +46 -40
  25. package/plugins/vite/index.js +2 -0
  26. package/plugins/vite/meta.js +5 -2
  27. package/plugins/vite/peer.js +28 -0
  28. package/plugins/vite/reload.js +17 -13
  29. package/src/engine/codegen/register_types.js +2 -0
  30. package/src/engine/engine_serialization_core.ts +1 -1
  31. package/src/engine/engine_utils.ts +7 -7
  32. package/src/engine-components/SceneSwitcher.ts +38 -5
  33. package/src/engine-components/codegen/components.ts +1 -0
  34. package/src/engine-components/export/usdz/USDZExporter.ts +73 -28
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.13-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",
@@ -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
 
@@ -108,6 +108,7 @@ import { PlayerSync } from "../../engine-components-experimental/networking/Play
108
108
  import { PointerEventData } from "../../engine-components/ui/PointerEvents";
109
109
  import { PostProcessingHandler } from "../../engine-components/postprocessing/PostProcessingHandler";
110
110
  import { PresentationMode } from "../../engine-components-experimental/Presentation";
111
+ import { QuickLookOverlay } from "../../engine-components/export/usdz/USDZExporter";
111
112
  import { RawImage } from "../../engine-components/ui/Image";
112
113
  import { Raycaster } from "../../engine-components/ui/Raycaster";
113
114
  import { Rect } from "../../engine-components/ui/RectTransform";
@@ -296,6 +297,7 @@ TypeStore.add("PlayerSync", PlayerSync);
296
297
  TypeStore.add("PointerEventData", PointerEventData);
297
298
  TypeStore.add("PostProcessingHandler", PostProcessingHandler);
298
299
  TypeStore.add("PresentationMode", PresentationMode);
300
+ TypeStore.add("QuickLookOverlay", QuickLookOverlay);
299
301
  TypeStore.add("RawImage", RawImage);
300
302
  TypeStore.add("Raycaster", Raycaster);
301
303
  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
  }
@@ -103,6 +103,7 @@ export { PlayableDirector } from "../timeline/PlayableDirector";
103
103
  export { PlayerColor } from "../PlayerColor";
104
104
  export { PointerEventData } from "../ui/PointerEvents";
105
105
  export { PostProcessingHandler } from "../postprocessing/PostProcessingHandler";
106
+ export { QuickLookOverlay } from "../export/usdz/USDZExporter";
106
107
  export { RawImage } from "../ui/Image";
107
108
  export { Raycaster } from "../ui/Raycaster";
108
109
  export { Rect } from "../ui/RectTransform";
@@ -10,16 +10,23 @@ import { IUSDZExporterExtension } from "./Extension";
10
10
  import { Behaviour, GameObject } from "../../Component";
11
11
  import { WebXR } from "../../webxr/WebXR"
12
12
  import { serializable } from "../../../engine/engine_serialization";
13
- import { showBalloonWarning } from "../../../engine/debug/debug";
13
+ import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../../../engine/debug/debug";
14
14
  import { Context } from "../../../engine/engine_setup";
15
15
  import { WebARSessionRoot } from "../../webxr/WebARSessionRoot";
16
16
 
17
17
  const debug = getParam("debugusdz");
18
18
 
19
- export type QuickLookOverlay = {
19
+ export class QuickLookOverlay {
20
+ @serializable()
20
21
  callToAction?: string;
22
+ @serializable()
21
23
  checkoutTitle?: string;
24
+ @serializable()
22
25
  checkoutSubtitle?: string;
26
+
27
+ /** if assigned the call to action button in quicklook will open the URL. Otherwise it will just close quicklook. */
28
+ @serializable()
29
+ callToActionURL?: string;
23
30
  }
24
31
 
25
32
  export class USDZExporter extends Behaviour {
@@ -30,6 +37,9 @@ export class USDZExporter extends Behaviour {
30
37
  @serializable()
31
38
  autoExportAnimations: boolean = false;
32
39
 
40
+ @serializable(QuickLookOverlay)
41
+ overlay?: QuickLookOverlay;
42
+
33
43
  extensions: IUSDZExporterExtension[] = [];
34
44
 
35
45
  private link!: HTMLAnchorElement;
@@ -39,6 +49,8 @@ export class USDZExporter extends Behaviour {
39
49
 
40
50
  start() {
41
51
  if (debug) {
52
+ console.log(this);
53
+ console.log("Debug USDZ, press 't' to export")
42
54
  window.addEventListener("keydown", (evt) => {
43
55
  switch (evt.key) {
44
56
  case "t":
@@ -58,6 +70,12 @@ export class USDZExporter extends Behaviour {
58
70
  });
59
71
 
60
72
  if (!this.objectToExport) this.objectToExport = this.gameObject;
73
+
74
+
75
+ if (isDevEnvironment() && (!this.objectToExport || this.objectToExport.children.length <= 0)) {
76
+ showBalloonWarning("USDZ Exporter has nothing to export");
77
+ console.warn("USDZExporter has no objects to export assigned:", this)
78
+ }
61
79
  }
62
80
 
63
81
 
@@ -66,16 +84,25 @@ export class USDZExporter extends Behaviour {
66
84
  const ios = isiOS()
67
85
  const safari = isSafari();
68
86
  if (debug || (ios && safari)) {
69
- this.createQuicklookButton();
87
+ this.addQuicklookButton();
70
88
  this.webARSessionRoot = GameObject.findObjectOfType(WebARSessionRoot) ?? undefined;
71
89
  this.lastCallback = this.quicklookCallback.bind(this);
72
90
  this.link = ensureQuicklookLinkIsCreated(this.context);
73
91
  this.link.addEventListener('message', this.lastCallback);
74
92
  }
93
+ if (debug)
94
+ showBalloonMessage("USDZ Exporter enabled: " + this.name);
75
95
  }
76
96
 
77
97
  onDisable() {
78
98
  this.link?.removeEventListener('message', this.lastCallback);
99
+ const ios = isiOS()
100
+ const safari = isSafari();
101
+ if (debug || (ios && safari)) {
102
+ this.removeQuicklookButton();
103
+ }
104
+ if (debug)
105
+ showBalloonMessage("USDZ Exporter disabled: " + this.name);
79
106
  }
80
107
 
81
108
  async exportAsync() {
@@ -125,14 +152,15 @@ export class USDZExporter extends Behaviour {
125
152
 
126
153
  // see https://developer.apple.com/documentation/arkit/adding_an_apple_pay_button_or_a_custom_action_in_ar_quick_look
127
154
  const overlay = this.buildQuicklookOverlay();
155
+ console.log(overlay);
128
156
  const callToAction = overlay.callToAction ? encodeURIComponent(overlay.callToAction) : "";
129
157
  const checkoutTitle = overlay.checkoutTitle ? encodeURIComponent(overlay.checkoutTitle) : "";
130
158
  const checkoutSubtitle = overlay.checkoutSubtitle ? encodeURIComponent(overlay.checkoutSubtitle) : "";
131
- this.link.href = URL.createObjectURL(blob) + `#callToAction=${callToAction}&checkoutTitle=${checkoutTitle}&checkoutSubtitle=${checkoutSubtitle}&`;
159
+ this.link.href = URL.createObjectURL(blob) + `#callToAction=${callToAction}&checkoutTitle=${checkoutTitle}&checkoutSubtitle=${checkoutSubtitle}&callToActionURL=${overlay.callToActionURL}`;
132
160
 
133
161
 
134
162
  if (!this.lastCallback) {
135
- this.lastCallback = this.quicklookCallback.bind(this);
163
+ this.lastCallback = this.quicklookCallback.bind(this)
136
164
  this.link.addEventListener('message', this.lastCallback);
137
165
  }
138
166
 
@@ -146,24 +174,44 @@ export class USDZExporter extends Behaviour {
146
174
 
147
175
  private lastCallback?: any;
148
176
 
149
- private quicklookCallback(event) {
177
+ private quicklookCallback(event: Event) {
150
178
  if ((event as any)?.data == '_apple_ar_quicklook_button_tapped') {
151
179
  if (debug) showBalloonWarning("Quicklook closed via call to action button");
152
- this.dispatchEvent(new CustomEvent("quicklook-button-tapped", { detail: this }));
180
+ var evt = new CustomEvent("quicklook-button-tapped", { detail: this });
181
+ this.dispatchEvent(evt);
182
+ if (!evt.defaultPrevented) {
183
+ const url = new URLSearchParams(this.link.href);
184
+ if (url) {
185
+ const callToActionURL = url.get("callToActionURL");
186
+ if (debug)
187
+ showBalloonMessage("Quicklook url: " + callToActionURL);
188
+ if (callToActionURL) {
189
+ globalThis.open(callToActionURL, "_blank");
190
+ }
191
+ }
192
+ }
153
193
  }
154
194
  }
155
195
 
156
196
  private buildQuicklookOverlay(): QuickLookOverlay {
157
197
  const obj: QuickLookOverlay = {};
158
- obj.callToAction = "Close";
159
- obj.checkoutTitle = "🌵 Made with Needle";
160
- obj.checkoutSubtitle = "_";
198
+ if (this.overlay) Object.assign(obj, this.overlay);
199
+ if (!obj.callToAction?.length)
200
+ obj.callToAction = "Close";
201
+ if (!obj.checkoutTitle?.length)
202
+ obj.checkoutTitle = "🌵 Made with Needle";
203
+ if (!obj.checkoutSubtitle?.length)
204
+ obj.checkoutSubtitle = "_";
161
205
  // Use the quicklook-overlay event to customize the overlay
162
206
  this.dispatchEvent(new CustomEvent("quicklook-overlay", { detail: obj }));
163
207
  return obj;
164
208
  }
165
209
 
166
- private _arButton?: HTMLElement;
210
+
211
+
212
+
213
+ private _quicklookButton?: HTMLElement;
214
+
167
215
  private async createQuicklookButton() {
168
216
  if (!this.webxr) {
169
217
  await delay(1);
@@ -171,8 +219,8 @@ export class USDZExporter extends Behaviour {
171
219
  if (this.webxr) {
172
220
  if (this.webxr.VRButton) this.webxr.VRButton.parentElement?.removeChild(this.webxr.VRButton);
173
221
  // 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;
222
+ if (this.webxr.ARButton && this._quicklookButton !== this.webxr.ARButton) {
223
+ this._quicklookButton = this.webxr.ARButton;
176
224
  // Hack to remove the immersiveweb link
177
225
  const linkInButton = this.webxr.ARButton.parentElement?.querySelector("a");
178
226
  if (linkInButton) {
@@ -185,6 +233,7 @@ export class USDZExporter extends Behaviour {
185
233
  this.exportAsync();
186
234
  });
187
235
  this.webxr.ARButton.classList.add("quicklook-ar-button");
236
+ this._quicklookButtonContainer = this.webxr.ARButton.parentElement;
188
237
  this.dispatchEvent(new CustomEvent("created-button", { detail: this.webxr.ARButton }))
189
238
  }
190
239
  // create a button if WebXR didnt create one yet
@@ -205,6 +254,7 @@ export class USDZExporter extends Behaviour {
205
254
  button.classList.add('webxr-button');
206
255
  button.classList.add("quicklook-ar-button");
207
256
  container.appendChild(button);
257
+ this._quicklookButtonContainer = container;
208
258
  this.dispatchEvent(new CustomEvent("created-button", { detail: button }))
209
259
  }
210
260
  }
@@ -214,20 +264,15 @@ export class USDZExporter extends Behaviour {
214
264
  }
215
265
  }
216
266
 
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 = "";
267
+
268
+ private _quicklookButtonContainer: Element | null = null;
269
+ private async addQuicklookButton() {
270
+ await this.createQuicklookButton();
271
+ if (this._quicklookButton && this._quicklookButtonContainer) {
272
+ this._quicklookButtonContainer.appendChild(this._quicklookButton);
273
+ }
274
+ }
275
+ private removeQuicklookButton() {
276
+ this._quicklookButton?.remove();
232
277
  }
233
278
  }