@needle-tools/engine 4.5.4 → 4.5.5

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 (42) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/{needle-engine.bundle-6da875f4.light.js → needle-engine.bundle-454a0b60.light.js} +3780 -3724
  3. package/dist/{needle-engine.bundle-9f1a1512.min.js → needle-engine.bundle-707758a3.min.js} +114 -114
  4. package/dist/{needle-engine.bundle-8959a299.umd.cjs → needle-engine.bundle-bca99998.umd.cjs} +108 -108
  5. package/dist/{needle-engine.bundle-49a4fe2f.js → needle-engine.bundle-ca7fba70.js} +3399 -3343
  6. package/dist/{needle-engine.bundle-3fd607cc.light.umd.cjs → needle-engine.bundle-e8cb9309.light.umd.cjs} +108 -108
  7. package/dist/{needle-engine.bundle-3de53f53.light.min.js → needle-engine.bundle-ebd3952c.light.min.js} +113 -113
  8. package/dist/needle-engine.js +151 -154
  9. package/dist/needle-engine.light.js +151 -154
  10. package/dist/needle-engine.light.min.js +1 -1
  11. package/dist/needle-engine.light.umd.cjs +1 -1
  12. package/dist/needle-engine.min.js +1 -1
  13. package/dist/needle-engine.umd.cjs +1 -1
  14. package/lib/engine/debug/debug_console.js +1 -1
  15. package/lib/engine/debug/debug_console.js.map +1 -1
  16. package/lib/engine/engine_create_objects.d.ts +1 -0
  17. package/lib/engine/engine_create_objects.js +13 -1
  18. package/lib/engine/engine_create_objects.js.map +1 -1
  19. package/lib/engine/engine_license.js +16 -8
  20. package/lib/engine/engine_license.js.map +1 -1
  21. package/lib/engine/engine_utils.d.ts +5 -23
  22. package/lib/engine/engine_utils.js +147 -32
  23. package/lib/engine/engine_utils.js.map +1 -1
  24. package/lib/engine/webcomponents/buttons.js +4 -3
  25. package/lib/engine/webcomponents/buttons.js.map +1 -1
  26. package/lib/engine/xr/NeedleXRSession.d.ts +1 -0
  27. package/lib/engine/xr/NeedleXRSession.js +16 -1
  28. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  29. package/lib/engine-components/webxr/WebXRImageTracking.js +1 -0
  30. package/lib/engine-components/webxr/WebXRImageTracking.js.map +1 -1
  31. package/package.json +1 -1
  32. package/plugins/types/userconfig.d.ts +7 -0
  33. package/plugins/types/vite.d.ts +2 -2
  34. package/plugins/vite/index.js +4 -0
  35. package/plugins/vite/local-files.js +232 -0
  36. package/src/engine/debug/debug_console.ts +1 -1
  37. package/src/engine/engine_create_objects.ts +14 -1
  38. package/src/engine/engine_license.ts +16 -8
  39. package/src/engine/engine_utils.ts +168 -47
  40. package/src/engine/webcomponents/buttons.ts +4 -3
  41. package/src/engine/xr/NeedleXRSession.ts +17 -2
  42. package/src/engine-components/webxr/WebXRImageTracking.ts +1 -0
@@ -1,3 +1,4 @@
1
+ import { isDevEnvironment } from "./debug/index.js";
1
2
  import { BUILD_TIME, GENERATOR, PUBLIC_KEY, VERSION } from "./engine_constants.js";
2
3
  import { ContextEvent, ContextRegistry } from "./engine_context_registry.js";
3
4
  import { Context } from "./engine_setup.js";
@@ -68,8 +69,8 @@ function invokeLicenseCheckResultChanged(result: boolean) {
68
69
 
69
70
  ContextRegistry.registerCallback(ContextEvent.ContextRegistered, evt => {
70
71
  showLicenseInfo(evt.context);
71
- sendUsageMessageToAnalyticsBackend();
72
72
  handleForbidden(evt.context);
73
+ setTimeout(() => sendUsageMessageToAnalyticsBackend(evt.context), 2000);
73
74
  });
74
75
 
75
76
  export let runtimeLicenseCheckPromise: Promise<void> | undefined = undefined;
@@ -336,16 +337,23 @@ async function logNonCommercialUse(_logo?: string) {
336
337
  }
337
338
 
338
339
 
339
- async function sendUsageMessageToAnalyticsBackend() {
340
+ async function sendUsageMessageToAnalyticsBackend(context: IContext) {
340
341
  // We can't send beacons from cross-origin isolated pages
341
342
  if (window.crossOriginIsolated) return;
342
343
 
344
+ const licenseType = NEEDLE_ENGINE_LICENSE_TYPE;
345
+ if (licenseType === "pro") {
346
+ const attribute = context?.domElement?.getAttribute("no-telemetry");
347
+ if (attribute === "" || attribute === "true" || attribute === "1") {
348
+ if (debug) console.debug("Telemetry is disabled");
349
+ return;
350
+ }
351
+ if (debug) console.debug("Telemetry attribute: " + attribute);
352
+ }
353
+
343
354
  try {
344
- const analyticsUrl = "https://needle-engine-analytics-v2-r26roub2hq-lz.a.run.app";
355
+ const analyticsUrl = "https:" + "//needle-engine-an" + "alytics-v2-r26roub2hq-lz" + ".a.run.app";
345
356
  if (analyticsUrl) {
346
- if (debug) console.log("Analytics backend url", analyticsUrl);
347
-
348
- // analyticsUrl = "http://localhost:3000/";
349
357
 
350
358
  // current url without query parameters
351
359
  const currentUrl = window.location.href.split("?")[0];
@@ -354,7 +362,7 @@ async function sendUsageMessageToAnalyticsBackend() {
354
362
  if (!analyticsUrl.endsWith("/")) endpoint = "/" + endpoint;
355
363
  const license = NEEDLE_ENGINE_LICENSE_TYPE;
356
364
  const finalUrl = `${analyticsUrl}${endpoint}`;
357
- if (debug) console.log("Sending non-commercial usage message to analytics backend", finalUrl);
365
+ if (debug) console.debug("Sending beacon");
358
366
 
359
367
  const beaconData = {
360
368
  license,
@@ -369,7 +377,7 @@ async function sendUsageMessageToAnalyticsBackend() {
369
377
  public_key: PUBLIC_KEY,
370
378
  };
371
379
  const res = navigator.sendBeacon?.(finalUrl, JSON.stringify(beaconData));
372
- if (debug) console.log("Send beacon result", res);
380
+ if (debug) console.debug("Sent beacon: " + res);
373
381
  }
374
382
  }
375
383
  catch (err) {
@@ -3,9 +3,11 @@ import { Quaternion, Vector2, Vector3, Vector4 } from "three";
3
3
 
4
4
  declare type Vector = Vector2 | Vector3 | Vector4 | Quaternion;
5
5
 
6
- import { type Context } from "./engine_context.js";
6
+ import { needleLogoOnlySVG } from "./assets/index.js";
7
+ import type { Context } from "./engine_context.js";
7
8
  import { ContextRegistry } from "./engine_context_registry.js";
8
9
  import { type SourceIdentifier } from "./engine_types.js";
10
+ import type { NeedleEngineHTMLElement } from "./webcomponents/needle-engine.js";
9
11
 
10
12
  // https://schneidenbach.gitbooks.io/typescript-cookbook/content/nameof-operator.html
11
13
  /** @internal */
@@ -779,50 +781,7 @@ export async function microphonePermissionsGranted() {
779
781
  return DeviceUtilities.microphonePermissionsGranted();
780
782
  }
781
783
 
782
- const cloudflareIPRegex = /ip=(?<ip>.+?)\n/s;
783
- export async function getIpCloudflare() {
784
- const data = await fetch('https://www.cloudflare.com/cdn-cgi/trace');
785
- const body = await data.text();
786
- // we are only interested in the ip= part:
787
- const match = cloudflareIPRegex.exec(body);
788
- if (match)
789
- return match[1];
790
- return null;
791
- }
792
-
793
- /** Gets the public IP address of this device.
794
- * @returns IP address, or `undefined` when it can't be determined.
795
- */
796
- export async function getIp() {
797
- const res = (await fetch("https://api.db-ip.com/v2/free/self").catch(() => null));
798
- if (!res) return undefined;
799
- const json = await res.json() as IpAndLocation;
800
- return json.ipAddress;
801
- }
802
784
 
803
- /**
804
- * Contains information about public IP, continent, country, state, city.
805
- * This information may be affected by VPNs, proxies, or other network configurations.
806
- */
807
- export type IpAndLocation = {
808
- ipAddress: string;
809
- continentCode: string;
810
- continentName: string;
811
- countryCode: string;
812
- countryName: string;
813
- stateProv: string;
814
- city: string;
815
- }
816
-
817
- /** Gets the public IP address, location, and country data of this device.
818
- * @returns an object containing {@link IpAndLocation} data, or `undefined` when it can't be determined.
819
- */
820
- export async function getIpAndLocation(): Promise<IpAndLocation | undefined> {
821
- const res = (await fetch("https://api.db-ip.com/v2/free/self").catch(() => null));
822
- if (!res) return undefined;
823
- const json = await res.json() as IpAndLocation;
824
- return json;
825
- }
826
785
 
827
786
  declare type AttributeChangeCallback = (value: string | null) => void;
828
787
  declare type HtmlElementExtra = {
@@ -937,12 +896,14 @@ export async function PromiseAllWithErrors<T>(promise: Promise<T>[]): Promise<{
937
896
  * @param args.colorDark The color of the dark squares
938
897
  * @param args.colorLight The color of the light squares
939
898
  * @param args.correctLevel The error correction level to use
899
+ * @param args.showLogo If true, the same logo as for the Needle loading screen will be drawn in the center of the QR code
900
+ * @param args.showUrl If true, the URL will be shown below the QR code
940
901
  * @param args.domElement The dom element to append the QR code to. If not provided a new div will be created and returned
941
902
  * @returns The dom element containing the QR code
942
903
  */
943
- export async function generateQRCode(args: { domElement?: HTMLElement, text: string, width?: number, height?: number, colorDark?: string, colorLight?: string, correctLevel?: any }): Promise<HTMLElement> {
904
+ export async function generateQRCode(args: { domElement?: HTMLElement, text: string, width?: number, height?: number, colorDark?: string, colorLight?: string, correctLevel?: any, showLogo?: boolean, showUrl?: boolean }): Promise<HTMLElement> {
944
905
 
945
- // ensure that the QRCode library is loaded
906
+ // Ensure that the QRCode library is loaded
946
907
  if (!globalThis["QRCode"]) {
947
908
  const url = "https://cdn.rawgit.com/davidshimjs/qrcodejs/gh-pages/qrcode.min.js";
948
909
  let script = document.head.querySelector(`script[src="${url}"]`) as HTMLScriptElement;
@@ -961,7 +922,7 @@ export async function generateQRCode(args: { domElement?: HTMLElement, text: str
961
922
 
962
923
  const QRCODE = globalThis["QRCode"];
963
924
  const target = args.domElement ?? document.createElement("div");
964
- new QRCODE(target, {
925
+ const qrCode = new QRCODE(target, {
965
926
  width: args.width ?? 256,
966
927
  height: args.height ?? 256,
967
928
  colorDark: "#000000",
@@ -969,5 +930,165 @@ export async function generateQRCode(args: { domElement?: HTMLElement, text: str
969
930
  correctLevel: QRCODE.CorrectLevel.M,
970
931
  ...args,
971
932
  });
933
+
934
+ if (args.showLogo !== false) {
935
+ const canvas = qrCode?._oDrawing?._elCanvas as HTMLCanvasElement;
936
+ try {
937
+ const img = await addOverlays(canvas).catch(_e => { /** ignore */ });
938
+ if (img) {
939
+ target.innerHTML = "";
940
+ target.append(img);
941
+ }
942
+ }
943
+ catch { } // Ignore
944
+ }
945
+
946
+ if (args.showUrl !== false && args.text) {
947
+ // Add link label below the QR code
948
+ // Clean up the text. If it's a URL: remove the protocol, www. part, trailing slashes or trailing question marks
949
+ const existingLabel = target.querySelector(".qr-code-link-label");
950
+ let displayText = args.text.replace(/^(https?:\/\/)?(www\.)?/, "").replace(/\/+$/, "").replace(/\?+$/, "");
951
+ displayText = "Scan to visit: " + displayText;
952
+ if (existingLabel) {
953
+ existingLabel.textContent = displayText;
954
+ } else {
955
+ // Create a new label
956
+ const linkLabel = document.createElement("div");
957
+ linkLabel.classList.add("qr-code-link-label");
958
+ args.text = displayText;
959
+ linkLabel.textContent = args.text;
960
+ linkLabel.addEventListener("click", (ev) => {
961
+ // Prevent the QR panel from closing
962
+ ev.stopImmediatePropagation();
963
+ });
964
+
965
+ linkLabel.style.textAlign = "center";
966
+ linkLabel.style.fontSize = "0.8em";
967
+ linkLabel.style.marginTop = "0.1em";
968
+ linkLabel.style.color = "#000000";
969
+ linkLabel.style.fontFamily = "'Roboto Flex', sans-serif";
970
+ linkLabel.style.opacity = "0.5";
971
+ linkLabel.style.wordBreak = "break-all";
972
+ linkLabel.style.wordWrap = "break-word";
973
+ linkLabel.style.marginBottom = "0.3em";
974
+
975
+ // Ensure max. width
976
+ target.style.width = "calc(210px + 20px)";
977
+
978
+ target.appendChild(linkLabel);
979
+ }
980
+ }
981
+
972
982
  return target;
983
+ }
984
+
985
+ async function addOverlays(canvas: HTMLCanvasElement): Promise<HTMLImageElement | void> {
986
+ if (!canvas) return;
987
+
988
+ // Internal settings
989
+ const canvasPadding = 8;
990
+ const shadowColor = "#00000099";
991
+ const shadowBlur = 20;
992
+ const rectanglePadding = 12;
993
+ const rectangleRadius = 0.4 * 16;
994
+
995
+ // Draw the website's icon in the center of the QR code
996
+ const faviconImage = new Image();
997
+ const element = document.querySelector("needle-engine") as NeedleEngineHTMLElement;
998
+ const logoSrc = element?.getAttribute("loading-logo-src") || needleLogoOnlySVG;
999
+ if (!logoSrc) return;
1000
+
1001
+ faviconImage.src = logoSrc;
1002
+ const haveLogo = await new Promise((resolve, _reject) => {
1003
+ faviconImage.onload = () => resolve(true);
1004
+ faviconImage.onerror = (err) => {
1005
+ console.error("Error loading favicon image for QR code", err);
1006
+ resolve(false);
1007
+ };
1008
+ });
1009
+
1010
+ // Add some padding around the canvas – we need to copy the QR code image to a larger canvas
1011
+ const paddedCanvas = document.createElement("canvas");
1012
+ paddedCanvas.width = canvas.width + canvasPadding;
1013
+ paddedCanvas.height = canvas.height + canvasPadding;
1014
+ const paddedContext = paddedCanvas.getContext("2d");
1015
+ if (!paddedContext) {
1016
+ return;
1017
+ }
1018
+
1019
+ // Clear with white
1020
+ paddedContext.fillStyle = "#ffffff";
1021
+ paddedContext.fillRect(0, 0, paddedCanvas.width, paddedCanvas.height);
1022
+ paddedContext.drawImage(canvas, canvasPadding / 2, canvasPadding / 2);
1023
+
1024
+ // Enable anti-aliasing
1025
+ paddedContext.imageSmoothingEnabled = true;
1026
+ paddedContext.imageSmoothingQuality = "high";
1027
+ // @ts-ignore
1028
+ paddedContext.mozImageSmoothingEnabled = true;
1029
+ // @ts-ignore
1030
+ paddedContext.webkitImageSmoothingEnabled = true;
1031
+
1032
+ // Draw a slight gradient background with 10% opacity and "lighten" composite operation
1033
+ paddedContext.globalCompositeOperation = "lighten";
1034
+ const gradient = paddedContext.createLinearGradient(0, 0, 0, paddedCanvas.height);
1035
+ gradient.addColorStop(0, "rgb(45, 45, 45)");
1036
+ gradient.addColorStop(1, "rgb(45, 45, 45)");
1037
+ paddedContext.fillStyle = gradient;
1038
+ paddedContext.fillRect(0, 0, paddedCanvas.width, paddedCanvas.height);
1039
+ paddedContext.globalCompositeOperation = "source-over";
1040
+
1041
+
1042
+ let sizeX = Math.min(paddedCanvas.width, paddedCanvas.height) / 4;
1043
+ let sizeY = sizeX;
1044
+
1045
+ if (haveLogo) {
1046
+ // get aspect of image
1047
+ const aspect = faviconImage.width / faviconImage.height;
1048
+ if (aspect > 1) sizeY = sizeX / aspect;
1049
+ else sizeX = sizeY * aspect;
1050
+
1051
+ // Apply padding
1052
+ const sizeXPadded = sizeX + rectanglePadding;
1053
+ const sizeYPadded = sizeY + rectanglePadding;
1054
+ const x = (paddedCanvas.width - sizeX) / 2;
1055
+ const y = (paddedCanvas.height - sizeY) / 2;
1056
+
1057
+ // Draw shape with blurred shadow
1058
+ paddedContext.shadowColor = shadowColor;
1059
+ paddedContext.shadowBlur = shadowBlur;
1060
+
1061
+ // Draw rounded rectangle with radius
1062
+ // Convert 0.4rem to pixels, taking DPI into account
1063
+ const radius = rectangleRadius;
1064
+ const xPadded = x - rectanglePadding / 2;
1065
+ const yPadded = y - rectanglePadding / 2;
1066
+ paddedContext.beginPath();
1067
+ paddedContext.moveTo(xPadded + radius, yPadded);
1068
+ paddedContext.lineTo(xPadded + sizeXPadded - radius, yPadded);
1069
+ paddedContext.quadraticCurveTo(xPadded + sizeXPadded, yPadded, xPadded + sizeXPadded, yPadded + radius);
1070
+ paddedContext.lineTo(xPadded + sizeXPadded, yPadded + sizeYPadded - radius);
1071
+ paddedContext.quadraticCurveTo(xPadded + sizeXPadded, yPadded + sizeYPadded, xPadded + sizeXPadded - radius, yPadded + sizeYPadded);
1072
+ paddedContext.lineTo(xPadded + radius, yPadded + sizeYPadded);
1073
+ paddedContext.quadraticCurveTo(xPadded, yPadded + sizeYPadded, xPadded, yPadded + sizeYPadded - radius);
1074
+ paddedContext.lineTo(xPadded, yPadded + radius);
1075
+ paddedContext.quadraticCurveTo(xPadded, yPadded, xPadded + radius, yPadded);
1076
+ paddedContext.fillStyle = "#ffffff";
1077
+ paddedContext.closePath();
1078
+ paddedContext.fill();
1079
+ paddedContext.clip();
1080
+
1081
+ // Reset shadow and draw favicon
1082
+ paddedContext.shadowColor = "transparent";
1083
+ paddedContext.drawImage(faviconImage, x, y, sizeX, sizeY);
1084
+ }
1085
+
1086
+ // Replace the canvas with the padded one
1087
+ const paddedImage = paddedCanvas.toDataURL("image/png");
1088
+ const img = document.createElement("img");
1089
+ img.src = paddedImage;
1090
+ img.style.width = "100%";
1091
+ img.style.height = "auto";
1092
+
1093
+ return img;
973
1094
  }
@@ -190,7 +190,7 @@ export class ButtonsFactory {
190
190
  qrCodeContainer.style.cssText = `
191
191
  position: fixed;
192
192
  display: inline-block;
193
- padding: 1rem;
193
+ padding: 0.5rem;
194
194
  background-color: white;
195
195
  border-radius: 0.4rem;
196
196
  cursor: pointer;
@@ -223,10 +223,11 @@ export class ButtonsFactory {
223
223
  const buttonRect = qrCodeButton.getBoundingClientRect();
224
224
  qrCodeContainer.style.left = (buttonRect.left + buttonRect.width * .5 - containerRect.width * .5) + "px";
225
225
  const isButtonInTopHalf = buttonRect.top < containerRect.height;
226
+ const padding = "1.3rem";
226
227
  if (isButtonInTopHalf)
227
- qrCodeContainer.style.top = `calc(${buttonRect.bottom}px + ${qrCodeContainer.style.padding} * .6)`;
228
+ qrCodeContainer.style.top = `calc(${buttonRect.bottom}px + ${qrCodeContainer.style.padding} + 0.0rem)`;
228
229
  else
229
- qrCodeContainer.style.top = `calc(${buttonRect.top - containerRect.height}px - ${qrCodeContainer.style.padding} * 2.5)`;
230
+ qrCodeContainer.style.top = `calc(${buttonRect.top - containerRect.height}px - ${qrCodeContainer.style.padding} - ${padding})`;
230
231
  qrCodeContainer.style.opacity = "0";
231
232
  qrCodeContainer.style.pointerEvents = "all";
232
233
  qrCodeContainer.style.transition = "opacity 0.2s ease-in-out";
@@ -574,9 +574,24 @@ export class NeedleXRSession implements INeedleXRSession {
574
574
  console.log("%c" + `Started ${mode} session`, "font-weight:bold;");
575
575
  return this._activeSession;
576
576
  }
577
+
578
+ private static $_stop_request = Symbol();
577
579
  /** stops the active XR session */
578
- static stop() {
579
- this._activeSession?.end();
580
+ static stop(): void {
581
+ const session = this._activeSession;
582
+ if (session) {
583
+ if (session[this.$_stop_request] === undefined) {
584
+ if (debug) console.log("[NeedleXRSession] Stopping XR Session... (new)");
585
+ // We can not call session.end() immediately because we might be within a frame callback
586
+ // For example if a use calls `NeedleXRSession.stop()` within a `update()` callback
587
+ session[this.$_stop_request] = setTimeout(() => {
588
+ session.end();
589
+ });
590
+ }
591
+ else if (debug) {
592
+ console.warn("[NeedleXRSession] XR Session stop already requested");
593
+ }
594
+ }
580
595
  }
581
596
  private static onEnd = () => {
582
597
  if (debug) console.log("XR Session ended");
@@ -258,6 +258,7 @@ export class WebXRImageTracking extends Behaviour {
258
258
  for (const trackedImage of this.trackedImages) {
259
259
  if (trackedImage.image) {
260
260
  if (WebXRImageTracking._imageElements.has(trackedImage.image)) {
261
+ // already loaded
261
262
  }
262
263
  else {
263
264
  const url = trackedImage.image;