@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.
- package/CHANGELOG.md +5 -0
- package/dist/{needle-engine.bundle-6da875f4.light.js → needle-engine.bundle-454a0b60.light.js} +3780 -3724
- package/dist/{needle-engine.bundle-9f1a1512.min.js → needle-engine.bundle-707758a3.min.js} +114 -114
- package/dist/{needle-engine.bundle-8959a299.umd.cjs → needle-engine.bundle-bca99998.umd.cjs} +108 -108
- package/dist/{needle-engine.bundle-49a4fe2f.js → needle-engine.bundle-ca7fba70.js} +3399 -3343
- package/dist/{needle-engine.bundle-3fd607cc.light.umd.cjs → needle-engine.bundle-e8cb9309.light.umd.cjs} +108 -108
- package/dist/{needle-engine.bundle-3de53f53.light.min.js → needle-engine.bundle-ebd3952c.light.min.js} +113 -113
- package/dist/needle-engine.js +151 -154
- package/dist/needle-engine.light.js +151 -154
- package/dist/needle-engine.light.min.js +1 -1
- package/dist/needle-engine.light.umd.cjs +1 -1
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/lib/engine/debug/debug_console.js +1 -1
- package/lib/engine/debug/debug_console.js.map +1 -1
- package/lib/engine/engine_create_objects.d.ts +1 -0
- package/lib/engine/engine_create_objects.js +13 -1
- package/lib/engine/engine_create_objects.js.map +1 -1
- package/lib/engine/engine_license.js +16 -8
- package/lib/engine/engine_license.js.map +1 -1
- package/lib/engine/engine_utils.d.ts +5 -23
- package/lib/engine/engine_utils.js +147 -32
- package/lib/engine/engine_utils.js.map +1 -1
- package/lib/engine/webcomponents/buttons.js +4 -3
- package/lib/engine/webcomponents/buttons.js.map +1 -1
- package/lib/engine/xr/NeedleXRSession.d.ts +1 -0
- package/lib/engine/xr/NeedleXRSession.js +16 -1
- package/lib/engine/xr/NeedleXRSession.js.map +1 -1
- package/lib/engine-components/webxr/WebXRImageTracking.js +1 -0
- package/lib/engine-components/webxr/WebXRImageTracking.js.map +1 -1
- package/package.json +1 -1
- package/plugins/types/userconfig.d.ts +7 -0
- package/plugins/types/vite.d.ts +2 -2
- package/plugins/vite/index.js +4 -0
- package/plugins/vite/local-files.js +232 -0
- package/src/engine/debug/debug_console.ts +1 -1
- package/src/engine/engine_create_objects.ts +14 -1
- package/src/engine/engine_license.ts +16 -8
- package/src/engine/engine_utils.ts +168 -47
- package/src/engine/webcomponents/buttons.ts +4 -3
- package/src/engine/xr/NeedleXRSession.ts +17 -2
- 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
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
-
//
|
|
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:
|
|
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}
|
|
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}
|
|
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
|
|
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;
|